From c07ffd513dd4f3947efafaf13337da81f9ed168f Mon Sep 17 00:00:00 2001 From: Stephen Callender Date: Wed, 13 May 2026 10:27:56 -0400 Subject: [PATCH] Prep 2.1.0 --- CHANGELOG.md | 5 ++ docs/getting-started/configuration.md | 6 ++ docs/usage/exporting.md | 2 + docs/usage/importing.md | 4 ++ src/Plugin.php | 28 ++++----- src/ProductExportAssetBundle.php | 24 ++++++++ src/assets/js/product-export.js | 74 ++++++++++++++++++++++++ src/config.php | 2 + src/services/Csv.php | 15 ++++- src/templates/fields/product_export.twig | 55 ++++++------------ src/translations/en/variant-manager.php | 3 + 11 files changed, 161 insertions(+), 57 deletions(-) create mode 100644 src/ProductExportAssetBundle.php create mode 100644 src/assets/js/product-export.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 3abd301..db220fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Release Notes for Variant Manager +## 2.1.0 - 2026-05-13 + +- Add optional `status` column for product imports and exports. +- Refactor the Export Product sidebar button. + ## 2.0.5 - 2026-02-19 - Fix an error that would be thrown when gc runs during a web request diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index 11a146a..a731552 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -14,6 +14,7 @@ return [ '*' => [ 'title' => 'title', 'slug' => 'slug', + 'status' => 'status', ], ], 'variantFieldMap' => [ @@ -67,8 +68,13 @@ Supported field types and formatting: - title - slug +- status - entries +#### `status` column + +Optional. Exports write `enabled` or `disabled`. Imports accept `disabled` (case-insensitive, whitespace trimmed) as disabled; any other value or empty cell imports as enabled. Remove the `'status' => 'status'` entry from `productFieldMap` to skip. + ### `variantFieldMap` The map of column names to variant properties. diff --git a/docs/usage/exporting.md b/docs/usage/exporting.md index b82d154..d76f15e 100644 --- a/docs/usage/exporting.md +++ b/docs/usage/exporting.md @@ -11,3 +11,5 @@ column on the bottom there is an "Export Product" button. Click it, and Variant Manager will generate a CSV file of the current product's variant data and it will download automatically to your computer. + +The exported CSV includes columns for every entry in `productFieldMap` and `variantFieldMap`. If `productFieldMap` includes a `status` entry, the export writes `enabled` or `disabled` based on the current product status. See [Configuration](../getting-started/configuration.md) for the full column list. diff --git a/docs/usage/importing.md b/docs/usage/importing.md index b79e563..75aa4ec 100644 --- a/docs/usage/importing.md +++ b/docs/usage/importing.md @@ -16,6 +16,10 @@ If you have any plain text or number fields setup in your Commerce product type your spreadsheet as well, these columns should also be present and use the name that is configured in the [Variant Manager configuration file](../getting-started/configuration.md) (ex. "notes", "reference", etc) +### Product status column + +If `productFieldMap` includes a `status` entry, the `status` column controls whether the imported product is enabled. Write `disabled` to disable a product on import. Any other value, including an empty cell, imports the product as enabled. See [Configuration](../getting-started/configuration.md) for details. + You will then need to export your spreadsheet as a CSV file from your spreadsheet program and save it. ### For new products diff --git a/src/Plugin.php b/src/Plugin.php index 03744f4..fa25e2d 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -205,33 +205,25 @@ private function registerComponents(): void private function registerViewHooks(): void { - Craft::$app->view->hook('cp.commerce.product.edit.details', static fn (array &$context) => Craft::$app->getView()->renderTemplate( - 'variant-manager/fields/product_export', - [ - 'id' => 'product-export', - 'namespacedId' => 'product-export', - 'name' => 'product-export', - 'product' => $context['product'], - ] - )); - Event::on( Product::class, Element::EVENT_DEFINE_SIDEBAR_HTML, static function (DefineHtmlEvent $event): void { - $entry = $event->sender ?? null; + /** @var Product|null $product */ + $product = $event->sender ?? null; + + $view = Craft::$app->getView(); + $view->registerAssetBundle(ProductExportAssetBundle::class); + $view->registerTranslations('variant-manager', [ + 'Export request failed with status {status}', + ]); - $html = Craft::$app->getView()->renderTemplate( + $event->html .= $view->renderTemplate( 'variant-manager/fields/product_export', [ - 'id' => 'product-export', - 'namespacedId' => 'product-export', - 'name' => 'product-export', - 'product' => $entry, + 'product' => $product, ] ); - - $event->html .= $html; } ); } diff --git a/src/ProductExportAssetBundle.php b/src/ProductExportAssetBundle.php new file mode 100644 index 0000000..7a693c6 --- /dev/null +++ b/src/ProductExportAssetBundle.php @@ -0,0 +1,24 @@ +sourcePath = '@fostercommerce/variantmanager/assets'; + + $this->depends = [ + CpAsset::class, + ]; + + $this->js = [ + 'js/product-export.js', + ]; + + parent::init(); + } +} diff --git a/src/assets/js/product-export.js b/src/assets/js/product-export.js new file mode 100644 index 0000000..4c3b020 --- /dev/null +++ b/src/assets/js/product-export.js @@ -0,0 +1,74 @@ +(function () { + 'use strict'; + + function bindExportButton(button) { + if (button.dataset.variantManagerBound === 'true') { + return; + } + button.dataset.variantManagerBound = 'true'; + + button.addEventListener('click', function (event) { + event.preventDefault(); + + var productId = button.getAttribute('data-product-id'); + if (! productId) { + return; + } + + var exportUrl = button.getAttribute('data-export-url'); + if (! exportUrl) { + return; + } + + fetch(exportUrl + '?ids=' + encodeURIComponent(productId) + '&download=true', { + headers: { + 'X-CSRF-Token': Craft.csrfTokenValue, + 'X-Requested-With': 'XMLHttpRequest', + }, + }).then(function (response) { + if (! response.ok) { + throw new Error( + Craft.t('variant-manager', 'Export request failed with status {status}', { + status: response.status, + }) + ); + } + + return response.blob().then(function (blob) { + var disposition = response.headers.get('content-disposition') || ''; + // Capture only the filename value so surrounding quotes are not included. + // Browsers replace literal quote characters in the download filename with underscores. + var match = disposition.match(/filename="?([^";]+)"?/i); + var filename = match ? match[1] : 'export.csv'; + + var link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(link.href); + }); + }).catch(function (error) { + if (typeof Craft !== 'undefined' && Craft.cp && typeof Craft.cp.displayError === 'function') { + Craft.cp.displayError(error.message); + } else { + console.error(error); + } + }); + }); + } + + function init() { + var buttons = document.querySelectorAll('.js-variant-manager-export'); + for (var index = 0; index < buttons.length; index++) { + bindExportButton(buttons[index]); + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/src/config.php b/src/config.php index 3b2b121..5c46a93 100644 --- a/src/config.php +++ b/src/config.php @@ -22,6 +22,8 @@ 'productFieldMap' => [ '*' => [ 'title' => 'title', + 'slug' => 'slug', + 'status' => 'status', // Add default product fields here ], ], diff --git a/src/services/Csv.php b/src/services/Csv.php index e165ba8..55dc48f 100644 --- a/src/services/Csv.php +++ b/src/services/Csv.php @@ -166,8 +166,9 @@ public function import(string $filename, string $csvData, ?string $productTypeHa */ public function export(string $productId): array|bool { + // status(null) bypasses the default enabled-only filter so disabled products and variants are still exported. /** @var Product|null $product */ - $product = Product::find()->id($productId)->one(); + $product = Product::find()->id($productId)->status(null)->one(); if (! isset($product)) { return false; @@ -175,7 +176,7 @@ public function export(string $productId): array|bool return [ 'filename' => "{$product->id}__{$product->slug}", - 'export' => $this->exportProduct($product, Variant::find()->product($product)->all()), + 'export' => $this->exportProduct($product, Variant::find()->product($product)->status(null)->all()), ]; } @@ -624,7 +625,7 @@ private function resolveVariantImportMapping(TabularDataReader $tabularDataReade private function resolveProductModel(string $title, ?string $productId, ?string $productTypeHandle): Product { if ($productId !== null) { - $product = Product::find()->id($productId)->one(); + $product = Product::find()->id($productId)->status(null)->one(); if ($product === null) { throw new \RuntimeException('Invalid product id'); } @@ -852,6 +853,12 @@ private function applyProductFields(Product $product, array $titleRecord): void return; } + if ($fieldHandle === 'status') { + $normalized = is_string($value) ? strtolower(trim($value)) : ''; + $product->enabled = $normalized !== 'disabled'; + return; + } + $this->setFieldValue($product, $fieldHandle, $value); }); } @@ -976,6 +983,8 @@ private function normalizeProductExport(Product $product, array $mapping): array $row[] = $product->title; } elseif ($fieldHandle === 'slug') { $row[] = $product->slug; + } elseif ($fieldHandle === 'status') { + $row[] = $product->enabled ? 'enabled' : 'disabled'; } else { $value = $product->getFieldValue($fieldHandle); $value = $this->normalizeValue($value); diff --git a/src/templates/fields/product_export.twig b/src/templates/fields/product_export.twig index 6a9bd6b..f64ab3e 100644 --- a/src/templates/fields/product_export.twig +++ b/src/templates/fields/product_export.twig @@ -1,37 +1,20 @@ -{% set canExport = currentUser.can("variant-manager:export") %} -
-
- +{% set canExport = currentUser.can('variant-manager:export') %} +
+ {{ 'Variant Manager'|t('variant-manager') }} +
+
+
+ +
+
-
- - - + diff --git a/src/translations/en/variant-manager.php b/src/translations/en/variant-manager.php index 1d4ba1c..e0e1f3d 100644 --- a/src/translations/en/variant-manager.php +++ b/src/translations/en/variant-manager.php @@ -15,4 +15,7 @@ */ return [ 'Variant Manager plugin loaded' => 'Variant Manager plugin loaded', + 'Variant Manager' => 'Variant Manager', + 'Export Product' => 'Export Product', + 'Export request failed with status {status}' => 'Export request failed with status {status}', ];