diff --git a/CHANGELOG.md b/CHANGELOG.md index e87033e..6659717 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Bulk Edit Changelog +## 2.0.2 - 2020-02-20 +### Added +- All field types (including custom ones and Matrix) now support bulk replacement!!! + ## 2.0.1 - 2020-02-13 ### Fixed - Fixed problem with saving bulk edit jobs in Firefox diff --git a/README.md b/README.md index f936518..ea6a946 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Bulk Edit plugin for Craft CMS 3.2 +# Bulk Edit plugin for Craft CMS 3.2+ ## Overview The Bulk Edit plugin adds an action to supported element index pages that allows you to edit fields on a large number of @@ -24,10 +24,6 @@ Additionally, some fields support different strategies for the edit process. At Craft to pick it up. After the queue has finished, you may reload the page and see your changes. ## Limitation & Issues -* Custom fields and Matrix fields are not currently supported due to issues that arise when a field is rendered without -single entry selected. -* Currently, there isn't a way to edit properties on elements that are not custom fields (for example, title, slug, -post date, etc) * Validation is not enforced when you're editing these fields, this means you can end up with elements with fields in potentially erroneous states (for example, removing all content on a required field) * After the queue finishes running, make sure you refresh the page to see the updates in the element index diff --git a/composer.json b/composer.json index dc73667..0a1f2ec 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "venveo/craft-bulkedit", "description": "Bulk edit entries", "type": "craft-plugin", - "version": "2.0.1", + "version": "2.0.2", "keywords": [ "craft", "cms", diff --git a/src/base/AbstractElementTypeProcessor.php b/src/base/AbstractElementTypeProcessor.php index 70417fe..b84b4a6 100644 --- a/src/base/AbstractElementTypeProcessor.php +++ b/src/base/AbstractElementTypeProcessor.php @@ -2,7 +2,20 @@ namespace venveo\bulkedit\base; +use Craft; +use craft\base\Element; + abstract class AbstractElementTypeProcessor implements ElementTypeProcessorInterface { + public static function getEditableAttributes(): array + { + return []; + } + public static function getMockElement($elementIds = [], $params = []): Element + { + /** @var Element $elementPlaceholder */ + $elementPlaceholder = Craft::createObject(static::getType(), $params); + return $elementPlaceholder; + } } \ No newline at end of file diff --git a/src/base/ElementTypeProcessorInterface.php b/src/base/ElementTypeProcessorInterface.php index 39f068e..b51ca0a 100644 --- a/src/base/ElementTypeProcessorInterface.php +++ b/src/base/ElementTypeProcessorInterface.php @@ -2,6 +2,7 @@ namespace venveo\bulkedit\base; +use craft\base\Element; use craft\web\User; interface ElementTypeProcessorInterface @@ -27,4 +28,8 @@ public static function hasPermission($elementIds, User $user): bool; * @return string */ public static function getType(): string; + + public static function getEditableAttributes(): array; + + public static function getMockElement($elementIds = [], $params = []): Element; } \ No newline at end of file diff --git a/src/controllers/BulkEditController.php b/src/controllers/BulkEditController.php index 85e4f87..b1dce74 100644 --- a/src/controllers/BulkEditController.php +++ b/src/controllers/BulkEditController.php @@ -21,6 +21,7 @@ use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; +use venveo\bulkedit\base\ElementTypeProcessorInterface; use venveo\bulkedit\Plugin; use venveo\bulkedit\services\BulkEdit as BulkEditService; use yii\web\BadRequestHttpException; @@ -81,12 +82,13 @@ public function actionGetFields(): Response /** @var BulkEditService $service */ $service = Plugin::$plugin->bulkEdit; - $fields = $service->getFieldsForElementIds($elementIds, $elementType); - + $fields = $service->getFieldWrappers($elementIds, $elementType); + $attributes = $service->getAttributeWrappers($elementType); $view = Craft::$app->getView(); $modalHtml = $view->renderTemplate('venveo-bulk-edit/elementactions/BulkEdit/_fields', [ 'fieldWrappers' => $fields, + 'attributeWrappers' => $attributes, 'elementType' => $elementType, 'bulkedit' => $service, 'elementIds' => $elementIds, @@ -168,10 +170,18 @@ public function actionGetEditScreen() $view = Craft::$app->getView(); + /** @var ElementTypeProcessorInterface $processor */ + $processor = Plugin::getInstance()->bulkEdit->getElementTypeProcessor($elementType); + $elementPlaceholder = $processor::getMockElement($elementIds, [ + 'siteId' => $siteId + ]); + // We've gotta register any asset bundles - this won't actually be rendered foreach ($fieldModels as $fieldModel) { $view->renderPageTemplate('_includes/field', [ 'field' => $fieldModel, + 'static' => true, + 'element' => $elementPlaceholder, 'required' => false ]); } @@ -179,6 +189,7 @@ public function actionGetEditScreen() $modalHtml = $view->renderTemplate('venveo-bulk-edit/elementactions/BulkEdit/_edit', [ 'fields' => $fieldModels, 'elementType' => $elementType, + 'elementPlaceholder' => $elementPlaceholder, 'elementIds' => $elementIds, 'fieldData' => $enabledFields, 'site' => $site diff --git a/src/elements/processors/AssetProcessor.php b/src/elements/processors/AssetProcessor.php index 915138b..af7d0d3 100644 --- a/src/elements/processors/AssetProcessor.php +++ b/src/elements/processors/AssetProcessor.php @@ -2,6 +2,8 @@ namespace venveo\bulkedit\elements\processors; +use Craft; +use craft\base\Element; use craft\elements\Asset; use craft\helpers\ArrayHelper; use craft\records\FieldLayout; @@ -39,7 +41,7 @@ public static function getLayoutsFromElementIds($elementIds): array */ public static function getType(): string { - return get_class(new Asset); + return Asset::class; } /** @@ -52,4 +54,14 @@ public static function hasPermission($elementIds, User $user): bool { return $user->checkPermission(Plugin::PERMISSION_BULKEDIT_ASSETS); } + + public static function getMockElement($elementIds = [], $params = []): Element + { + $asset = Craft::$app->assets->getAssetById($elementIds[0]); + /** @var Asset $elementPlaceholder */ + $elementPlaceholder = Craft::createObject(static::getType(), $params); + // Field availability is determined by volume ID + $elementPlaceholder->volumeId = $asset->volumeId; + return $elementPlaceholder; + } } \ No newline at end of file diff --git a/src/elements/processors/CategoryProcessor.php b/src/elements/processors/CategoryProcessor.php index 89a10c9..1c937ba 100644 --- a/src/elements/processors/CategoryProcessor.php +++ b/src/elements/processors/CategoryProcessor.php @@ -2,6 +2,8 @@ namespace venveo\bulkedit\elements\processors; +use Craft; +use craft\base\Element; use craft\elements\Category; use craft\helpers\ArrayHelper; use craft\records\CategoryGroup; @@ -48,7 +50,7 @@ public static function getLayoutsFromElementIds($elementIds): array */ public static function getType(): string { - return get_class(new Category); + return Category::class; } /** @@ -61,4 +63,14 @@ public static function hasPermission($elementIds, User $user): bool { return $user->checkPermission(Plugin::PERMISSION_BULKEDIT_CATEGORIES); } + + public static function getMockElement($elementIds = [], $params = []): Element + { + $category = Craft::$app->categories->getCategoryById($elementIds[0]); + /** @var Category $elementPlaceholder */ + $elementPlaceholder = Craft::createObject(static::getType(), $params); + // Field availability is determined by volume ID + $elementPlaceholder->groupId = $category->groupId; + return $elementPlaceholder; + } } \ No newline at end of file diff --git a/src/elements/processors/EntryProcessor.php b/src/elements/processors/EntryProcessor.php index 2947690..a49db5c 100644 --- a/src/elements/processors/EntryProcessor.php +++ b/src/elements/processors/EntryProcessor.php @@ -2,11 +2,14 @@ namespace venveo\bulkedit\elements\processors; +use Craft; +use craft\base\Element; use craft\elements\Entry; use craft\records\FieldLayout; use craft\web\User; use venveo\bulkedit\base\AbstractElementTypeProcessor; use venveo\bulkedit\Plugin; +use venveo\bulkedit\services\BulkEdit; class EntryProcessor extends AbstractElementTypeProcessor { @@ -36,7 +39,7 @@ public static function getLayoutsFromElementIds($elementIds): array */ public static function getType(): string { - return get_class(new Entry); + return Entry::class; } /** @@ -49,4 +52,36 @@ public static function hasPermission($elementIds, User $user): bool { return $user->checkPermission(Plugin::PERMISSION_BULKEDIT_ENTRIES); } + + /** + * @return array + */ + public static function getEditableAttributes(): array { +// return [ +// [ +// 'name' => 'Title', +// 'handle' => 'title', +// 'strategies' => [ +// BulkEdit::STRATEGY_REPLACE +// ] +// ] +// ]; + return []; + } + + /** + * @param array $elementIds + * @param array $params + * @return Element + * @throws \yii\base\InvalidConfigException + */ + public static function getMockElement($elementIds = [], $params = []): Element + { + $templateEntry = Craft::$app->entries->getEntryById($elementIds[0]); + /** @var Entry $entry */ + $entry = Craft::createObject(self::getType(), $params); + $entry->typeId = $templateEntry->typeId; + $entry->sectionId = $templateEntry->sectionId; + return $entry; + } } \ No newline at end of file diff --git a/src/elements/processors/ProductProcessor.php b/src/elements/processors/ProductProcessor.php index 881a1dd..f0d4cf4 100644 --- a/src/elements/processors/ProductProcessor.php +++ b/src/elements/processors/ProductProcessor.php @@ -2,6 +2,8 @@ namespace venveo\bulkedit\elements\processors; +use Craft; +use craft\base\Element; use craft\commerce\records\Product; use craft\commerce\records\ProductType; use craft\helpers\ArrayHelper; @@ -48,7 +50,7 @@ public static function getLayoutsFromElementIds($elementIds): array */ public static function getType(): string { - return get_class(new \craft\commerce\elements\Product); + return \craft\commerce\elements\Product::class; } /** @@ -61,4 +63,15 @@ public static function hasPermission($elementIds, User $user): bool { return $user->checkPermission(Plugin::PERMISSION_BULKEDIT_PRODUCTS); } + + public static function getMockElement($elementIds = [], $params = []): Element + { + /** @var \craft\commerce\elements\Product $product */ + $product = \Craft::$app->elements->getElementById($elementIds[0], \craft\commerce\elements\Product::class); + /** @var \craft\commerce\elements\Product $elementPlaceholder */ + $elementPlaceholder = Craft::createObject(static::getType(), $params); + // Field availability is determined by volume ID + $elementPlaceholder->typeId = $product->typeId; + return $elementPlaceholder; + } } \ No newline at end of file diff --git a/src/elements/processors/UserProcessor.php b/src/elements/processors/UserProcessor.php index 6af457d..a57980d 100644 --- a/src/elements/processors/UserProcessor.php +++ b/src/elements/processors/UserProcessor.php @@ -34,7 +34,7 @@ public static function getLayoutsFromElementIds($elementIds): array */ public static function getType(): string { - return get_class(new User); + return User::class; } /** diff --git a/src/fields/processors/PlainTextProcessor.php b/src/fields/processors/PlainTextProcessor.php index 4db004f..7f40c67 100644 --- a/src/fields/processors/PlainTextProcessor.php +++ b/src/fields/processors/PlainTextProcessor.php @@ -15,6 +15,7 @@ use craft\fields\Table; use craft\fields\Url; use craft\redactor\Field as RedactorField; +use fruitstudios\linkit\fields\LinkitField; use venveo\bulkedit\base\AbstractFieldProcessor; use venveo\bulkedit\services\BulkEdit; @@ -27,25 +28,7 @@ class PlainTextProcessor extends AbstractFieldProcessor */ public static function getSupportedFields(): array { - $fields = [ - PlainText::class, - Color::class, - Checkboxes::class, - Dropdown::class, - Date::class, - Table::class, - RadioButtons::class, - Lightswitch::class, - Url::class, - Email::class, - MultiSelect::class - ]; - - if (Craft::$app->plugins->isPluginInstalled('redactor')) { - $fields[] = RedactorField::class; - } - - return $fields; + return Craft::$app->fields->getAllFieldTypes(); } /** diff --git a/src/fields/processors/RelationFieldProcessor.php b/src/fields/processors/RelationFieldProcessor.php index fd073ba..a5115da 100644 --- a/src/fields/processors/RelationFieldProcessor.php +++ b/src/fields/processors/RelationFieldProcessor.php @@ -17,11 +17,9 @@ class RelationFieldProcessor extends AbstractFieldProcessor */ public static function getSupportedFields(): array { - $fields = [ + return [ BaseRelationField::class ]; - - return $fields; } /** diff --git a/src/models/AttributeWrapper.php b/src/models/AttributeWrapper.php new file mode 100644 index 0000000..b3e83ff --- /dev/null +++ b/src/models/AttributeWrapper.php @@ -0,0 +1,25 @@ +getElementTypeProcessor($elementType); if (!$processor) { throw new Exception('Unable to process element type'); @@ -95,6 +100,23 @@ public function getFieldsForElementIds($elementIds, $elementType): array return $fields; } + public function getAttributeWrappers($elementType): array { + + /** @var ElementTypeProcessorInterface $processor */ + $processor = $this->getElementTypeProcessor($elementType); + if (!$processor) { + throw new Exception('Unable to process element type'); + } + + $attributes = $processor::getEditableAttributes(); + return array_map(function($attribute) { + return new AttributeWrapper([ + 'handle' => $attribute['handle'], + 'name' => $attribute['name'] + ]); + }, $attributes); + } + /** * Retrieves the processor for a type of element. The processor determines how to do things like get the field * layout. @@ -169,6 +191,7 @@ public function getPendingElementsHistoriesFromContext(EditContext $context): Ac * Gets all pending bulk edit changes for a particular job * * @param EditContext $context + * @param null $elementId * @return ActiveQueryInterface */ public function getPendingHistoryFromContext(EditContext $context, $elementId = null): ActiveQueryInterface diff --git a/src/templates/elementactions/BulkEdit/_edit.twig b/src/templates/elementactions/BulkEdit/_edit.twig index 8a484bb..6e7a9fa 100644 --- a/src/templates/elementactions/BulkEdit/_edit.twig +++ b/src/templates/elementactions/BulkEdit/_edit.twig @@ -13,20 +13,21 @@ {% include "_includes/field" with { field: field, required: false, + element: elementPlaceholder, siteId: site.id } only %} {% endnamespace %}
-
Cancel
+
{{"Cancel"|t('venveo-bulk-edit')}}
{% else %} -

