From b9fdfa31340684f36b7fe8af05349fcc973e7d0b Mon Sep 17 00:00:00 2001 From: i-just Date: Fri, 25 Jul 2025 10:05:21 +0100 Subject: [PATCH 1/3] adds show post and expiry date toggles to entry types Co-Authored-By: Claude Sonnet 5 --- resources/js/common/types/index.ts | 2 + .../components/EntryTypeSelect.test.ts | 2 + .../js/pages/settings/sections/Edit.test.ts | 2 + resources/translations/en/app.php | 2 + src/Cms.php | 2 +- ..._type_show_post_and_expiry_date_fields.php | 24 +++++ src/Database/Migrations/Install.php | 2 + src/Entry/Data/EntryType.php | 6 ++ src/Entry/Elements/Entry.php | 92 ++++++++++--------- src/Entry/EntryTypes.php | 18 +++- src/Entry/Models/EntryType.php | 2 + src/Gql/Resolvers/Mutations/Entry.php | 26 ++++++ .../Settings/EntryTypesController.php | 4 + .../ViewModels/EntryTypeEditViewModel.php | 4 + yii2-adapter/legacy/records/EntryType.php | 2 + 15 files changed, 146 insertions(+), 44 deletions(-) create mode 100644 src/Database/Migrations/2026_09_18_120000_add_entry_type_show_post_and_expiry_date_fields.php diff --git a/resources/js/common/types/index.ts b/resources/js/common/types/index.ts index bd1d1621084..aa743ee13a1 100644 --- a/resources/js/common/types/index.ts +++ b/resources/js/common/types/index.ts @@ -150,6 +150,8 @@ export interface EntryType { slugTranslationMethod: TranslationMethod; slugTranslationKeyFormat: null; showStatusField: boolean; + showPostDateField: boolean; + showExpiryDateField: boolean; uid: string; validateHandleUniqueness: boolean; group: null; diff --git a/resources/js/modules/entry-types/components/EntryTypeSelect.test.ts b/resources/js/modules/entry-types/components/EntryTypeSelect.test.ts index 605aa2f59e3..8c30ac09175 100644 --- a/resources/js/modules/entry-types/components/EntryTypeSelect.test.ts +++ b/resources/js/modules/entry-types/components/EntryTypeSelect.test.ts @@ -54,6 +54,8 @@ function entryType(id: number, name: string, handle: string): EntryType { slugTranslationMethod: {name: 'Site', value: 'site'}, slugTranslationKeyFormat: null, showStatusField: true, + showPostDateField: true, + showExpiryDateField: true, uid: `entry-type-${id}`, validateHandleUniqueness: true, group: null, diff --git a/resources/js/pages/settings/sections/Edit.test.ts b/resources/js/pages/settings/sections/Edit.test.ts index 087e013b4f8..9b9fb6a3432 100644 --- a/resources/js/pages/settings/sections/Edit.test.ts +++ b/resources/js/pages/settings/sections/Edit.test.ts @@ -73,6 +73,8 @@ function entryType(id: number, name: string, handle: string): EntryType { slugTranslationMethod: {name: 'Site', value: 'site'}, slugTranslationKeyFormat: null, showStatusField: true, + showPostDateField: true, + showExpiryDateField: true, uid: `entry-type-${id}`, validateHandleUniqueness: true, group: null, diff --git a/resources/translations/en/app.php b/resources/translations/en/app.php index 909011fecf4..8f16c223d00 100644 --- a/resources/translations/en/app.php +++ b/resources/translations/en/app.php @@ -1617,6 +1617,8 @@ 'Show nav' => 'Show nav', 'Show nested sources' => 'Show nested sources', 'Show sidebar' => 'Show sidebar', + 'Show the Expiry Date field' => 'Show the Expiry Date field', + 'Show the Post Date field' => 'Show the Post Date field', 'Show the Slug field' => 'Show the Slug field', 'Show the Status field' => 'Show the Status field', 'Show the search input' => 'Show the search input', diff --git a/src/Cms.php b/src/Cms.php index 6f7b6a2e53b..fa78bab28e5 100644 --- a/src/Cms.php +++ b/src/Cms.php @@ -29,7 +29,7 @@ public const string VERSION = '6.0.0-alpha.18'; - public const string SCHEMA_VERSION = '6.0.0.14'; + public const string SCHEMA_VERSION = '6.0.0.15'; public const string MIN_VERSION_REQUIRED = '5.9.0'; diff --git a/src/Database/Migrations/2026_09_18_120000_add_entry_type_show_post_and_expiry_date_fields.php b/src/Database/Migrations/2026_09_18_120000_add_entry_type_show_post_and_expiry_date_fields.php new file mode 100644 index 00000000000..940ce18de4a --- /dev/null +++ b/src/Database/Migrations/2026_09_18_120000_add_entry_type_show_post_and_expiry_date_fields.php @@ -0,0 +1,24 @@ +boolean('showPostDateField')->default(true)->after('showStatusField'); + } + + if (! Schema::hasColumn(Table::ENTRYTYPES, 'showExpiryDateField')) { + $table->boolean('showExpiryDateField')->default(true)->after('showPostDateField'); + } + }); + } + + public function down(): void {} +}; diff --git a/src/Database/Migrations/Install.php b/src/Database/Migrations/Install.php index 08bfa322838..6c48273195e 100644 --- a/src/Database/Migrations/Install.php +++ b/src/Database/Migrations/Install.php @@ -593,6 +593,8 @@ public function createTables(?Logger $logger = null): void $table->string('slugTranslationMethod')->default(TranslationMethod::Site->value); $table->text('slugTranslationKeyFormat')->nullable(); $table->boolean('showStatusField')->default(true)->nullable(); + $table->boolean('showPostDateField')->default(true); + $table->boolean('showExpiryDateField')->default(true); $table->dateTime('dateCreated'); $table->dateTime('dateUpdated'); $table->dateTime('dateDeleted')->nullable()->default(null); diff --git a/src/Entry/Data/EntryType.php b/src/Entry/Data/EntryType.php index 237331f1fa1..d2606a5ae64 100644 --- a/src/Entry/Data/EntryType.php +++ b/src/Entry/Data/EntryType.php @@ -74,6 +74,10 @@ class EntryType extends Component implements Actionable, Chippable, Colorable, C public ?bool $showStatusField = true; + public ?bool $showPostDateField = true; + + public ?bool $showExpiryDateField = true; + public ?string $uid = null; public bool $validateHandleUniqueness = true; @@ -287,6 +291,8 @@ public function getConfig(): array 'slugTranslationMethod' => $this->slugTranslationMethod->value, 'slugTranslationKeyFormat' => $this->slugTranslationKeyFormat ?: null, 'showStatusField' => $this->showStatusField, + 'showPostDateField' => $this->showPostDateField, + 'showExpiryDateField' => $this->showExpiryDateField, ]; $fieldLayout = $this->getFieldLayout(); diff --git a/src/Entry/Elements/Entry.php b/src/Entry/Elements/Entry.php index 123497b9248..78a740c8592 100644 --- a/src/Entry/Elements/Entry.php +++ b/src/Entry/Elements/Entry.php @@ -2186,31 +2186,35 @@ protected function metaFieldsNodes(bool $static): array ); } - $nodes[] = Field::make(t('Post Date')) - ->control( - DateTime::make('postDate') - ->showTime() - // Stored times aren't constrained to a picker step, and - // the screen submits natively — a coarser increment - // would make any off-step value fail validation and - // silently block saving. - ->minuteIncrement(1) - ->value(self::dateTimeControlValue($this->postDate)) - ->mode($static ? ControlMode::Disabled : ControlMode::Editable), - ); + if ($this->getType()->showPostDateField) { + $nodes[] = Field::make(t('Post Date')) + ->control( + DateTime::make('postDate') + ->showTime() + // Stored times aren't constrained to a picker step, and + // the screen submits natively — a coarser increment + // would make any off-step value fail validation and + // silently block saving. + ->minuteIncrement(1) + ->value(self::dateTimeControlValue($this->postDate)) + ->mode($static ? ControlMode::Disabled : ControlMode::Editable), + ); + } - $nodes[] = Field::make(t('Expiry Date')) - ->control( - DateTime::make('expiryDate') - ->showTime() - // Stored times aren't constrained to a picker step, and - // the screen submits natively — a coarser increment - // would make any off-step value fail validation and - // silently block saving. - ->minuteIncrement(1) - ->value(self::dateTimeControlValue($this->expiryDate)) - ->mode($static ? ControlMode::Disabled : ControlMode::Editable), - ); + if ($this->getType()->showExpiryDateField) { + $nodes[] = Field::make(t('Expiry Date')) + ->control( + DateTime::make('expiryDate') + ->showTime() + // Stored times aren't constrained to a picker step, and + // the screen submits natively — a coarser increment + // would make any off-step value fail validation and + // silently block saving. + ->minuteIncrement(1) + ->value(self::dateTimeControlValue($this->expiryDate)) + ->mode($static ? ControlMode::Disabled : ControlMode::Editable), + ); + } } return $nodes; @@ -2376,26 +2380,30 @@ public function metaFieldsHtml(bool $static): string }); // Post Date - $fields['postDate'] = FormFields::dateTimeFieldHtml([ - 'status' => $this->getAttributeStatus('postDate'), - 'label' => t('Post Date'), - 'id' => 'postDate', - 'name' => 'postDate', - 'value' => $this->postDate, - 'errors' => $this->errors()->get('postDate'), - 'disabled' => $static, - ]); + if ($this->getType()->showPostDateField) { + $fields['postDate'] = FormFields::dateTimeFieldHtml([ + 'status' => $this->getAttributeStatus('postDate'), + 'label' => t('Post Date'), + 'id' => 'postDate', + 'name' => 'postDate', + 'value' => $this->postDate, + 'errors' => $this->errors()->get('postDate'), + 'disabled' => $static, + ]); + } // Expiry Date - $fields['expiryDate'] = FormFields::dateTimeFieldHtml([ - 'status' => $this->getAttributeStatus('expiryDate'), - 'label' => t('Expiry Date'), - 'id' => 'expiryDate', - 'name' => 'expiryDate', - 'value' => $this->expiryDate, - 'errors' => $this->errors()->get('expiryDate'), - 'disabled' => $static, - ]); + if ($this->getType()->showExpiryDateField) { + $fields['expiryDate'] = FormFields::dateTimeFieldHtml([ + 'status' => $this->getAttributeStatus('expiryDate'), + 'label' => t('Expiry Date'), + 'id' => 'expiryDate', + 'name' => 'expiryDate', + 'value' => $this->expiryDate, + 'errors' => $this->errors()->get('expiryDate'), + 'disabled' => $static, + ]); + } } $fields[] = parent::metaFieldsHtml($static); diff --git a/src/Entry/EntryTypes.php b/src/Entry/EntryTypes.php index ed9993644a4..ef3fb5a3435 100644 --- a/src/Entry/EntryTypes.php +++ b/src/Entry/EntryTypes.php @@ -47,6 +47,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Schema; use InvalidArgumentException; use Throwable; use Tpetry\QueryExpressions\Language\Alias; @@ -128,7 +129,7 @@ function (object $result) { private function _createEntryTypeQuery(): Builder { - return DB::table(Table::ENTRYTYPES) + $query = DB::table(Table::ENTRYTYPES) ->select([ 'id', 'fieldLayoutId', @@ -150,6 +151,19 @@ private function _createEntryTypeQuery(): Builder 'uid', ]) ->whereNull('dateDeleted'); + + // `craft:up` reads entry types (to diff project config) before running + // pending migrations, so a DB that hasn't been migrated yet doesn't + // have these columns. + if (Schema::hasColumn(Table::ENTRYTYPES, 'showPostDateField')) { + $query->addSelect('showPostDateField'); + } + + if (Schema::hasColumn(Table::ENTRYTYPES, 'showExpiryDateField')) { + $query->addSelect('showExpiryDateField'); + } + + return $query; } /** @@ -338,6 +352,8 @@ public function handleChangedEntryType(ConfigEvent $event): void $entryTypeModel->slugTranslationMethod = $data['slugTranslationMethod'] ?? TranslationMethod::Site->value; $entryTypeModel->slugTranslationKeyFormat = $data['slugTranslationKeyFormat'] ?? null; $entryTypeModel->showStatusField = $data['showStatusField'] ?? true; + $entryTypeModel->showPostDateField = $data['showPostDateField'] ?? true; + $entryTypeModel->showExpiryDateField = $data['showExpiryDateField'] ?? true; $entryTypeModel->uid = $entryTypeUid; $entryTypeModel->description = $data['description'] ?? null; diff --git a/src/Entry/Models/EntryType.php b/src/Entry/Models/EntryType.php index fca87e5d632..229720b614b 100644 --- a/src/Entry/Models/EntryType.php +++ b/src/Entry/Models/EntryType.php @@ -34,6 +34,8 @@ protected function casts(): array 'hasTitleField' => 'bool', 'showSlugField' => 'bool', 'showStatusField' => 'bool', + 'showPostDateField' => 'bool', + 'showExpiryDateField' => 'bool', 'allowLineBreaksInTitles' => 'bool', ]; } diff --git a/src/Gql/Resolvers/Mutations/Entry.php b/src/Gql/Resolvers/Mutations/Entry.php index d8b6573eb9d..769e42451d8 100644 --- a/src/Gql/Resolvers/Mutations/Entry.php +++ b/src/Gql/Resolvers/Mutations/Entry.php @@ -60,6 +60,32 @@ public function saveEntry(mixed $source, array $arguments, mixed $context, Resol unset($arguments['enabled']); } + // If saving an entry and the postDate is provided, check if we should allow changing it. + if (array_key_exists('postDate', $arguments)) { + try { + $showPostDateField = $entry->getType()->showPostDateField; + } catch (Throwable) { + $showPostDateField = true; + } + + if (! $showPostDateField) { + unset($arguments['postDate']); + } + } + + // If saving an entry and the expiryDate is provided, check if we should allow changing it. + if (array_key_exists('expiryDate', $arguments)) { + try { + $showExpiryDateField = $entry->getType()->showExpiryDateField; + } catch (Throwable) { + $showExpiryDateField = true; + } + + if (! $showExpiryDateField) { + unset($arguments['expiryDate']); + } + } + // If saving an entry the slug is provided, check if we should allow changing it. if (array_key_exists('slug', $arguments)) { try { diff --git a/src/Http/Controllers/Settings/EntryTypesController.php b/src/Http/Controllers/Settings/EntryTypesController.php index b4b4bd91242..9ff67fd3553 100644 --- a/src/Http/Controllers/Settings/EntryTypesController.php +++ b/src/Http/Controllers/Settings/EntryTypesController.php @@ -167,6 +167,8 @@ public function renderForm(Request $request): JsonResponse 'values.slugTranslationMethod' => ['nullable', Rule::enum(TranslationMethod::class)], 'values.slugTranslationKeyFormat' => ['nullable', 'string'], 'values.showStatusField' => ['required', 'boolean'], + 'values.showPostDateField' => ['required', 'boolean'], + 'values.showExpiryDateField' => ['required', 'boolean'], 'values.fieldLayout' => ['present', 'array'], 'scope' => ['present', 'array', 'size:0'], ]); @@ -215,6 +217,8 @@ public function store(Request $request): Response $entryType->slugTranslationMethod = $request->enum('slugTranslationMethod', TranslationMethod::class, $entryType->slugTranslationMethod); $entryType->slugTranslationKeyFormat = $request->input('slugTranslationKeyFormat', $entryType->slugTranslationKeyFormat); $entryType->showStatusField = $request->boolean('showStatusField', $entryType->showStatusField); + $entryType->showPostDateField = $request->boolean('showPostDateField', $entryType->showPostDateField); + $entryType->showExpiryDateField = $request->boolean('showExpiryDateField', $entryType->showExpiryDateField); // If we're duplicating the entry type and the handle hasn't changed, find a unique one if ($saveAsNew && $entryType->handle === ($originalEntryType->handle ?? null)) { diff --git a/src/Http/ViewModels/EntryTypeEditViewModel.php b/src/Http/ViewModels/EntryTypeEditViewModel.php index a576771364a..9f095c17406 100644 --- a/src/Http/ViewModels/EntryTypeEditViewModel.php +++ b/src/Http/ViewModels/EntryTypeEditViewModel.php @@ -130,6 +130,8 @@ public function form(): FormPayload $form->add( Field::make(t('Show the Status field'), Lightswitch::make('showStatusField')), + Field::make(t('Show the Post Date field'), Lightswitch::make('showPostDateField')), + Field::make(t('Show the Expiry Date field'), Lightswitch::make('showExpiryDateField')), Separator::make('field-layout-separator'), Field::make(null, FieldLayoutDesigner::make('fieldLayout') ->elementType(Entry::class) @@ -209,6 +211,8 @@ private function initialValues(): array 'slugTranslationMethod' => $this->entryType->slugTranslationMethod->value, 'slugTranslationKeyFormat' => $this->entryType->slugTranslationKeyFormat ?? '', 'showStatusField' => (bool) $this->entryType->showStatusField, + 'showPostDateField' => (bool) $this->entryType->showPostDateField, + 'showExpiryDateField' => (bool) $this->entryType->showExpiryDateField, 'fieldLayout' => [ 'id' => $fieldLayout->id, 'uid' => $fieldLayout->uid, diff --git a/yii2-adapter/legacy/records/EntryType.php b/yii2-adapter/legacy/records/EntryType.php index c8b805bca71..692277103b3 100644 --- a/yii2-adapter/legacy/records/EntryType.php +++ b/yii2-adapter/legacy/records/EntryType.php @@ -33,6 +33,8 @@ * @property string $slugTranslationMethod Slug translation method * @property string|null $slugTranslationKeyFormat Slug translation key format * @property bool $showStatusField Whether to show the Status field + * @property bool $showPostDateField Whether to show the Post Date field + * @property bool $showExpiryDateField Whether to show the Expiry Date field * @property FieldLayout $fieldLayout Field layout * @mixin SoftDeleteBehavior * @author Pixel & Tonic, Inc. From 74560d26334a088848217f3e951c393e2998c519 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Fri, 18 Sep 2026 16:20:01 -0700 Subject: [PATCH 2/3] Release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b756b5955b4..ab6b5ed8bf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Added support for upload sessions, tus and direct S3 multipart transports, and an extensible JavaScript upload API. ([#19604](https://github.com/craftcms/cms/pull/19604)) - Removed Craft-managed filesystems and their control panel settings. Volumes and Craft asset transformers now reference Laravel filesystem disks configured in `config/filesystems.php`, and each defines whether its assets have public URLs. Existing filesystem references are migrated to matching disks automatically, with an actionable error if a disk isn’t configured. ([#19650](https://github.com/craftcms/cms/pull/19650)) - Improved performance of element queries, control panel rendering, asset transforms, date formatting, and queue status checks, and fixed related SQLite index and timezone issues. +- Added the “Show the Post Date field” and “Show the Expiry Date field” entry type settings. ([#17675](https://github.com/craftcms/cms/pull/17675)) - Added the `autoEagerLoadElements` general config setting (`true` by default), which determines whether element queries should be automatically lazy eager-loaded. ([#19637](https://github.com/craftcms/cms/pull/19637)) - Added Markdown comments to element activity timelines, with support for editing, removing, structured user mentions, and email notifications. - The image editor now supports Undo/Redo. ([#19600](https://github.com/craftcms/cms/pull/19600)) From cecaa38ab4972516ccbc6c4fd193d922587f09e9 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Fri, 18 Sep 2026 16:53:46 -0700 Subject: [PATCH 3/3] Fix EntryTypesControllerTest fixtures for the new post/expiry date fields renderForm() validates showPostDateField/showExpiryDateField as required, but two existing tests posted values payloads from before those fields existed, so they started failing with a 422. Co-Authored-By: Claude Sonnet 5 --- .../Http/Controllers/Settings/EntryTypesControllerTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Feature/Http/Controllers/Settings/EntryTypesControllerTest.php b/tests/Feature/Http/Controllers/Settings/EntryTypesControllerTest.php index f555ec417ac..12631cb5f31 100644 --- a/tests/Feature/Http/Controllers/Settings/EntryTypesControllerTest.php +++ b/tests/Feature/Http/Controllers/Settings/EntryTypesControllerTest.php @@ -108,6 +108,8 @@ 'slugTranslationMethod' => 'custom', 'slugTranslationKeyFormat' => '', 'showStatusField' => true, + 'showPostDateField' => true, + 'showExpiryDateField' => true, 'fieldLayout' => [], ]; @@ -138,6 +140,8 @@ 'allowLineBreaksInTitles' => false, 'showSlugField' => false, 'showStatusField' => true, + 'showPostDateField' => true, + 'showExpiryDateField' => true, 'fieldLayout' => [], ], 'scope' => [],