diff --git a/resources/js/components/form/ErrorSummary.vue b/resources/js/components/form/ErrorSummary.vue
new file mode 100644
index 00000000000..d317079431b
--- /dev/null
+++ b/resources/js/components/form/ErrorSummary.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+ {{ t('Could not save settings') }}
+
+
+
+
+
+
diff --git a/resources/js/components/form/Input.vue b/resources/js/components/form/Input.vue
deleted file mode 100644
index 8dd0b76d6d1..00000000000
--- a/resources/js/components/form/Input.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/resources/js/components/form/InputCombobox.vue b/resources/js/components/form/InputCombobox.vue
index f2035803298..15a4c3578ad 100644
--- a/resources/js/components/form/InputCombobox.vue
+++ b/resources/js/components/form/InputCombobox.vue
@@ -142,7 +142,7 @@
-
+
{{ item.label }}
-
+
+
+
-
+
+
+
diff --git a/resources/js/components/form/InputComboboxOption.vue b/resources/js/components/form/InputComboboxOption.vue
index 1518f08b1fd..50965173dd3 100644
--- a/resources/js/components/form/InputComboboxOption.vue
+++ b/resources/js/components/form/InputComboboxOption.vue
@@ -15,16 +15,21 @@
:checked="selected"
:hint="option.data?.hint"
>
-
-
+
+
+
+
+
+
+ {{ option.label }}
+
+
+
{{ option.label }}
-
-
-
- {{ option.label }}
-
+
+
diff --git a/resources/js/components/sites/SiteFields.vue b/resources/js/components/sites/SiteFields.vue
index 945caf67dfe..ef10f8c28b3 100644
--- a/resources/js/components/sites/SiteFields.vue
+++ b/resources/js/components/sites/SiteFields.vue
@@ -4,9 +4,10 @@
import {type InertiaForm, usePage} from '@inertiajs/vue3';
import {computed, useTemplateRef} from 'vue';
import type {SelectItem, SelectOption, Site} from '@/types';
- import InputCombobox from '@/components/form/InputCombobox.vue';
import {useInputGenerator} from '@/composables/useInputGenerator';
import {toHandle} from '@craftcms/cp/utilities/string.ts.mjs';
+ import {transformBooleanOptions} from '@/utils/transformBooleanOptions';
+ import CraftCombobox from '@/components/form/CraftCombobox.vue';
const props = withDefaults(
defineProps<{
@@ -26,21 +27,6 @@
groupOptions: Array;
}>();
- function addBooleanHints(option: SelectOption) {
- const isEnvVar =
- option.value.startsWith('$') || option.value.startsWith('@');
-
- return isEnvVar
- ? {
- ...option,
- data: {
- ...(option.data || {}),
- hint: option.data?.boolean === '1' ? t('Enabled') : t('Disabled'),
- },
- }
- : option;
- }
-
const form = computed(() => props.inertiaForm);
const isMultisite = computed(() => page.props.isMultisite);
const groupOptions = computed(() => page.props.groupOptions);
@@ -49,16 +35,7 @@
});
const languageOptions = computed(() => page.props.languageOptions);
const booleanEnvOptions = computed(() =>
- page.props.booleanEnvOptions.map((option) => {
- if (option.type === 'optgroup') {
- return {
- ...option,
- options: option.options.map(addBooleanHints),
- };
- }
-
- return addBooleanHints(option);
- })
+ transformBooleanOptions(page.props.booleanEnvOptions)
);
const baseUrlSuggestions = computed(() => page.props.baseUrlSuggestions);
const site = computed(() => page.props.site);
@@ -66,15 +43,6 @@
const handleRef = useTemplateRef('handle');
const baseUrlRef = useTemplateRef('baseUrl');
- const enabledValue = computed({
- get() {
- return form.value.enabled ? '1' : '0';
- },
- set(newValue) {
- form.value.enabled = newValue;
- },
- });
-
const handleGenerator = useInputGenerator(
() => form.value.name,
(v) => (form.value.handle = toHandle(v))
@@ -97,18 +65,6 @@
-
-
-
- {{ t('Could not save settings') }}
-
-
-
-
-
-
-
+
-
-
-
- - {{ form.errors.name }}
-
-
-
+
-
-
-
-
+
-
-
-
-
- - {{ form.errors.language }}
-
-
-
+
+
-
-
-
-
-
-
- {{ option.label }}
- {{ option.label }}
-
-
-
-
-
-
-
- - {{ form.errors.enabled }}
-
-
-
+
@@ -328,20 +240,16 @@
-
-
-
-
+
diff --git a/resources/js/components/utilities/FindReplace/FindReplace.vue b/resources/js/components/utilities/FindReplace/FindReplace.vue
index 9d07f46a705..97a074c5916 100644
--- a/resources/js/components/utilities/FindReplace/FindReplace.vue
+++ b/resources/js/components/utilities/FindReplace/FindReplace.vue
@@ -2,8 +2,8 @@
import {t} from '@craftcms/cp/utilities/translate.ts';
import findAndReplaceController from '@actions/Utilities/FindAndReplaceController.ts';
import {useForm} from '@inertiajs/vue3';
- import TransitionFade from '@/components/TransitionFade.vue';
- import Input from '@/components/form/Input.vue';
+ import CraftInput from '@craftcms/cp/vue/CraftInput.vue';
+ import InlineFlash from '@/components/InlineFlash.vue';
const form = useForm({
find: '',
@@ -24,13 +24,13 @@
diff --git a/resources/js/composables/useFlash.ts b/resources/js/composables/useFlash.ts
new file mode 100644
index 00000000000..a13bf82c9ee
--- /dev/null
+++ b/resources/js/composables/useFlash.ts
@@ -0,0 +1,17 @@
+import {usePage} from '@inertiajs/vue3';
+import {computed} from 'vue';
+
+export function useFlash() {
+ const page = usePage<{
+ flash: {
+ success: string | null;
+ error: string | null;
+ };
+ }>();
+
+ const flash = computed(() => page.props.flash);
+ const successFlash = computed(() => flash.value.success);
+ const errorFlash = computed(() => flash.value.error);
+
+ return {flash, successFlash, errorFlash};
+}
diff --git a/resources/js/composables/useSettingsSave.ts b/resources/js/composables/useSettingsSave.ts
new file mode 100644
index 00000000000..b4d86c83f73
--- /dev/null
+++ b/resources/js/composables/useSettingsSave.ts
@@ -0,0 +1,44 @@
+import {useEventListener} from '@vueuse/core';
+import {type InertiaForm, usePage} from '@inertiajs/vue3';
+import {computed} from 'vue';
+
+export function useSettingsSave>(
+ form: InertiaForm,
+ action: any
+) {
+ const page = usePage<{
+ redirectUrl?: string;
+ }>();
+ const redirectUrl = computed(() => page.props.redirectUrl);
+
+ // Handle cmd + s events
+ useEventListener('keydown', (event) => {
+ if ((event.metaKey || event.ctrlKey) && event.key === 's') {
+ event.preventDefault();
+ save({redirect: false});
+ }
+ });
+
+ function save({redirect = true} = {}) {
+ let options = {};
+ if (redirect) {
+ options = {
+ preserveScroll: true,
+ preserveState: true,
+ };
+ }
+
+ form
+ .clearErrors()
+ .transform((data: T) => {
+ return {
+ ...data,
+ redirect:
+ redirect && redirectUrl.value ? redirectUrl.value : undefined,
+ };
+ })
+ .submit(action(), options);
+ }
+
+ return {save};
+}
diff --git a/resources/js/layout/AppLayout.vue b/resources/js/layout/AppLayout.vue
index b452e9d0b70..f83ecb6c175 100644
--- a/resources/js/layout/AppLayout.vue
+++ b/resources/js/layout/AppLayout.vue
@@ -4,36 +4,49 @@
import {computed, reactive, ref, useTemplateRef, watch} from 'vue';
import CpSidebar from '@/components/CpSidebar.vue';
import {useMediaQuery} from '@vueuse/core';
- import {Head, usePage} from '@inertiajs/vue3';
+ import {Head, type InertiaForm, usePage} from '@inertiajs/vue3';
import VarDump from '@/components/VarDump.vue';
import Breadcrumbs from '@/components/Breadcrumbs.vue';
import {useAnnouncer} from '@/composables/useAnnouncer';
import LiveRegion from '@/components/LiveRegion.vue';
import {useAppendHtml} from '@/composables/useAppendHtml';
+ import ActionMenu from '@/components/ActionMenu.vue';
+ import type {ActionItem} from '@/types';
+ import {useFlash} from '@/composables/useFlash';
+ import InlineFlash from '@/components/InlineFlash.vue';
+ import ErrorSummary from '@/components/form/ErrorSummary.vue';
+ import CalloutReadOnly from '@/components/CalloutReadOnly.vue';
+ interface SaveOptions {
+ redirect?: boolean;
+ }
+
+ const emit = defineEmits<{
+ (e: 'save', options?: Partial): void;
+ }>();
const props = withDefaults(
defineProps<{
title?: string;
debug?: any;
fullWidth?: boolean;
+ form?: InertiaForm | null;
+ formActions?: Array;
}>(),
- {fullWidth: false, crumbs: () => []}
+ {fullWidth: false, crumbs: () => [], form: null}
);
const page = usePage<{
title: string;
- flash: {
- success: string | null;
- error: string | null;
- };
+ readOnly: boolean;
crumbs?: Array<{
url?: string;
label: string;
}> | null;
}>();
- const errorFlash = computed(() => page.props.flash?.error);
- const successFlash = computed(() => page.props.flash?.success);
+
+ const {errorFlash, successFlash} = useFlash();
const crumbs = computed(() => page.props.crumbs ?? null);
+ const readOnly = computed(() => page.props.readOnly);
const sidebarToggle = useTemplateRef('sidebarToggle');
const {announcement, announce} = useAnnouncer();
@@ -155,25 +168,79 @@
-
-
-