It doesn't look like there are any fields that support bulk editing on these elements.

+

{{"It doesn't look like there are any fields that support bulk editing on these elements."|t('venveo-bulk-edit')}}

-
Cancel
+
{{"Cancel"|t('venveo-bulk-edit')}}
{% endfor %} diff --git a/src/templates/elementactions/BulkEdit/_fields.twig b/src/templates/elementactions/BulkEdit/_fields.twig index e1522f8..7aff38e 100644 --- a/src/templates/elementactions/BulkEdit/_fields.twig +++ b/src/templates/elementactions/BulkEdit/_fields.twig @@ -1,91 +1,135 @@ {% import '_includes/forms' as forms %}
-

Select Fields

-

Editing {{ elementIds|length }} elements on site - {{ site.name }} ({{ site.language }})

- {% if fieldWrappers|length %} +

{{ "Select Fields"|t('venveo-bulk-edit') }}

+

Editing {{ elementIds|length }} elements on site + {{ site.name }} ({{ site.language }})

+ {% if fieldWrappers|length or attributeWrappers|length %}
- {{ csrfInput() }} - {{ actionInput('bulkedit/bulk-edit/edit') }} - - - - - - + {% endfor %} +
+ {{ csrfInput() }} + {{ actionInput('bulkedit/bulk-edit/edit') }} + + + + + {% if fieldWrappers|length %} + + + - + - + - + - - {# fieldWrapper #} - {% for fieldWrapper in fieldWrappers %} - {% set field = fieldWrapper.field %} - {% set fieldUnsupported = false %} - {% if not bulkedit.isFieldSupported(field) %} - {% set fieldUnsupported = true %} - {% endif %} - - + - - + + - + - - {% endfor %} -
Field Name - + Field Handle - + Edit - + Strategy -
+ + + {# fieldWrapper #} + {% for fieldWrapper in fieldWrappers %} + {% set field = fieldWrapper.field %} + {% set fieldUnsupported = false %} + {% if not bulkedit.isFieldSupported(field) %} + {% set fieldUnsupported = true %} + {% endif %} +
- {{ field.name | t('site') }} {% if field.instructions %}{{ field.instructions | t('site') | e | md }}{% endif %} + {{ field.name | t('site') }} {% if field.instructions %}{{ field.instructions | t('site') | e | md }}{% endif %} - {{ field.handle }} + {{ field.handle }} {{ forms.lightswitchField({ - id: 'fieldEnabled-'~field.handle, - name: 'fields['~field.id~'][enabled]', - on: false, - disabled: fieldUnsupported, - value: true, + id: 'fieldEnabled-'~field.handle, + name: 'fields['~field.id~'][enabled]', + on: false, + disabled: fieldUnsupported, + value: true, }) }} - + + {{ forms.selectField({ - id: 'fieldStrategy-'~field.handle, - name: 'fields['~field.id~'][strategy]', - disabled: fieldUnsupported, - options: bulkedit.getSupportedStrategiesForField(field), - default: 'replace', + id: 'fieldStrategy-'~field.handle, + name: 'fields['~field.id~'][strategy]', + disabled: fieldUnsupported, + options: bulkedit.getSupportedStrategiesForField(field), + default: 'replace', }) }} -
+ +
+ {% endif %} + {% else %} +

These elements have no associated fields.

+ {% endif %} -
-
-
Cancel
-
-
+ {% if attributeWrappers|length %} + + + + + + + {# fieldWrapper #} + {% for attributeWrapper in attributeWrappers %} + + + + + + {% endfor %} +
+ + Attribute + + + + Edit + + + + Strategy + +
+ + {{ attributeWrapper.name|t('site') }} + + + + {{ forms.lightswitchField({ + id: 'attributeEnabled-'~attributeWrapper.handle, + name: 'attributes['~attributeWrapper.handle~'][enabled]', + on: false, + value: true, + }) }} + + + +
+ {% endif %} +
+
+
Cancel
+
+
- {% else %} -

These elements have no associated fields.

-
-
-
Cancel
-
-
- {% endif %}