diff --git a/resources/js/bootstrap/components.js b/resources/js/bootstrap/components.js index 6d123d1157..b909ca0ff2 100644 --- a/resources/js/bootstrap/components.js +++ b/resources/js/bootstrap/components.js @@ -58,6 +58,7 @@ import Modal from '../components/Modal.vue'; import ConfirmationModal from '../components/modals/ConfirmationModal.vue'; import FavoriteCreator from '../components/FavoriteCreator.vue'; import KeyboardShortcutsModal from '../components/modals/KeyboardShortcutsModal.vue'; +import FieldActionModal from '../components/field-actions/FieldActionModal.vue'; import ResourceDeleter from '../components/ResourceDeleter.vue'; import Stack from '../components/stacks/Stack.vue'; import StackTest from '../components/stacks/StackTest.vue'; @@ -144,6 +145,7 @@ Vue.component('confirmation-modal', ConfirmationModal); Vue.component('favorite-creator', FavoriteCreator); Vue.component('keyboard-shortcuts-modal', KeyboardShortcutsModal); Vue.component('resource-deleter', ResourceDeleter); +Vue.component('field-action-modal', FieldActionModal); Vue.component('stack', Stack); Vue.component('stack-test', StackTest); diff --git a/resources/js/components/field-actions/FieldAction.js b/resources/js/components/field-actions/FieldAction.js index 8a19f3b666..7e9d41be55 100644 --- a/resources/js/components/field-actions/FieldAction.js +++ b/resources/js/components/field-actions/FieldAction.js @@ -1,3 +1,5 @@ +import modal from './modal'; + export default class FieldAction { #payload; #run; @@ -5,10 +7,12 @@ export default class FieldAction { #visibleWhenReadOnly; #icon; #quick; + #confirm; constructor(action, payload) { this.#payload = payload; this.#run = action.run; + this.#confirm = action.confirm; this.#visible = action.visible ?? true; this.#visibleWhenReadOnly = action.visibleWhenReadOnly ?? false; this.#icon = action.icon ?? 'image'; @@ -32,7 +36,32 @@ export default class FieldAction { return typeof this.#icon === 'function' ? this.#icon(this.#payload) : this.#icon; } - run() { - this.#run(this.#payload); + async run() { + let payload = {...this.#payload}; + + if (this.#confirm) { + const confirmation = await modal(this.#modalProps()); + if (!confirmation.confirmed) return; + payload = {...payload, confirmation}; + } + + const response = this.#run(payload); + + if (response instanceof Promise) { + const progress = this.#payload.vm.$progress; + const name = this.#payload.fieldPathPrefix ?? this.#payload.handle; + progress.loading(name, true); + response.finally(() => progress.loading(name, false)); + } + } + + #modalProps() { + let props = this.#confirm === true ? {} : {...this.#confirm}; + + if (! props.title) { + props.title = this.title; + } + + return props; } } diff --git a/resources/js/components/field-actions/FieldActionModal.vue b/resources/js/components/field-actions/FieldActionModal.vue new file mode 100644 index 0000000000..11d29de49a --- /dev/null +++ b/resources/js/components/field-actions/FieldActionModal.vue @@ -0,0 +1,152 @@ + + + diff --git a/resources/js/components/field-actions/modal.js b/resources/js/components/field-actions/modal.js new file mode 100644 index 0000000000..b8fa6f9488 --- /dev/null +++ b/resources/js/components/field-actions/modal.js @@ -0,0 +1,16 @@ +export default function(props) { + return new Promise((resolve) => { + const component = Statamic.$components.append('field-action-modal', { props }); + const close = () => Statamic.$components.destroy(component.id); + + component.on('confirm', (data = {}) => { + resolve({ ...data, confirmed: true }); + close(); + }); + + component.on('cancel', () => { + resolve({ confirmed: false }); + close(); + }); + }); +} diff --git a/routes/cp.php b/routes/cp.php index 19f8743826..34dac73184 100644 --- a/routes/cp.php +++ b/routes/cp.php @@ -42,6 +42,7 @@ use Statamic\Http\Controllers\CP\CpController; use Statamic\Http\Controllers\CP\DashboardController; use Statamic\Http\Controllers\CP\DuplicatesController; +use Statamic\Http\Controllers\CP\FieldActionModalController; use Statamic\Http\Controllers\CP\Fields\BlueprintController; use Statamic\Http\Controllers\CP\Fields\FieldsController; use Statamic\Http\Controllers\CP\Fields\FieldsetController; @@ -319,6 +320,11 @@ Route::get('dictionaries/{dictionary}', DictionaryFieldtypeController::class)->name('dictionary-fieldtype'); }); + Route::group(['prefix' => 'field-action-modal'], function () { + Route::post('resolve', [FieldActionModalController::class, 'resolve'])->name('resolve'); + Route::post('process', [FieldActionModalController::class, 'process'])->name('process'); + }); + Route::group(['prefix' => 'api', 'as' => 'api.'], function () { Route::resource('addons', AddonsApiController::class)->only('index'); Route::resource('templates', TemplatesController::class)->only('index'); diff --git a/src/Http/Controllers/CP/FieldActionModalController.php b/src/Http/Controllers/CP/FieldActionModalController.php new file mode 100644 index 0000000000..049a6287ee --- /dev/null +++ b/src/Http/Controllers/CP/FieldActionModalController.php @@ -0,0 +1,48 @@ +getFields($request->fields) + ->preProcess(); + + return [ + 'fields' => $fields->toPublishArray(), + 'values' => $fields->values(), + 'meta' => $fields->meta(), + ]; + } + + public function process(Request $request) + { + $fields = $this + ->getFields($request->fields) + ->addValues($request->values); + + $fields->validate(); + + $processed = $fields->process()->values(); + + $fields->preProcess(); + + return [ + 'values' => $fields->values(), + 'meta' => $fields->meta(), + 'processed' => $processed, + ]; + } + + private function getFields($fieldItems) + { + return new Fields( + collect($fieldItems)->map(fn ($field, $handle) => compact('handle', 'field')) + ); + } +}