diff --git a/resources/js/components/fieldtypes/withDefaultConfig.js b/resources/js/components/fieldtypes/withDefaultConfig.js new file mode 100644 index 00000000000..a34c20595f1 --- /dev/null +++ b/resources/js/components/fieldtypes/withDefaultConfig.js @@ -0,0 +1,5 @@ +export default function withDefaultConfig(type, config) { + const { common, types } = Statamic.$config.get('fieldtypeConfigs'); + + return { ...common, ...types[type], ...config } +} diff --git a/resources/js/components/ui/Publish/Field.vue b/resources/js/components/ui/Publish/Field.vue index d082c79016d..9e85b99c414 100644 --- a/resources/js/components/ui/Publish/Field.vue +++ b/resources/js/components/ui/Publish/Field.vue @@ -5,6 +5,7 @@ import { injectFieldsContext } from './FieldsProvider.vue'; import { Field, Icon, Tooltip, Label } from '@statamic/ui'; import FieldActions from '@statamic/components/field-actions/FieldActions.vue'; import ShowField from '@statamic/components/field-conditions/ShowField.js'; +import withDefaultConfig from '@statamic/components/fieldtypes/withDefaultConfig.js'; const props = defineProps({ config: { @@ -34,10 +35,11 @@ const { setHiddenField, } = injectContainerContext(); const { fieldPathPrefix, metaPathPrefix } = injectFieldsContext(); -const handle = props.config.handle; +const config = computed(() => withDefaultConfig(props.config.type, props.config)); +const handle = config.value.handle; const fieldtypeComponent = computed(() => { - return `${props.config.component || props.config.type}-fieldtype`; + return `${config.value.component || config.value.type}-fieldtype`; }); const fieldtypeComponentExists = computed(() => { @@ -54,7 +56,7 @@ const meta = computed(() => { const errors = computed(() => containerErrors.value[fullPath.value]); const fieldId = computed(() => `field_${fullPath.value.replaceAll('.', '_')}`); const namePrefix = ''; -const isRequired = computed(() => props.config.required); +const isRequired = computed(() => config.value.required); const fieldtype = useTemplateRef('fieldtype'); const fieldActions = computed(() => { @@ -62,7 +64,7 @@ const fieldActions = computed(() => { }); const shouldShowFieldActions = computed(() => { - return props.config.actions && fieldActions.value?.length > 0; + return config.value.actions && fieldActions.value?.length > 0; }); function valueUpdated(value) { @@ -108,10 +110,10 @@ const shouldShowField = computed(() => { hiddenFields.value, revealerFields.value, setHiddenField - ).showField(props.config, fullPath.value); + ).showField(config.value, fullPath.value); }); -const shouldShowLabelText = computed(() => !props.config.hide_display); +const shouldShowLabelText = computed(() => !config.value.hide_display); const shouldShowLabel = computed( () => @@ -120,14 +122,14 @@ const shouldShowLabel = computed( isSyncable.value, // Need to see the icon ); -const isLocalizable = computed(() => props.config.localizable); +const isLocalizable = computed(() => config.value.localizable); const isReadOnly = computed(() => { if (containerReadOnly.value) return true; if (isTrackingOriginValues.value && isSyncable.value && !isLocalizable.value) return true; - return isLocked.value || props.config.visibility === 'read_only' || false; + return isLocked.value || config.value.visibility === 'read_only' || false; }); const isLocked = computed(() => false); // todo @@ -144,7 +146,7 @@ const isSynced = computed(() => isSyncable.value && !localizedFields.value.inclu const isNested = computed(() => fullPath.value.includes('.')); const wrapperComponent = computed(() => { // Todo: Find a way to not need to hard code this. - if (props.config.type === 'dictionary_fields') return 'div'; + if (config.value.type === 'dictionary_fields') return 'div'; return asConfig.value && !isNested.value ? 'card' : 'div'; }); diff --git a/src/Fields/Field.php b/src/Fields/Field.php index 07fceecd24d..dc354e00f49 100644 --- a/src/Fields/Field.php +++ b/src/Fields/Field.php @@ -266,7 +266,7 @@ public function shouldBeDuplicated() public function toPublishArray() { - return array_merge($this->preProcessedConfig(), [ + $arr = array_merge($this->preProcessedConfig(), [ 'handle' => $this->handle, 'prefix' => $this->prefix, 'type' => $this->type(), @@ -277,6 +277,23 @@ public function toPublishArray() 'read_only' => $this->visibility() === 'read_only', // Deprecated: Addon fieldtypes should now reference new `visibility` state. 'always_save' => $this->alwaysSave(), ]); + + $defaults = [ + ...self::commonFieldOptions()->all()->map->defaultValue()->all(), + ...[ + 'read_only' => false, + 'always_save' => false, + 'prefix' => null, + ], + ...$this->fieldtype()->configFields()->all()->map->defaultValue()->all(), + ]; + + // Return the array excluding keys where the value is the same as the default. + return collect($arr) + ->reject(fn ($value, $key) => array_key_exists($key, $defaults) && $value === $defaults[$key]) + ->all(); + + return $arr; } public function setValue($value) @@ -419,9 +436,6 @@ private function preProcessedConfig() self::commonFieldOptions()->all()->map->defaultValue()->all(), $this->config, $fields->preProcess()->values()->all(), - [ - 'component' => $fieldtype->component(), - ] ); } diff --git a/src/Fields/Fieldtype.php b/src/Fields/Fieldtype.php index c4a9d93787e..66bef5718e4 100644 --- a/src/Fields/Fieldtype.php +++ b/src/Fields/Fieldtype.php @@ -61,6 +61,13 @@ public function setField(Field $field) return $this; } + public function removeField() + { + $this->field = null; + + return $this; + } + public function field(): ?Field { return $this->field; @@ -343,13 +350,9 @@ public function view() public function config(?string $key = null, $fallback = null) { - if (! $this->field) { - return $fallback; - } - $config = $this->configFields()->all() ->map->defaultValue() - ->merge($this->field->config()); + ->merge($this->field?->config() ?? []); return $key ? ($config->get($key) ?? $fallback) diff --git a/src/Fields/FieldtypeRepository.php b/src/Fields/FieldtypeRepository.php index 0fe7eefa277..1f10b626818 100644 --- a/src/Fields/FieldtypeRepository.php +++ b/src/Fields/FieldtypeRepository.php @@ -17,7 +17,7 @@ public function preloadable() public function find($handle) { if (isset($this->fieldtypes[$handle])) { - return clone $this->fieldtypes[$handle]; + return (clone $this->fieldtypes[$handle])->removeField(); } if (! ($fieldtypes = $this->classes())->has($handle)) { @@ -39,6 +39,11 @@ public function handles() }); } + public function all() + { + return $this->handles()->map(fn ($handle) => $this->find($handle)); + } + public function makeSelectableInForms($handle) { $this->selectableInForms[$handle] = true; diff --git a/src/Http/View/Composers/JavascriptComposer.php b/src/Http/View/Composers/JavascriptComposer.php index 17bba2a78ec..543d40a66b0 100644 --- a/src/Http/View/Composers/JavascriptComposer.php +++ b/src/Http/View/Composers/JavascriptComposer.php @@ -10,6 +10,8 @@ use Statamic\Facades\Preference; use Statamic\Facades\Site; use Statamic\Facades\User; +use Statamic\Fields\Field; +use Statamic\Fields\Fieldtype; use Statamic\Fieldtypes\Icon; use Statamic\Statamic; use Statamic\Support\Str; @@ -74,6 +76,7 @@ private function protectedVariables() 'hasLicenseBanner' => ! $licenses->outpostIsOffline() && ($licenses->invalid() || $licenses->requestFailed()), 'customSvgIcons' => Icon::getCustomSvgIcons(), 'commandPaletteCategories' => Category::order(), + 'fieldtypeConfigs' => $this->fieldtypeConfigs(), ]; } @@ -115,4 +118,24 @@ protected function translations(): array return array_merge($fallbackTranslations, $translations); } + + private function fieldtypeConfigs(): array + { + // TODO: Cache this. + return [ + 'common' => [ + ...Field::commonFieldOptions()->values()->all(), + ...[ + 'read_only' => false, + 'always_save' => false, + 'prefix' => null, + ], + ], + 'types' => FieldtypeRepository::all() + ->mapWithKeys(fn (Fieldtype $fieldtype) => [$fieldtype->handle() => [ + ...$fieldtype->config(), + ...['component' => $fieldtype->component()], + ]])->all(), + ]; + } }