geoproject-app/public/kirby/src/Panel/PageCreateDialog.php
2025-11-24 14:01:48 +01:00

432 lines
10 KiB
PHP

<?php
namespace Kirby\Panel;
use Kirby\Cms\File;
use Kirby\Cms\Find;
use Kirby\Cms\Page;
use Kirby\Cms\PageBlueprint;
use Kirby\Cms\PageRules;
use Kirby\Cms\Site;
use Kirby\Cms\User;
use Kirby\Content\MemoryStorage;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Form\Form;
use Kirby\Toolkit\A;
use Kirby\Toolkit\I18n;
use Kirby\Uuid\Uuid;
use Kirby\Uuid\Uuids;
/**
* Manages the Panel dialog to create new pages
* @since 4.0.0
*
* @package Kirby Panel
* @author Bastian Allgeier <bastian@getkirby.com>
* @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;
}
}