diff --git a/packages/craftcms-cp/scripts/generate-vue-wrappers.js b/packages/craftcms-cp/scripts/generate-vue-wrappers.js index f494c43792e..7e190849fc7 100644 --- a/packages/craftcms-cp/scripts/generate-vue-wrappers.js +++ b/packages/craftcms-cp/scripts/generate-vue-wrappers.js @@ -230,6 +230,10 @@ function generateValueWrapper(component) { }); const model = defineModel<${component.modelType}>(); + + defineProps<{ + error?: null | string + }>() `; diff --git a/packages/craftcms-cp/src/components/checkbox/checkbox.ts b/packages/craftcms-cp/src/components/checkbox/checkbox.ts index 191568c8b4c..7f3ebca79b5 100644 --- a/packages/craftcms-cp/src/components/checkbox/checkbox.ts +++ b/packages/craftcms-cp/src/components/checkbox/checkbox.ts @@ -8,9 +8,10 @@ export default class CraftCheckbox extends LionCheckbox { css` /* same as radio, potentially consolidate */ :host { + --_gap-x: var(--gap-x, --c-spacing-md); display: grid; align-items: center; - gap: 0 var(--c-spacing-md); + gap: 0 var(--_gap-x); grid-template-areas: 'input label' '. help-text'; grid-template-columns: auto 1fr; grid-template-rows: repeat(2, auto); @@ -36,6 +37,8 @@ export default class CraftCheckbox extends LionCheckbox { var(--c-form-control-border-color) ); border-radius: var(--c-input-radius, var(--c-radius-sm)); + width: var(--c-size-control-2xs); + height: var(--c-size-control-2xs); } .choice-field__help-text { diff --git a/packages/craftcms-cp/src/styles/shared/tokens.css b/packages/craftcms-cp/src/styles/shared/tokens.css index 39c24cb3697..409c8a93059 100644 --- a/packages/craftcms-cp/src/styles/shared/tokens.css +++ b/packages/craftcms-cp/src/styles/shared/tokens.css @@ -137,6 +137,7 @@ --c-size-icon-lg: calc(22rem / 16); --c-size-icon-xl: calc(30rem / 16); + --c-size-control-2xs: calc(14rem / 16); --c-size-control-xs: calc(16rem / 16); --c-size-control-sm: calc(24rem / 16); --c-size-control-md: calc(34rem / 16); diff --git a/resources/js/components/ActionMenu.vue b/resources/js/components/ActionMenu.vue index c09a9987cbf..f5bba5026ff 100644 --- a/resources/js/components/ActionMenu.vue +++ b/resources/js/components/ActionMenu.vue @@ -30,16 +30,18 @@ diff --git a/resources/js/components/Combobox.vue b/resources/js/components/Combobox.vue deleted file mode 100644 index 79f8c3ca71c..00000000000 --- a/resources/js/components/Combobox.vue +++ /dev/null @@ -1,101 +0,0 @@ - - - - - diff --git a/resources/js/components/InlineFlash.vue b/resources/js/components/InlineFlash.vue new file mode 100644 index 00000000000..86acc4be651 --- /dev/null +++ b/resources/js/components/InlineFlash.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/resources/js/components/PermissionList.vue b/resources/js/components/PermissionList.vue new file mode 100644 index 00000000000..cf5d9bff549 --- /dev/null +++ b/resources/js/components/PermissionList.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/resources/js/components/form/CraftCombobox.vue b/resources/js/components/form/CraftCombobox.vue index 2123870271c..4b21500793d 100644 --- a/resources/js/components/form/CraftCombobox.vue +++ b/resources/js/components/form/CraftCombobox.vue @@ -2,13 +2,13 @@ import {t} from '@craftcms/cp'; import InputCombobox from '@/components/form/InputCombobox.vue'; import type {SelectItem} from '@/types'; - import {computed} from 'vue'; + import {computed, useSlots} from 'vue'; const emit = defineEmits<{ (e: 'update:modelValue', value: string): void; }>(); const props = defineProps<{ - modelValue: string; + modelValue: string | boolean | number; label: string; id?: string; name?: string; @@ -16,6 +16,7 @@ options?: Array; callouts?: Array; error?: string; + requireOptionMatch?: boolean; }>(); const valueProxy = computed({ @@ -26,6 +27,12 @@ emit('update:modelValue', newValue); }, }); + + const slots = useSlots(); + const forwardedSlots = computed(() => { + const {default: _, ...rest} = slots; + return rest; + }); 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" > - + 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 @@ - + diff --git a/resources/js/pages/GraphQlTokensPage.vue b/resources/js/pages/GraphQlTokensPage.vue index 04eb862a9f4..10b917e9d6e 100644 --- a/resources/js/pages/GraphQlTokensPage.vue +++ b/resources/js/pages/GraphQlTokensPage.vue @@ -97,7 +97,7 @@ >{{ t('New token') }} - + diff --git a/resources/js/pages/SettingsGeneralPage.vue b/resources/js/pages/SettingsGeneralPage.vue index 9f81c979609..e668f8f9d78 100644 --- a/resources/js/pages/SettingsGeneralPage.vue +++ b/resources/js/pages/SettingsGeneralPage.vue @@ -4,205 +4,108 @@ import {store} from '@/actions/CraftCms/Cms/Http/Controllers/Settings/GeneralSettingsController'; import {type SystemData, type TimezoneOption} from '@/types/settings'; import {useForm} from '@inertiajs/vue3'; - import useCraftData from '@/composables/useCraftData'; - import TransitionFade from '@/components/TransitionFade.vue'; import {computed} from 'vue'; - import {useEventListener} from '@vueuse/core'; - import type {SelectOption, SuggestionGroup} from '@/types'; - import CalloutReadOnly from '@/components/CalloutReadOnly.vue'; - - const props = defineProps<{ - readOnly?: boolean; - system: SystemData; - nameSuggestions?: Array; - timezoneOptions?: Array; - systemStatusOptions?: Array; - saveUrl: string; - flash?: Record; - errors: Record; - }>(); - - const flash = computed(() => props.flash); - const errors = computed(() => props.errors); + import type {SelectItem} from '@/types'; + import {useSettingsSave} from '@/composables/useSettingsSave'; + import Pane from '@/components/Pane.vue'; + import CraftCombobox from '@/components/form/CraftCombobox.vue'; + import CraftInput from '@craftcms/cp/vue/CraftInput.vue'; + import {transformBooleanOptions} from '@/utils/transformBooleanOptions'; + + const props = withDefaults( + defineProps<{ + readOnly?: boolean; + system: SystemData; + nameSuggestions?: Array; + timezoneOptions?: Array; + systemStatusOptions?: Array; + saveUrl: string; + errors: Record; + }>(), + {systemStatusOptions: () => []} + ); const form = useForm({ - name: props.system.name, + name: props.system.name ?? '', live: props.system.live, retryDuration: props.system.retryDuration, timeZone: props.system.timeZone, }); - function handleUpdate(event: CustomEvent) { - const target = event.target as HTMLSelectElement & {modelValue: string}; - if (target) { - // @ts-ignore we're just going to trust that `name` is a key of `form` for now - form[target.name] = target.modelValue; - } - } + const {save} = useSettingsSave(form, store); - // Handle cmd + s events - useEventListener('keydown', (event) => { - if ((event.metaKey || event.ctrlKey) && event.key === 's') { - event.preventDefault(); - save(); - } + const systemStatusOptions = computed(() => { + return transformBooleanOptions(props.systemStatusOptions, { + trueLabel: t('Online'), + falseLabel: t('Offline'), + }); }); - function save() { - form.clearErrors().submit(store()); - } + const liveOptions = computed(() => { + return [ + { + value: '1', + label: t('Online'), + data: { + indicator: {variant: 'success'}, + }, + }, + { + value: '0', + label: t('Offline'), + data: { + indicator: {variant: 'empty'}, + }, + }, + ...systemStatusOptions.value, + ]; + });