* @link https://getkirby.com * @copyright Bastian Allgeier * @license https://getkirby.com/license */ class PageCreateDialog { protected PageBlueprint $blueprint; protected Page $model; protected Page|Site $parent; protected string $parentId; protected string|null $sectionId; protected string|null $slug; protected string|null $template; protected string|null $title; protected string|null $uuid; protected Page|Site|User|File $view; protected string|null $viewId; public static array $fieldTypes = [ 'checkboxes', 'date', 'email', 'info', 'line', 'link', 'list', 'number', 'multiselect', 'radio', 'range', 'select', 'slug', 'tags', 'tel', 'text', 'toggle', 'toggles', 'time', 'url' ]; public function __construct( string|null $parentId, string|null $sectionId, string|null $template, string|null $viewId, // optional string|null $slug = null, string|null $title = null, string|null $uuid = null, ) { $this->parentId = $parentId ?? 'site'; $this->parent = Find::parent($this->parentId); $this->sectionId = $sectionId; $this->slug = $slug; $this->template = $template; $this->title = $title; $this->uuid = $uuid; $this->viewId = $viewId; $this->view = Find::parent($this->viewId ?? $this->parentId); } /** * Get the blueprint settings for the new page */ public function blueprint(): PageBlueprint { // create a temporary page object return $this->blueprint ??= $this->model()->blueprint(); } /** * Get an array of all blueprints for the parent view */ public function blueprints(): array { return A::map( $this->view->blueprints($this->sectionId), function ($blueprint) { $blueprint['name'] ??= $blueprint['value'] ?? null; return $blueprint; } ); } /** * All the default fields for the dialog */ public function coreFields(): array { $fields = []; $title = $this->blueprint()->create()['title'] ?? null; $slug = $this->blueprint()->create()['slug'] ?? null; if ($title === false || $slug === false) { throw new InvalidArgumentException( message: 'Page create dialog: title and slug must not be false' ); } // title field if ($title === null || is_array($title) === true) { $label = $title['label'] ?? 'title'; $fields['title'] = Field::title([ ...$title ?? [], 'label' => I18n::translate($label, $label), 'required' => true, 'preselect' => true ]); } // slug field if ($slug === null) { $fields['slug'] = Field::slug([ 'required' => true, 'sync' => 'title', 'path' => $this->parent instanceof Page ? '/' . $this->parent->id() . '/' : '/' ]); } // pass uuid field to the dialog if uuids are enabled // to use the same uuid and prevent generating a new one // when the page is created if (Uuids::enabled() === true) { $fields['uuid'] = Field::hidden(); } return [ ...$fields, 'parent' => Field::hidden(), 'section' => Field::hidden(), 'template' => Field::hidden(), 'view' => Field::hidden(), ]; } /** * Loads custom fields for the page type */ public function customFields(): array { $custom = []; $ignore = ['title', 'slug', 'parent', 'template', 'uuid']; $blueprint = $this->blueprint(); $fields = $blueprint->fields(); foreach ($blueprint->create()['fields'] ?? [] as $name) { if (!$field = ($fields[$name] ?? null)) { throw new InvalidArgumentException( message: 'Unknown field "' . $name . '" in create dialog' ); } if (in_array($field['type'], static::$fieldTypes, true) === false) { throw new InvalidArgumentException( message: 'Field type "' . $field['type'] . '" not supported in create dialog' ); } if (in_array($name, $ignore, true) === true) { throw new InvalidArgumentException( message: 'Field name "' . $name . '" not allowed as custom field in create dialog' ); } // switch all fields to 1/1 $field['width'] = '1/1'; // add the field to the form $custom[$name] = $field; } // create form so that field props, options etc. // can be properly resolved $form = new Form( fields: $custom, model: $this->model() ); return $form->fields()->toProps(); } /** * Loads all the fields for the dialog */ public function fields(): array { return [ ...$this->coreFields(), ...$this->customFields() ]; } /** * Provides all the props for the * dialog, including the fields and * initial values */ public function load(): array { $blueprints = $this->blueprints(); $this->template ??= $blueprints[0]['name']; $status = $this->blueprint()->create()['status'] ?? 'draft'; $status = $this->blueprint()->status()[$status]['label'] ?? null; $status ??= I18n::translate('page.status.' . $status); $fields = $this->fields(); $visible = array_filter( $fields, fn ($field) => ($field['hidden'] ?? null) !== true ); // immediately submit the dialog if there is no editable field if ($visible === [] && count($blueprints) < 2) { $input = $this->value(); $response = $this->submit($input); $response['redirect'] ??= $this->parent->panel()->url(true); Panel::go($response['redirect']); } return [ 'component' => 'k-page-create-dialog', 'props' => [ 'blueprints' => $blueprints, 'fields' => $fields, 'submitButton' => I18n::template('page.create', [ 'status' => $status ]), 'template' => $this->template, 'value' => $this->value() ] ]; } /** * Temporary model for the page to * be created, used to properly render * the blueprint for fields */ public function model(): Page { if (isset($this->model) === true) { return $this->model; } $props = [ 'slug' => '__new__', 'template' => $this->template, 'model' => $this->template, 'parent' => $this->parent instanceof Page ? $this->parent : null ]; // make sure that a UUID gets generated // and added to content right away if (Uuids::enabled() === true) { $props['content'] = [ 'uuid' => $this->uuid = Uuid::generate() ]; } $this->model = Page::factory($props); // change the storage to memory immediately // since this is a temporary model // so that the model does not write to disk $this->model->changeStorage(MemoryStorage::class); return $this->model; } /** * Generates values for title and slug * from template strings from the blueprint */ public function resolveFieldTemplates(array $input): array { $title = $this->blueprint()->create()['title'] ?? null; $slug = $this->blueprint()->create()['slug'] ?? null; // create temporary page object // to resolve the template strings $page = $this->model()->clone(['content' => $input]); if (is_string($title)) { $input['title'] = $page->toSafeString($title); } if (is_string($slug)) { $input['slug'] = $page->toSafeString($slug); } return $input; } /** * Prepares and cleans up the input data */ public function sanitize(array $input): array { $input['title'] ??= $this->title ?? ''; $input['slug'] ??= $this->slug ?? ''; $input['uuid'] ??= $this->uuid ?? null; $input = $this->resolveFieldTemplates($input); $content = ['title' => trim($input['title'])]; if ($uuid = $input['uuid'] ?? null) { $content['uuid'] = $uuid; } foreach ($this->customFields() as $name => $field) { $content[$name] = $input[$name] ?? null; } // create temporary form to sanitize the input // and add default values $form = Form::for($this->model())->fill(input: $content); return [ 'content' => $form->strings(true), 'slug' => $input['slug'], 'template' => $this->template, ]; } /** * Submits the dialog form and creates the new page */ public function submit(array $input): array { $input = $this->sanitize($input); $status = $this->blueprint()->create()['status'] ?? 'draft'; // validate the input before creating the page $this->validate($input, $status); $page = $this->parent->createChild($input); if ($status !== 'draft') { // grant all permissions as the status is set in the blueprint and // should not be treated as if the user would try to change it $page->kirby()->impersonate( 'kirby', fn () => $page->changeStatus($status) ); } $payload = [ 'event' => 'page.create' ]; // add redirect, if not explicitly disabled if (($this->blueprint()->create()['redirect'] ?? null) !== false) { $payload['redirect'] = $page->panel()->url(true); } return $payload; } public function validate(array $input, string $status = 'draft'): bool { // basic validation PageRules::validateTitleLength($input['content']['title']); PageRules::validateSlugLength($input['slug']); // if the page is supposed to be published directly, // ensure that all field validations are met if ($status !== 'draft') { // create temporary form to validate the input $form = Form::for($this->model())->fill(input: $input['content']); if ($form->isInvalid() === true) { throw new InvalidArgumentException( key: 'page.changeStatus.incomplete' ); } } return true; } public function value(): array { $value = [ 'parent' => $this->parentId, 'section' => $this->sectionId, 'slug' => $this->slug ?? '', 'template' => $this->template, 'title' => $this->title ?? '', 'uuid' => $this->uuid, 'view' => $this->viewId, ]; // add default values for custom fields foreach ($this->customFields() as $name => $field) { if ($default = $field['default'] ?? null) { $value[$name] = $default; } } return $value; } }