diff --git a/resources/css/elements/dropdowns.css b/resources/css/elements/dropdowns.css index f7d51f7aef..5d9d4c8d5a 100644 --- a/resources/css/elements/dropdowns.css +++ b/resources/css/elements/dropdowns.css @@ -40,6 +40,12 @@ .divider { @apply h-px bg-gray-400 overflow-hidden; margin: 6px -8px; + /* Hide dividers that come first, last or immediately after another (due to v-if) */ + & + &, + &:first-child, + &:last-child { + display: none; + } } .align-left & { diff --git a/resources/js/components/assets/Editor/Editor.vue b/resources/js/components/assets/Editor/Editor.vue index bdc5bdc38b..1c5251bb17 100644 --- a/resources/js/components/assets/Editor/Editor.vue +++ b/resources/js/components/assets/Editor/Editor.vue @@ -64,6 +64,16 @@ + + + + @@ -271,7 +281,24 @@ export default { isToolbarVisible() { return ! this.readOnly && this.showToolbar; - } + }, + + actionsMenu() + { + // We filter out the actions that are already in the toolbar. + // We don't want them to appear in the dropdown as well. + // If we filtered them out in PHP they wouldn't appear as buttons. + return this.actions.filter(action => ![ + 'rename_asset', + 'move_asset', + 'replace_asset', + 'reupload_asset', + 'download_asset', + 'delete', + 'copy_asset_url', + ].includes(action.handle)); + }, + }, mounted() { diff --git a/resources/js/components/data-list/Action.vue b/resources/js/components/data-list/Action.vue index 28cf706d4c..af870a14d8 100644 --- a/resources/js/components/data-list/Action.vue +++ b/resources/js/components/data-list/Action.vue @@ -11,9 +11,11 @@ @confirm="confirm" @cancel="reset" > -
+
-
+
+ +
{ data = JSON.parse(data); - if (data.redirect) window.location = data.redirect; + if (data.redirect) { + if (data.bypassesDirtyWarning) this.$dirty.disableWarning(); + window.location = data.redirect; + } if (data.callback) Statamic.$callbacks.call(data.callback[0], ...data.callback.slice(1)); this.$emit('completed', true, data); }); diff --git a/resources/js/components/data-list/InlineActions.vue b/resources/js/components/data-list/InlineActions.vue index f6f208e178..17bce57d57 100644 --- a/resources/js/components/data-list/InlineActions.vue +++ b/resources/js/components/data-list/InlineActions.vue @@ -7,6 +7,7 @@ :action="action" :selections="1" :errors="errors" + :is-dirty="isDirty" @selected="run" >
@@ -30,7 +31,8 @@ export default { props: { actions: Array, - item: { required: true } + item: { required: true }, + isDirty: { type: Boolean, default: false }, }, computed: { diff --git a/resources/js/components/entries/PublishForm.vue b/resources/js/components/entries/PublishForm.vue index 5abcf95a01..b090ccc7cb 100644 --- a/resources/js/components/entries/PublishForm.vue +++ b/resources/js/components/entries/PublishForm.vue @@ -13,6 +13,16 @@ +
  • +
    @@ -297,12 +307,14 @@ import SaveButtonOptions from '../publish/SaveButtonOptions.vue'; import RevisionHistory from '../revision-history/History.vue'; import HasPreferences from '../data-list/HasPreferences'; import HasHiddenFields from '../publish/HasHiddenFields'; +import HasActions from '../publish/HasActions'; export default { mixins: [ HasPreferences, HasHiddenFields, + HasActions, ], components: { @@ -496,7 +508,14 @@ export default { saving(saving) { this.$progress.loading(`${this.publishContainer}-entry-publish-form`, saving); - } + }, + + title(title) { + if (this.isBase) { + const arrow = this.direction === 'ltr' ? '‹' : '›'; + document.title = `${title} ${arrow} ${this.breadcrumbs[1].text} ${arrow} ${this.breadcrumbs[0].text} ${arrow} ${__('Statamic')}`; + } + }, }, @@ -550,10 +569,6 @@ export default { } this.title = response.data.data.title; this.isWorkingCopy = true; - if (this.isBase) { - const arrow = this.direction === 'ltr' ? '‹' : '›'; - document.title = `${this.title} ${arrow} ${this.breadcrumbs[1].text} ${arrow} ${this.breadcrumbs[0].text} ${arrow} ${__('Statamic')}`; - } if (!this.revisionsEnabled) this.permalink = response.data.data.permalink; if (!this.isCreating && !this.isAutosave) this.$toast.success(__('Saved')); this.$refs.container.saved(); @@ -794,7 +809,20 @@ export default { }, this.autosaveInterval); this.$store.commit(`publish/${this.publishContainer}/setAutosaveInterval`, interval); - } + }, + + afterActionSuccessfullyCompleted(response) { + if (response.data) { + this.title = response.data.title; + if (!this.revisionsEnabled) this.permalink = response.data.permalink; + this.values = this.resetValuesFromResponse(response.data.values); + this.initialPublished = response.data.published; + this.activeLocalization.published = response.data.published; + this.activeLocalization.status = response.data.status; + this.itemActions = response.data.itemActions; + } + }, + }, mounted() { diff --git a/resources/js/components/publish/HasActions.js b/resources/js/components/publish/HasActions.js new file mode 100644 index 0000000000..329eb28dff --- /dev/null +++ b/resources/js/components/publish/HasActions.js @@ -0,0 +1,44 @@ +export default { + + props: { + initialItemActions: Array, + itemActionUrl: String, + }, + + data() { + return { + itemActions: this.initialItemActions, + } + }, + + methods: { + + actionStarted() { + this.saving = true; + }, + + actionCompleted(successful=null, response) { + this.saving = false; + + if (successful === false) return; + + this.$events.$emit('reset-action-modals'); + + if (response.message !== false) { + this.$toast.success(response.message || __("Action completed")); + } + + if (response.data) { + this.itemActions = response.data.itemActions; + } + + this.afterActionSuccessfullyCompleted(response); + }, + + afterActionSuccessfullyCompleted(response) { + // + } + + } + +} diff --git a/resources/js/components/terms/PublishForm.vue b/resources/js/components/terms/PublishForm.vue index 02e42f05c3..f44e830286 100644 --- a/resources/js/components/terms/PublishForm.vue +++ b/resources/js/components/terms/PublishForm.vue @@ -15,6 +15,16 @@ +
  • +
    @@ -243,12 +253,14 @@ import SaveButtonOptions from '../publish/SaveButtonOptions.vue'; import RevisionHistory from '../revision-history/History.vue'; import HasPreferences from '../data-list/HasPreferences'; import HasHiddenFields from '../publish/HasHiddenFields'; +import HasActions from '../publish/HasActions'; export default { mixins: [ HasPreferences, HasHiddenFields, + HasActions, ], components: { @@ -473,6 +485,7 @@ export default { .then(() => { // If revisions are enabled, just emit event. if (this.revisionsEnabled) { + this.values = this.resetValuesFromResponse(response.data.data.values); this.$nextTick(() => this.$emit('saved', response)); return; } @@ -493,6 +506,7 @@ export default { // the hooks are resolved because if this form is being shown in a stack, we only // want to close it once everything's done. else { + this.values = this.resetValuesFromResponse(response.data.data.values); this.$nextTick(() => this.$emit('saved', response)); } @@ -620,7 +634,16 @@ export default { this.localizedFields.push(handle); this.$refs.container.dirty(); - } + }, + + afterActionSuccessfullyCompleted(response) { + if (response.data) { + this.title = response.data.title; + this.permalink = response.data.permalink; + this.values = this.resetValuesFromResponse(response.data.values); + } + }, + }, mounted() { diff --git a/resources/js/components/users/PublishForm.vue b/resources/js/components/users/PublishForm.vue index 31c941a775..38e65e851e 100644 --- a/resources/js/components/users/PublishForm.vue +++ b/resources/js/components/users/PublishForm.vue @@ -8,6 +8,15 @@

    +
  • + import ChangePassword from './ChangePassword.vue'; import HasHiddenFields from '../publish/HasHiddenFields'; +import HasActions from '../publish/HasActions'; export default { mixins: [ HasHiddenFields, + HasActions, ], components: { @@ -95,7 +106,11 @@ export default { hasErrors() { return this.error || Object.keys(this.errors).length; - } + }, + + isDirty() { + return this.$dirty.has(this.publishContainer); + }, }, @@ -111,6 +126,7 @@ export default { this.$axios[this.method](this.actions.save, this.visibleValues).then(response => { this.title = response.data.title; + this.values = this.resetValuesFromResponse(response.data.data.values); if (!response.data.saved) { return this.$toast.error(`Couldn't save user`) } @@ -128,7 +144,14 @@ export default { this.$toast.error(__('Something went wrong')); } }); - } + }, + + afterActionSuccessfullyCompleted(response) { + if (response.data) { + this.title = response.data.title; + this.values = this.resetValuesFromResponse(response.data.values); + } + }, }, diff --git a/resources/lang/en/messages.php b/resources/lang/en/messages.php index 5c2abdf154..abcc65e45d 100644 --- a/resources/lang/en/messages.php +++ b/resources/lang/en/messages.php @@ -139,6 +139,7 @@ 'globals_configure_handle_instructions' => 'Used to reference this global set on the frontend. It\'s non-trivial to change later.', 'globals_configure_intro' => 'A global set is a group of variables available across all front-end pages.', 'globals_configure_title_instructions' => 'We recommend a noun representing the set\'s contents. eg. "Brand" or "Company"', + 'impersonate_action_confirmation' => 'You will be logged in as this user. You can return to your account using the avatar menu.', 'licensing_config_cached_warning' => 'Any changes you make to your .env or config files will not be detected until you clear the cache. If you are seeing unexpected licensing results here, it may be because of this. You can use the php artisan config:cache command to regenerate the cache.', 'licensing_error_invalid_domain' => 'Invalid domain', 'licensing_error_invalid_edition' => 'License is for :edition edition', diff --git a/resources/views/entries/edit.blade.php b/resources/views/entries/edit.blade.php index 96adf175cc..ee8cae9e06 100644 --- a/resources/views/entries/edit.blade.php +++ b/resources/views/entries/edit.blade.php @@ -36,6 +36,8 @@ initial-listing-url="{{ cp_route('collections.show', $collection) }}" :preview-targets="{{ json_encode($previewTargets) }}" :autosave-interval="{{ json_encode($autosaveInterval) }}" + :initial-item-actions="{{ json_encode($itemActions) }}" + item-action-url="{{ cp_route('collections.entries.actions.run', $collection) }}" > @endsection diff --git a/resources/views/terms/edit.blade.php b/resources/views/terms/edit.blade.php index bcf3e9e6a6..6be8d5041d 100644 --- a/resources/views/terms/edit.blade.php +++ b/resources/views/terms/edit.blade.php @@ -36,6 +36,8 @@ create-another-url="{{ cp_route('taxonomies.terms.create', [$taxonomy, $locale]) }}" listing-url="{{ cp_route('taxonomies.show', $taxonomy) }}" :preview-targets="{{ json_encode($previewTargets) }}" + :initial-item-actions="{{ json_encode($itemActions) }}" + item-action-url="{{ cp_route('taxonomies.terms.actions.run', $taxonomy) }}" > @endsection diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index 6138df9584..64f4943299 100644 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -17,6 +17,8 @@ :can-edit-password="{{ Statamic\Support\Str::bool($canEditPassword) }}" :can-edit-blueprint="{{ Statamic\Support\Str::bool($user->can('configure fields')) }}" :requires-current-password="{{ Statamic\Support\Str::bool($requiresCurrentPassword) }}" + :initial-item-actions="{{ json_encode($itemActions) }}" + item-action-url="{{ cp_route('users.actions.run') }}" > @endsection diff --git a/src/Actions/Action.php b/src/Actions/Action.php index 226d11ef06..7a91d390c9 100644 --- a/src/Actions/Action.php +++ b/src/Actions/Action.php @@ -62,6 +62,10 @@ public function authorizeBulk($user, $items) public function context($context) { + if (! isset($context['view'])) { + $context['view'] = 'list'; + } + $this->context = $context; return $this; @@ -104,6 +108,17 @@ public function warningText() return null; } + public function dirtyWarningText() + { + /** @translation */ + return "Any unsaved changes will not be reflected in this action's behavior."; + } + + public function bypassesDirtyWarning(): bool + { + return false; + } + public function toArray() { return [ @@ -113,6 +128,8 @@ public function toArray() 'buttonText' => $this->buttonText(), 'confirmationText' => $this->confirmationText(), 'warningText' => $this->warningText(), + 'dirtyWarningText' => $this->dirtyWarningText(), + 'bypassesDirtyWarning' => $this->bypassesDirtyWarning(), 'dangerous' => $this->dangerous, 'fields' => $this->fields()->toPublishArray(), 'values' => $this->fields()->preProcess()->values(), diff --git a/src/Actions/AssignGroups.php b/src/Actions/AssignGroups.php index db4fee7dd9..3cfb9116b3 100644 --- a/src/Actions/AssignGroups.php +++ b/src/Actions/AssignGroups.php @@ -14,7 +14,7 @@ public static function title() public function visibleTo($item) { - return $item instanceof UserContract && UserGroup::all()->isNotEmpty(); + return $this->context['view'] === 'list' && $item instanceof UserContract && UserGroup::all()->isNotEmpty(); } public function authorize($authed, $user) diff --git a/src/Actions/AssignRoles.php b/src/Actions/AssignRoles.php index 1c97ccb5ab..b4a4fdc7fc 100644 --- a/src/Actions/AssignRoles.php +++ b/src/Actions/AssignRoles.php @@ -14,7 +14,7 @@ public static function title() public function visibleTo($item) { - return $item instanceof UserContract && Role::all()->isNotEmpty(); + return $this->context['view'] === 'list' && $item instanceof UserContract && Role::all()->isNotEmpty(); } public function authorize($authed, $user) diff --git a/src/Actions/Delete.php b/src/Actions/Delete.php index 6270c3f504..30dbd99877 100644 --- a/src/Actions/Delete.php +++ b/src/Actions/Delete.php @@ -52,8 +52,31 @@ public function confirmationText() return 'Are you sure you want to delete this?|Are you sure you want to delete these :count items?'; } + public function bypassesDirtyWarning(): bool + { + return true; + } + public function run($items, $values) { $items->each->delete(); } + + public function redirect($items, $values) + { + if ($this->context['view'] !== 'form') { + return; + } + + $item = $items->first(); + + switch (true) { + case $item instanceof Contracts\Entries\Entry: + return cp_route('collections.show', $item->collection()->handle()); + case $item instanceof Contracts\Taxonomies\Term: + return cp_route('taxonomies.show', $item->taxonomy()->handle()); + case $item instanceof Contracts\Auth\User: + return cp_route('users.index'); + } + } } diff --git a/src/Actions/DuplicateEntry.php b/src/Actions/DuplicateEntry.php index 9f0bfe808b..96e90e2426 100644 --- a/src/Actions/DuplicateEntry.php +++ b/src/Actions/DuplicateEntry.php @@ -10,6 +10,8 @@ class DuplicateEntry extends Action { + private $newItems; + public static function title() { return __('Duplicate'); @@ -48,12 +50,18 @@ public function warningText() } } + public function dirtyWarningText() + { + /** @translation */ + return 'Any unsaved changes will not be duplicated into the new entry.'; + } + public function run($items, $values) { - $items + $this->newItems = $items ->map(fn ($entry) => $entry->hasOrigin() ? $entry->root() : $entry) ->unique() - ->each(fn ($original) => $this->duplicateEntry($original)); + ->map(fn ($original) => $this->duplicateEntry($original)); } private function duplicateEntry(Entry $original, ?string $origin = null) @@ -99,6 +107,8 @@ private function duplicateEntry(Entry $original, ?string $origin = null) ->appendTo($originalParent->id(), $entry) ->save(); } + + return $entry; } protected function getEntryParentFromStructure(Entry $entry) @@ -156,4 +166,13 @@ public function authorize($user, $item) { return $user->can('create', [Entry::class, $item->collection(), $item->site()]); } + + public function redirect($items, $values) + { + if ($this->context['view'] !== 'form') { + return; + } + + return $this->newItems->first()->editUrl(); + } } diff --git a/src/Actions/DuplicateTerm.php b/src/Actions/DuplicateTerm.php index 899f683cb6..ef02aa5ebe 100644 --- a/src/Actions/DuplicateTerm.php +++ b/src/Actions/DuplicateTerm.php @@ -8,6 +8,8 @@ class DuplicateTerm extends Action { + private $newItems; + public static function title() { return __('Duplicate'); @@ -20,7 +22,7 @@ public function visibleTo($item) public function run($items, $values) { - $items->each(function (Term $original) { + $this->newItems = $items->map(function (Term $original) { [$title, $slug] = $this->generateTitleAndSlug($original); $data = $original->data() @@ -37,6 +39,8 @@ public function run($items, $values) ->data($data); $term->save(); + + return $term; }); } @@ -72,4 +76,13 @@ public function authorize($user, $item) { return $user->can('create', [Term::class, $item->taxonomy()]); } + + public function redirect($items, $values) + { + if ($this->context['view'] !== 'form') { + return; + } + + return $this->newItems->first()->editUrl(); + } } diff --git a/src/Actions/Impersonate.php b/src/Actions/Impersonate.php index ce3db84560..6136f1e695 100644 --- a/src/Actions/Impersonate.php +++ b/src/Actions/Impersonate.php @@ -10,8 +10,6 @@ class Impersonate extends Action { - protected $confirm = false; - public static function title() { return __('Start Impersonating'); @@ -67,4 +65,21 @@ public function redirect($users, $values) return $users->first()->can('access cp') ? cp_route('index') : '/'; } + + public function confirmationText() + { + /** @translation */ + return 'statamic::messages.impersonate_action_confirmation'; + } + + public function buttonText() + { + /** @translation */ + return 'Confirm'; + } + + public function bypassesDirtyWarning(): bool + { + return true; + } } diff --git a/src/Actions/Publish.php b/src/Actions/Publish.php index b0bb9c0709..d573174cbc 100644 --- a/src/Actions/Publish.php +++ b/src/Actions/Publish.php @@ -14,7 +14,7 @@ public static function title() public function visibleTo($item) { - return $item instanceof Entry && ! $item->published(); + return $this->context['view'] === 'list' && $item instanceof Entry && ! $item->published(); } public function visibleToBulk($items) diff --git a/src/Actions/SendPasswordReset.php b/src/Actions/SendPasswordReset.php index 8a5a1c3700..22e8e6a516 100644 --- a/src/Actions/SendPasswordReset.php +++ b/src/Actions/SendPasswordReset.php @@ -27,6 +27,11 @@ public function confirmationText() return 'Send password reset email to this user?|Send password reset email to these :count users?'; } + public function dirtyWarningText() + { + return null; + } + public function buttonText() { /** @translation */ diff --git a/src/Actions/Unpublish.php b/src/Actions/Unpublish.php index f2d6ab367d..2d27e0f8dc 100644 --- a/src/Actions/Unpublish.php +++ b/src/Actions/Unpublish.php @@ -9,7 +9,7 @@ class Unpublish extends Action { public function visibleTo($item) { - return $item instanceof Entry && $item->published(); + return $this->context['view'] === 'list' && $item instanceof Entry && $item->published(); } public function visibleToBulk($items) diff --git a/src/Http/Controllers/CP/ActionController.php b/src/Http/Controllers/CP/ActionController.php index 9247cb17d1..59c4388e9b 100644 --- a/src/Http/Controllers/CP/ActionController.php +++ b/src/Http/Controllers/CP/ActionController.php @@ -5,6 +5,7 @@ use Illuminate\Http\Request; use Statamic\Facades\Action; use Statamic\Facades\User; +use Statamic\Support\Arr; use Symfony\Component\HttpFoundation\Response; abstract class ActionController extends CpController @@ -38,16 +39,25 @@ public function run(Request $request) $response = $action->run($items, $values); if ($redirect = $action->redirect($items, $values)) { - return ['redirect' => $redirect]; + return [ + 'redirect' => $redirect, + 'bypassesDirtyWarning' => $action->bypassesDirtyWarning(), + ]; } elseif ($download = $action->download($items, $values)) { return $download instanceof Response ? $download : response()->download($download); } if (is_string($response)) { - return ['message' => $response]; + $response = ['message' => $response]; } - return $response ?: []; + $response = $response ?: []; + + if (Arr::get($context, 'view') === 'form') { + $response['data'] = $this->getItemData($items->first(), $context); + } + + return $response; } public function bulkActions(Request $request) @@ -65,4 +75,6 @@ public function bulkActions(Request $request) } abstract protected function getSelectedItems($items, $context); + + abstract protected function getItemData($item, $context): array; } diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php index 9fb5891bd4..b0d41f87c9 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -7,6 +7,7 @@ use Statamic\Contracts\Entries\Entry as EntryContract; use Statamic\CP\Breadcrumbs; use Statamic\Exceptions\BlueprintNotFoundException; +use Statamic\Facades\Action; use Statamic\Facades\Asset; use Statamic\Facades\Entry; use Statamic\Facades\Site; @@ -156,6 +157,7 @@ public function edit(Request $request, $collection, $entry) 'canManagePublishState' => User::current()->can('publish', $entry), 'previewTargets' => $collection->previewTargets()->all(), 'autosaveInterval' => $collection->autosaveInterval(), + 'itemActions' => Action::for($entry, ['collection' => $collection->handle(), 'view' => 'form']), ]; if ($request->wantsJson()) { diff --git a/src/Http/Controllers/CP/Collections/EntryActionController.php b/src/Http/Controllers/CP/Collections/EntryActionController.php index 888687ee6e..f6a7f909f8 100644 --- a/src/Http/Controllers/CP/Collections/EntryActionController.php +++ b/src/Http/Controllers/CP/Collections/EntryActionController.php @@ -2,15 +2,33 @@ namespace Statamic\Http\Controllers\CP\Collections; +use Statamic\Facades\Action; use Statamic\Facades\Entry; use Statamic\Http\Controllers\CP\ActionController; +use Statamic\Http\Resources\CP\Entries\Entry as EntryResource; class EntryActionController extends ActionController { + use ExtractsFromEntryFields; + protected function getSelectedItems($items, $context) { return $items->map(function ($item) { return Entry::find($item); }); } + + protected function getItemData($entry, $context): array + { + $entry = $entry->fresh(); + + $blueprint = $entry->blueprint(); + + [$values] = $this->extractFromFields($entry, $blueprint); + + return array_merge((new EntryResource($entry))->resolve()['data'], [ + 'values' => $values, + 'itemActions' => Action::for($entry, $context), + ]); + } } diff --git a/src/Http/Controllers/CP/Taxonomies/ExtractsFromTermFields.php b/src/Http/Controllers/CP/Taxonomies/ExtractsFromTermFields.php new file mode 100644 index 0000000000..71c84d63e9 --- /dev/null +++ b/src/Http/Controllers/CP/Taxonomies/ExtractsFromTermFields.php @@ -0,0 +1,27 @@ +values() would have given us. + $values = $term->inDefaultLocale()->data()->merge( + $term->data() + ); + + $fields = $blueprint + ->fields() + ->addValues($values->all()) + ->preProcess(); + + $values = $fields->values()->merge([ + 'title' => $term->value('title'), + 'slug' => $term->slug(), + ]); + + return [$values->all(), $fields->meta()]; + } +} diff --git a/src/Http/Controllers/CP/Taxonomies/TermActionController.php b/src/Http/Controllers/CP/Taxonomies/TermActionController.php index 91d2304ebd..3b04b2eb80 100644 --- a/src/Http/Controllers/CP/Taxonomies/TermActionController.php +++ b/src/Http/Controllers/CP/Taxonomies/TermActionController.php @@ -2,15 +2,33 @@ namespace Statamic\Http\Controllers\CP\Taxonomies; +use Statamic\Facades\Action; use Statamic\Facades\Term; use Statamic\Http\Controllers\CP\ActionController; +use Statamic\Http\Resources\CP\Taxonomies\Term as TermResource; class TermActionController extends ActionController { + use ExtractsFromTermFields; + protected function getSelectedItems($items, $context) { return $items->map(function ($item) { return Term::find($item); })->filter(); } + + protected function getItemData($term, $context): array + { + $term = $term->fresh(); + + $blueprint = $term->blueprint(); + + [$values] = $this->extractFromFields($term, $blueprint); + + return array_merge((new TermResource($term))->resolve()['data'], [ + 'values' => $values, + 'itemActions' => Action::for($term, $context), + ]); + } } diff --git a/src/Http/Controllers/CP/Taxonomies/TermsController.php b/src/Http/Controllers/CP/Taxonomies/TermsController.php index f1a8516db2..77446b793d 100644 --- a/src/Http/Controllers/CP/Taxonomies/TermsController.php +++ b/src/Http/Controllers/CP/Taxonomies/TermsController.php @@ -5,6 +5,7 @@ use Illuminate\Http\Request; use Statamic\Contracts\Taxonomies\Term as TermContract; use Statamic\CP\Breadcrumbs; +use Statamic\Facades\Action; use Statamic\Facades\Asset; use Statamic\Facades\Site; use Statamic\Facades\Term; @@ -19,7 +20,8 @@ class TermsController extends CpController { - use QueriesFilters; + use ExtractsFromTermFields, + QueriesFilters; public function index(FilteredRequest $request, $taxonomy) { @@ -141,6 +143,7 @@ public function edit(Request $request, $taxonomy, $term) 'revisionsEnabled' => $term->revisionsEnabled(), 'breadcrumbs' => $this->breadcrumbs($taxonomy), 'previewTargets' => $taxonomy->previewTargets()->all(), + 'itemActions' => Action::for($term, ['taxonomy' => $taxonomy->handle(), 'view' => 'form']), ]; if ($request->wantsJson()) { @@ -204,8 +207,15 @@ public function update(Request $request, $taxonomy, $term, $site) $saved = $term->updateLastModified(User::current())->save(); } + [$values] = $this->extractFromFields($term, $term->blueprint()); + return (new TermResource($term)) - ->additional(['saved' => $saved]); + ->additional([ + 'saved' => $saved, + 'data' => [ + 'values' => $values, + ], + ]); } public function create(Request $request, $taxonomy, $site) @@ -313,27 +323,6 @@ public function store(Request $request, $taxonomy, $site) ->additional(['saved' => $saved]); } - protected function extractFromFields($term, $blueprint) - { - // The values should only be data merged with the origin data. - // We don't want injected taxonomy values, which $term->values() would have given us. - $values = $term->inDefaultLocale()->data()->merge( - $term->data() - ); - - $fields = $blueprint - ->fields() - ->addValues($values->all()) - ->preProcess(); - - $values = $fields->values()->merge([ - 'title' => $term->value('title'), - 'slug' => $term->slug(), - ]); - - return [$values->all(), $fields->meta()]; - } - protected function extractAssetsFromValues($values) { return collect($values) diff --git a/src/Http/Controllers/CP/Users/ExtractsFromUserFields.php b/src/Http/Controllers/CP/Users/ExtractsFromUserFields.php new file mode 100644 index 0000000000..31ff82e054 --- /dev/null +++ b/src/Http/Controllers/CP/Users/ExtractsFromUserFields.php @@ -0,0 +1,22 @@ +data() + ->merge($user->computedData()) + ->merge(['email' => $user->email()]); + + $fields = $blueprint + ->removeField('password') + ->removeField('password_confirmation') + ->fields() + ->addValues($values->all()) + ->preProcess(); + + return [$fields->values()->all(), $fields->meta()->all()]; + } +} diff --git a/src/Http/Controllers/CP/Users/UserActionController.php b/src/Http/Controllers/CP/Users/UserActionController.php index ca2b83c499..4e715d0e08 100644 --- a/src/Http/Controllers/CP/Users/UserActionController.php +++ b/src/Http/Controllers/CP/Users/UserActionController.php @@ -2,15 +2,31 @@ namespace Statamic\Http\Controllers\CP\Users; +use Statamic\Facades\Action; use Statamic\Facades\User; use Statamic\Http\Controllers\CP\ActionController; class UserActionController extends ActionController { + use ExtractsFromUserFields; + protected function getSelectedItems($items, $context) { return $items->map(function ($item) { return User::find($item); }); } + + protected function getItemData($user, $context): array + { + $blueprint = $user->blueprint(); + + [$values] = $this->extractFromFields($user, $blueprint); + + return [ + 'title' => $user->title(), + 'values' => array_merge($values, ['id' => $user->id()]), + 'itemActions' => Action::for($user, $context), + ]; + } } diff --git a/src/Http/Controllers/CP/Users/UsersController.php b/src/Http/Controllers/CP/Users/UsersController.php index 792fd05f68..61e2424417 100644 --- a/src/Http/Controllers/CP/Users/UsersController.php +++ b/src/Http/Controllers/CP/Users/UsersController.php @@ -6,6 +6,7 @@ use Statamic\Auth\Passwords\PasswordReset; use Statamic\Contracts\Auth\User as UserContract; use Statamic\Exceptions\NotFoundHttpException; +use Statamic\Facades\Action; use Statamic\Facades\CP\Toast; use Statamic\Facades\Scope; use Statamic\Facades\Search; @@ -22,7 +23,8 @@ class UsersController extends CpController { - use QueriesFilters; + use ExtractsFromUserFields, + QueriesFilters; /** * @var UserContract @@ -235,21 +237,12 @@ public function edit(Request $request, $user) $blueprint->ensureField('super', ['type' => 'toggle', 'display' => __('permissions.super')]); } - $values = $user->data() - ->merge($user->computedData()) - ->merge(['email' => $user->email()]); - - $fields = $blueprint - ->removeField('password') - ->removeField('password_confirmation') - ->fields() - ->addValues($values->all()) - ->preProcess(); + [$values, $meta] = $this->extractFromFields($user, $blueprint); $viewData = [ 'title' => $user->email(), - 'values' => $fields->values()->all(), - 'meta' => $fields->meta(), + 'values' => array_merge($values, ['id' => $user->id()]), + 'meta' => $meta, 'blueprint' => $user->blueprint()->toPublishArray(), 'reference' => $user->reference(), 'actions' => [ @@ -259,6 +252,7 @@ public function edit(Request $request, $user) ], 'canEditPassword' => User::fromUser($request->user())->can('editPassword', $user), 'requiresCurrentPassword' => $request->user()->id === $user->id(), + 'itemActions' => Action::for($user, ['view' => 'form']), ]; if ($request->wantsJson()) { @@ -274,7 +268,7 @@ public function update(Request $request, $user) $this->authorize('edit', $user); - $fields = $user->blueprint()->fields()->except(['password'])->addValues($request->all()); + $fields = $user->blueprint()->fields()->except(['password'])->addValues($request->except('id')); $fields ->validator() @@ -304,9 +298,14 @@ public function update(Request $request, $user) $save = $user->save(); + [$values] = $this->extractFromFields($user, $user->blueprint()); + return [ 'title' => $user->title(), 'saved' => is_bool($save) ? $save : true, + 'data' => [ + 'values' => $values, + ], ]; } } diff --git a/src/Http/Resources/CP/Assets/Asset.php b/src/Http/Resources/CP/Assets/Asset.php index 49c56cf8fa..4e0ff2c437 100644 --- a/src/Http/Resources/CP/Assets/Asset.php +++ b/src/Http/Resources/CP/Assets/Asset.php @@ -53,6 +53,7 @@ public function toArray($request) 'actions' => Action::for($this->resource, [ 'container' => $this->container()->handle(), 'folder' => $this->folder(), + 'view' => 'form', ]), 'blueprint' => $this->blueprint()->toPublishArray(),