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'))
+ );
+ }
+}