init with kirby, vue and pagedjs interactive

This commit is contained in:
isUnknown 2025-11-24 14:01:48 +01:00
commit dc0ae26464
968 changed files with 211706 additions and 0 deletions

View file

@ -0,0 +1,97 @@
<?php
return [
// cms classes
'collection' => 'Kirby\Cms\Collection',
'file' => 'Kirby\Cms\File',
'files' => 'Kirby\Cms\Files',
'find' => 'Kirby\Cms\Find',
'helpers' => 'Kirby\Cms\Helpers',
'html' => 'Kirby\Cms\Html',
'kirby' => 'Kirby\Cms\App',
'page' => 'Kirby\Cms\Page',
'pages' => 'Kirby\Cms\Pages',
'pagination' => 'Kirby\Cms\Pagination',
'r' => 'Kirby\Cms\R',
'response' => 'Kirby\Cms\Response',
's' => 'Kirby\Cms\S',
'sane' => 'Kirby\Sane\Sane',
'site' => 'Kirby\Cms\Site',
'structure' => 'Kirby\Cms\Structure',
'url' => 'Kirby\Cms\Url',
'user' => 'Kirby\Cms\User',
'users' => 'Kirby\Cms\Users',
'visitor' => 'Kirby\Cms\Visitor',
// content classes
'field' => 'Kirby\Content\Field',
// data handler
'data' => 'Kirby\Data\Data',
'json' => 'Kirby\Data\Json',
'yaml' => 'Kirby\Data\Yaml',
// file classes
'asset' => 'Kirby\Filesystem\Asset',
'dir' => 'Kirby\Filesystem\Dir',
'f' => 'Kirby\Filesystem\F',
'mime' => 'Kirby\Filesystem\Mime',
// data classes
'database' => 'Kirby\Database\Database',
'db' => 'Kirby\Database\Db',
// exceptions
'errorpageexception' => 'Kirby\Exception\ErrorPageException',
// http classes
'cookie' => 'Kirby\Http\Cookie',
'header' => 'Kirby\Http\Header',
'remote' => 'Kirby\Http\Remote',
// image classes
'dimensions' => 'Kirby\Image\Dimensions',
// panel classes
'panel' => 'Kirby\Panel\Panel',
// template classes
'snippet' => 'Kirby\Template\Snippet',
'slot' => 'Kirby\Template\Slot',
// toolkit classes
'a' => 'Kirby\Toolkit\A',
'c' => 'Kirby\Toolkit\Config',
'config' => 'Kirby\Toolkit\Config',
'escape' => 'Kirby\Toolkit\Escape',
'i18n' => 'Kirby\Toolkit\I18n',
'obj' => 'Kirby\Toolkit\Obj',
'str' => 'Kirby\Toolkit\Str',
'tpl' => 'Kirby\Toolkit\Tpl',
'v' => 'Kirby\Toolkit\V',
'xml' => 'Kirby\Toolkit\Xml',
// Deprecated aliases:
// Any of these might be removed at any point in the future
'kirby\cms\asset' => 'Kirby\Filesystem\Asset',
'kirby\cms\content' => 'Kirby\Content\Content',
'kirby\cms\dir' => 'Kirby\Filesystem\Dir',
'kirby\cms\filename' => 'Kirby\Filesystem\Filename',
'kirby\cms\filefoundation' => 'Kirby\Filesystem\IsFile',
'kirby\cms\field' => 'Kirby\Content\Field',
'kirby\cms\form' => 'Kirby\Form\Form',
'kirby\cms\kirbytag' => 'Kirby\Text\KirbyTag',
'kirby\cms\kirbytags' => 'Kirby\Text\KirbyTags',
'kirby\cms\plugin' => 'Kirby\Plugin\Plugin',
'kirby\cms\pluginasset' => 'Kirby\Plugin\Asset',
'kirby\cms\pluginassets' => 'Kirby\Plugin\Assets',
'kirby\cms\template' => 'Kirby\Template\Template',
'kirby\form\options' => 'Kirby\Option\Options',
'kirby\form\optionsapi' => 'Kirby\Option\OptionsApi',
'kirby\form\optionsquery' => 'Kirby\Option\OptionsQuery',
'kirby\toolkit\dir' => 'Kirby\Filesystem\Dir',
'kirby\toolkit\f' => 'Kirby\Filesystem\F',
'kirby\toolkit\file' => 'Kirby\Filesystem\File',
'kirby\toolkit\mime' => 'Kirby\Filesystem\Mime',
'kirby\toolkit\query' => 'Kirby\Query\Query',
];

View file

@ -0,0 +1,27 @@
<?php
use Kirby\Exception\AuthException;
return function () {
$auth = $this->kirby()->auth();
$allowImpersonation = $this->kirby()->option('api.allowImpersonation') ?? false;
// csrf token check
if (
$auth->type($allowImpersonation) === 'session' &&
$auth->csrf() === false
) {
throw new AuthException(message: 'Unauthenticated');
}
// get user from session or basic auth
if ($user = $auth->user(null, $allowImpersonation)) {
if ($user->role()->permissions()->for('access', 'panel') === false) {
throw new AuthException(key: 'access.panel');
}
return $user;
}
throw new AuthException(message: 'Unauthenticated');
};

View file

@ -0,0 +1,78 @@
<?php
/**
* Api Collection Definitions
*/
use Kirby\Cms\Files;
use Kirby\Cms\Languages;
use Kirby\Cms\Pages;
use Kirby\Cms\Roles;
use Kirby\Cms\Translations;
use Kirby\Cms\Users;
return [
/**
* Children
*/
'children' => [
'model' => 'page',
'type' => Pages::class,
'view' => 'compact'
],
/**
* Files
*/
'files' => [
'model' => 'file',
'type' => Files::class,
],
/**
* Languages
*/
'languages' => [
'model' => 'language',
'type' => Languages::class,
],
/**
* Pages
*/
'pages' => [
'model' => 'page',
'type' => Pages::class,
'view' => 'compact'
],
/**
* Roles
*/
'roles' => [
'model' => 'role',
'type' => Roles::class,
'view' => 'compact'
],
/**
* Translations
*/
'translations' => [
'model' => 'translation',
'type' => Translations::class,
'view' => 'compact'
],
/**
* Users
*/
'users' => [
'default' => fn () => $this->users(),
'model' => 'user',
'type' => Users::class,
'view' => 'compact'
]
];

View file

@ -0,0 +1,21 @@
<?php
/**
* Api Model Definitions
*/
return [
'File' => include __DIR__ . '/models/File.php',
'FileBlueprint' => include __DIR__ . '/models/FileBlueprint.php',
'FileVersion' => include __DIR__ . '/models/FileVersion.php',
'Language' => include __DIR__ . '/models/Language.php',
'License' => include __DIR__ . '/models/License.php',
'Page' => include __DIR__ . '/models/Page.php',
'PageBlueprint' => include __DIR__ . '/models/PageBlueprint.php',
'Role' => include __DIR__ . '/models/Role.php',
'Site' => include __DIR__ . '/models/Site.php',
'SiteBlueprint' => include __DIR__ . '/models/SiteBlueprint.php',
'System' => include __DIR__ . '/models/System.php',
'Translation' => include __DIR__ . '/models/Translation.php',
'User' => include __DIR__ . '/models/User.php',
'UserBlueprint' => include __DIR__ . '/models/UserBlueprint.php',
];

View file

@ -0,0 +1,119 @@
<?php
use Kirby\Cms\File;
use Kirby\Form\Form;
/**
* File
*/
return [
'fields' => [
'blueprint' => fn (File $file) => $file->blueprint(),
'content' => fn (File $file) => Form::for($file)->values(),
'dimensions' => fn (File $file) => $file->dimensions()->toArray(),
'dragText' => fn (File $file) => $file->panel()->dragText(),
'exists' => fn (File $file) => $file->exists(),
'extension' => fn (File $file) => $file->extension(),
'filename' => fn (File $file) => $file->filename(),
'id' => fn (File $file) => $file->id(),
'link' => fn (File $file) => $file->panel()->url(true),
'mime' => fn (File $file) => $file->mime(),
'modified' => fn (File $file) => $file->modified('c'),
'name' => fn (File $file) => $file->name(),
'next' => fn (File $file) => $file->next(),
'nextWithTemplate' => function (File $file) {
$files = $file->templateSiblings()->sorted();
$index = $files->indexOf($file);
return $files->nth($index + 1);
},
'niceSize' => fn (File $file) => $file->niceSize(),
'options' => fn (File $file) => $file->panel()->options(),
'panelImage' => fn (File $file) => $file->panel()->image(),
'panelUrl' => fn (File $file) => $file->panel()->url(true),
'prev' => fn (File $file) => $file->prev(),
'prevWithTemplate' => function (File $file) {
$files = $file->templateSiblings()->sorted();
$index = $files->indexOf($file);
return $files->nth($index - 1);
},
'parent' => fn (File $file) => $file->parent(),
'parents' => fn (File $file) => $file->parents()->flip(),
'size' => fn (File $file) => $file->size(),
'template' => fn (File $file) => $file->template(),
'thumbs' => function ($file) {
if ($file->isResizable() === false) {
return null;
}
return [
'tiny' => $file->resize(128)->url(),
'small' => $file->resize(256)->url(),
'medium' => $file->resize(512)->url(),
'large' => $file->resize(768)->url(),
'huge' => $file->resize(1024)->url(),
];
},
'type' => fn (File $file) => $file->type(),
'url' => fn (File $file) => $file->url(),
'uuid' => fn (File $file) => $file->uuid()?->toString()
],
'type' => File::class,
'views' => [
'default' => [
'content',
'dimensions',
'exists',
'extension',
'filename',
'id',
'link',
'mime',
'modified',
'name',
'next' => 'compact',
'niceSize',
'parent' => 'compact',
'options',
'prev' => 'compact',
'size',
'template',
'type',
'url',
'uuid'
],
'compact' => [
'filename',
'id',
'link',
'type',
'url',
'uuid'
],
'panel' => [
'blueprint',
'content',
'dimensions',
'extension',
'filename',
'id',
'link',
'mime',
'modified',
'name',
'nextWithTemplate' => 'compact',
'niceSize',
'options',
'panelIcon',
'panelImage',
'parent' => 'compact',
'parents' => ['id', 'slug', 'title'],
'prevWithTemplate' => 'compact',
'template',
'type',
'url',
'uuid'
]
],
];

View file

@ -0,0 +1,17 @@
<?php
use Kirby\Cms\FileBlueprint;
/**
* FileBlueprint
*/
return [
'fields' => [
'name' => fn (FileBlueprint $blueprint) => $blueprint->name(),
'options' => fn (FileBlueprint $blueprint) => $blueprint->options(),
'tabs' => fn (FileBlueprint $blueprint) => $blueprint->tabs(),
'title' => fn (FileBlueprint $blueprint) => $blueprint->title(),
],
'type' => FileBlueprint::class,
'views' => [],
];

View file

@ -0,0 +1,59 @@
<?php
use Kirby\Cms\FileVersion;
/**
* FileVersion
*/
return [
'fields' => [
'dimensions' => fn (FileVersion $file) => $file->dimensions()->toArray(),
'exists' => fn (FileVersion $file) => $file->exists(),
'extension' => fn (FileVersion $file) => $file->extension(),
'filename' => fn (FileVersion $file) => $file->filename(),
'id' => fn (FileVersion $file) => $file->id(),
'mime' => fn (FileVersion $file) => $file->mime(),
'modified' => fn (FileVersion $file) => $file->modified('c'),
'name' => fn (FileVersion $file) => $file->name(),
'niceSize' => fn (FileVersion $file) => $file->niceSize(),
'size' => fn (FileVersion $file) => $file->size(),
'type' => fn (FileVersion $file) => $file->type(),
'url' => fn (FileVersion $file) => $file->url(),
],
'type' => FileVersion::class,
'views' => [
'default' => [
'dimensions',
'exists',
'extension',
'filename',
'id',
'mime',
'modified',
'name',
'niceSize',
'size',
'type',
'url'
],
'compact' => [
'filename',
'id',
'type',
'url',
],
'panel' => [
'dimensions',
'extension',
'filename',
'id',
'mime',
'modified',
'name',
'niceSize',
'template',
'type',
'url'
]
],
];

View file

@ -0,0 +1,30 @@
<?php
use Kirby\Cms\Language;
/**
* Language
*/
return [
'fields' => [
'code' => fn (Language $language) => $language->code(),
'default' => fn (Language $language) => $language->isDefault(),
'direction' => fn (Language $language) => $language->direction(),
'locale' => fn (Language $language) => $language->locale(),
'name' => fn (Language $language) => $language->name(),
'rules' => fn (Language $language) => $language->rules(),
'url' => fn (Language $language) => $language->url(),
],
'type' => Language::class,
'views' => [
'default' => [
'code',
'default',
'direction',
'locale',
'name',
'rules',
'url'
]
]
];

View file

@ -0,0 +1,17 @@
<?php
use Kirby\Cms\License;
/**
* Page
*/
return [
'fields' => [
'status' => fn (License $license) => $license->status()->value(),
'code' => function (License $license) {
return $this->kirby()->user()->isAdmin() ? $license->code() : $license->code(true);
},
'type' => fn (License $license) => $license->type()->label(),
],
'type' => License::class,
];

View file

@ -0,0 +1,96 @@
<?php
use Kirby\Cms\Page;
use Kirby\Form\Form;
/**
* Page
*/
return [
'fields' => [
'blueprint' => fn (Page $page) => $page->blueprint(),
'blueprints' => fn (Page $page) => $page->blueprints(),
'children' => fn (Page $page) => $page->children(),
'content' => fn (Page $page) => Form::for($page)->values(),
'drafts' => fn (Page $page) => $page->drafts(),
'errors' => fn (Page $page) => $page->errors(),
'files' => fn (Page $page) => $page->files()->sorted(),
'hasChildren' => fn (Page $page) => $page->hasChildren(),
'hasDrafts' => fn (Page $page) => $page->hasDrafts(),
'hasFiles' => fn (Page $page) => $page->hasFiles(),
'id' => fn (Page $page) => $page->id(),
'isSortable' => fn (Page $page) => $page->isSortable(),
'num' => fn (Page $page) => $page->num(),
'options' => fn (Page $page) => $page->panel()->options(['preview']),
'panelImage' => fn (Page $page) => $page->panel()->image(),
'parent' => fn (Page $page) => $page->parent(),
'parents' => fn (Page $page) => $page->parents()->flip(),
'previewUrl' => fn (Page $page) => $page->previewUrl(),
'siblings' => function (Page $page) {
if ($page->isDraft() === true) {
return $page->parentModel()->children()->not($page);
}
return $page->siblings();
},
'slug' => fn (Page $page) => $page->slug(),
'status' => fn (Page $page) => $page->status(),
'template' => fn (Page $page) => $page->intendedTemplate()->name(),
'title' => fn (Page $page) => $page->title()->value(),
'url' => fn (Page $page) => $page->url(),
'uuid' => fn (Page $page) => $page->uuid()?->toString()
],
'type' => Page::class,
'views' => [
'compact' => [
'id',
'title',
'url',
'num',
'uuid'
],
'default' => [
'content',
'id',
'status',
'num',
'options',
'parent' => 'compact',
'slug',
'template',
'title',
'url',
'uuid'
],
'panel' => [
'id',
'blueprint',
'content',
'status',
'options',
'next' => ['id', 'slug', 'title'],
'parents' => ['id', 'slug', 'title'],
'prev' => ['id', 'slug', 'title'],
'previewUrl',
'slug',
'title',
'url',
'uuid'
],
'selector' => [
'id',
'title',
'parent' => [
'id',
'title'
],
'children' => [
'hasChildren',
'id',
'panelIcon',
'panelImage',
'title',
],
]
],
];

View file

@ -0,0 +1,20 @@
<?php
use Kirby\Cms\PageBlueprint;
/**
* PageBlueprint
*/
return [
'fields' => [
'name' => fn (PageBlueprint $blueprint) => $blueprint->name(),
'num' => fn (PageBlueprint $blueprint) => $blueprint->num(),
'options' => fn (PageBlueprint $blueprint) => $blueprint->options(),
'preview' => fn (PageBlueprint $blueprint) => $blueprint->preview(),
'status' => fn (PageBlueprint $blueprint) => $blueprint->status(),
'tabs' => fn (PageBlueprint $blueprint) => $blueprint->tabs(),
'title' => fn (PageBlueprint $blueprint) => $blueprint->title(),
],
'type' => PageBlueprint::class,
'views' => [],
];

View file

@ -0,0 +1,23 @@
<?php
use Kirby\Cms\Role;
/**
* Role
*/
return [
'fields' => [
'description' => fn (Role $role) => $role->description(),
'name' => fn (Role $role) => $role->name(),
'permissions' => fn (Role $role) => $role->permissions()->toArray(),
'title' => fn (Role $role) => $role->title(),
],
'type' => Role::class,
'views' => [
'compact' => [
'description',
'name',
'title'
]
]
];

View file

@ -0,0 +1,52 @@
<?php
use Kirby\Cms\Site;
use Kirby\Form\Form;
/**
* Site
*/
return [
'default' => fn () => $this->site(),
'fields' => [
'blueprint' => fn (Site $site) => $site->blueprint(),
'children' => fn (Site $site) => $site->children(),
'content' => fn (Site $site) => Form::for($site)->values(),
'drafts' => fn (Site $site) => $site->drafts(),
'files' => fn (Site $site) => $site->files()->sorted(),
'options' => fn (Site $site) => $site->permissions()->toArray(),
'previewUrl' => fn (Site $site) => $site->previewUrl(),
'title' => fn (Site $site) => $site->title()->value(),
'url' => fn (Site $site) => $site->url(),
],
'type' => Site::class,
'views' => [
'compact' => [
'title',
'url'
],
'default' => [
'content',
'options',
'title',
'url'
],
'panel' => [
'title',
'blueprint',
'content',
'options',
'previewUrl',
'url'
],
'selector' => [
'title',
'children' => [
'id',
'title',
'panelIcon',
'hasChildren'
],
]
]
];

View file

@ -0,0 +1,17 @@
<?php
use Kirby\Cms\SiteBlueprint;
/**
* SiteBlueprint
*/
return [
'fields' => [
'name' => fn (SiteBlueprint $blueprint) => $blueprint->name(),
'options' => fn (SiteBlueprint $blueprint) => $blueprint->options(),
'tabs' => fn (SiteBlueprint $blueprint) => $blueprint->tabs(),
'title' => fn (SiteBlueprint $blueprint) => $blueprint->title(),
],
'type' => SiteBlueprint::class,
'views' => [],
];

View file

@ -0,0 +1,91 @@
<?php
use Kirby\Cms\System;
use Kirby\Toolkit\Str;
/**
* System
*/
return [
'fields' => [
'ascii' => fn () => Str::$ascii,
'authStatus' => fn () => $this->kirby()->auth()->status()->toArray(),
'defaultLanguage' => fn () => $this->kirby()->panelLanguage(),
'isOk' => fn (System $system) => $system->isOk(),
'isInstallable' => fn (System $system) => $system->isInstallable(),
'isInstalled' => fn (System $system) => $system->isInstalled(),
'isLocal' => fn (System $system) => $system->isLocal(),
'multilang' => fn () => $this->kirby()->option('languages', false) !== false,
'languages' => fn () => $this->kirby()->languages(),
'license' => fn (System $system) => $system->license(),
'locales' => function () {
$locales = [];
$translations = $this->kirby()->translations();
foreach ($translations as $translation) {
$locales[$translation->code()] = $translation->locale();
}
return $locales;
},
'loginMethods' => fn (System $system) => array_keys($system->loginMethods()),
'requirements' => fn (System $system) => $system->toArray(),
'site' => fn (System $system) => $system->title(),
'slugs' => fn () => Str::$language,
'title' => fn () => $this->site()->title()->value(),
'translation' => function () {
$code = $this->user()?->language() ??
$this->kirby()->panelLanguage();
return
$this->kirby()->translation($code) ??
$this->kirby()->translation('en');
},
'kirbytext' => fn () => $this->kirby()->option('panel.kirbytext') ?? true,
'user' => fn () => $this->user(),
'version' => function () {
if ($this->user()?->role()->permissions()->for('access', 'system') === true) {
return $this->kirby()->version();
}
return null;
}
],
'type' => System::class,
'views' => [
'login' => [
'authStatus',
'isOk',
'isInstallable',
'isInstalled',
'loginMethods',
'title',
'translation'
],
'troubleshooting' => [
'isOk',
'isInstallable',
'isInstalled',
'title',
'translation',
'requirements'
],
'panel' => [
'ascii',
'defaultLanguage',
'isOk',
'isInstalled',
'isLocal',
'kirbytext',
'languages',
'license',
'locales',
'multilang',
'requirements',
'site',
'slugs',
'title',
'translation',
'user' => 'auth',
'version'
]
],
];

View file

@ -0,0 +1,24 @@
<?php
use Kirby\Cms\Translation;
/**
* Translation
*/
return [
'fields' => [
'author' => fn (Translation $translation) => $translation->author(),
'data' => fn (Translation $translation) => $translation->dataWithFallback(),
'direction' => fn (Translation $translation) => $translation->direction(),
'id' => fn (Translation $translation) => $translation->id(),
'name' => fn (Translation $translation) => $translation->name(),
],
'type' => Translation::class,
'views' => [
'compact' => [
'direction',
'id',
'name'
]
]
];

View file

@ -0,0 +1,81 @@
<?php
use Kirby\Cms\User;
use Kirby\Form\Form;
/**
* User
*/
return [
'default' => fn () => $this->user(),
'fields' => [
'avatar' => fn (User $user) => $user->avatar()?->crop(512),
'blueprint' => fn (User $user) => $user->blueprint(),
'content' => fn (User $user) => Form::for($user)->values(),
'email' => fn (User $user) => $user->email(),
'files' => fn (User $user) => $user->files()->sorted(),
'id' => fn (User $user) => $user->id(),
'language' => fn (User $user) => $user->language(),
'name' => fn (User $user) => $user->name()->value(),
'next' => fn (User $user) => $user->next(),
'options' => fn (User $user) => $user->panel()->options(),
'panelImage' => fn (User $user) => $user->panel()->image(),
'permissions' => fn (User $user) => $user->role()->permissions()->toArray(),
'prev' => fn (User $user) => $user->prev(),
'role' => fn (User $user) => $user->role(),
'roles' => fn (User $user) => $user->roles(),
'username' => fn (User $user) => $user->username(),
'uuid' => fn (User $user) => $user->uuid()?->toString()
],
'type' => User::class,
'views' => [
'default' => [
'avatar',
'content',
'email',
'id',
'language',
'name',
'next' => 'compact',
'options',
'prev' => 'compact',
'role',
'username',
'uuid'
],
'compact' => [
'avatar' => 'compact',
'id',
'email',
'language',
'name',
'role' => 'compact',
'username',
'uuid'
],
'auth' => [
'avatar' => 'compact',
'permissions',
'email',
'id',
'name',
'role',
'language'
],
'panel' => [
'avatar' => 'compact',
'blueprint',
'content',
'email',
'id',
'language',
'name',
'next' => ['id', 'name'],
'options',
'prev' => ['id', 'name'],
'role',
'username',
'uuid'
],
]
];

View file

@ -0,0 +1,17 @@
<?php
use Kirby\Cms\UserBlueprint;
/**
* UserBlueprint
*/
return [
'fields' => [
'name' => fn (UserBlueprint $blueprint) => $blueprint->name(),
'options' => fn (UserBlueprint $blueprint) => $blueprint->options(),
'tabs' => fn (UserBlueprint $blueprint) => $blueprint->tabs(),
'title' => fn (UserBlueprint $blueprint) => $blueprint->title(),
],
'type' => UserBlueprint::class,
'views' => [],
];

View file

@ -0,0 +1,29 @@
<?php
/**
* Api Routes Definitions
*/
return function ($kirby) {
$routes = [
...include __DIR__ . '/routes/auth.php',
...include __DIR__ . '/routes/changes.php',
...include __DIR__ . '/routes/pages.php',
...include __DIR__ . '/routes/roles.php',
...include __DIR__ . '/routes/site.php',
...include __DIR__ . '/routes/users.php',
...include __DIR__ . '/routes/files.php',
...include __DIR__ . '/routes/system.php',
...include __DIR__ . '/routes/translations.php'
];
// only add the language routes if the
// multi language setup is activated
if ($kirby->option('languages', false) !== false) {
$routes = [
...$routes,
...include __DIR__ . '/routes/languages.php'
];
}
return $routes;
};

View file

@ -0,0 +1,126 @@
<?php
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\NotFoundException;
/**
* Authentication
*/
return [
[
'pattern' => 'auth',
'method' => 'GET',
'action' => function () {
if ($user = $this->kirby()->auth()->user()) {
return $this->resolve($user)->view('auth');
}
throw new NotFoundException(
message: 'The user cannot be found'
);
}
],
[
'pattern' => 'auth/code',
'method' => 'POST',
'auth' => false,
'action' => function () {
$auth = $this->kirby()->auth();
// csrf token check
if ($auth->type() === 'session' && $auth->csrf() === false) {
throw new InvalidArgumentException(
message: 'Invalid CSRF token'
);
}
$user = $auth->verifyChallenge($this->requestBody('code'));
return [
'code' => 200,
'status' => 'ok',
'user' => $this->resolve($user)->view('auth')->toArray()
];
}
],
[
'pattern' => 'auth/login',
'method' => 'POST',
'auth' => false,
'action' => function () {
$auth = $this->kirby()->auth();
$methods = $this->kirby()->system()->loginMethods();
// csrf token check
if ($auth->type() === 'session' && $auth->csrf() === false) {
throw new InvalidArgumentException(
message: 'Invalid CSRF token'
);
}
$email = $this->requestBody('email');
$long = $this->requestBody('long');
$password = $this->requestBody('password');
if ($password) {
if (isset($methods['password']) !== true) {
throw new InvalidArgumentException(
message: 'Login with password is not enabled'
);
}
if (
isset($methods['password']['2fa']) === true &&
$methods['password']['2fa'] === true
) {
$status = $auth->login2fa($email, $password, $long);
} else {
$user = $auth->login($email, $password, $long);
}
} else {
$mode = match (true) {
isset($methods['code']) => 'login',
isset($methods['password-reset']) => 'password-reset',
default => throw new InvalidArgumentException(
message: 'Login without password is not enabled'
)
};
$status = $auth->createChallenge($email, $long, $mode);
}
if (isset($user)) {
return [
'code' => 200,
'status' => 'ok',
'user' => $this->resolve($user)->view('auth')->toArray()
];
}
return [
'code' => 200,
'status' => 'ok',
'challenge' => $status->challenge()
];
}
],
[
'pattern' => 'auth/logout',
'method' => 'POST',
'auth' => false,
'action' => function () {
$this->kirby()->auth()->logout();
return true;
}
],
[
'pattern' => 'auth/ping',
'method' => 'POST',
'auth' => false,
'action' => function () {
// refresh the session timeout
$this->kirby()->session();
return true;
}
],
];

View file

@ -0,0 +1,37 @@
<?php
use Kirby\Api\Controller\Changes;
use Kirby\Cms\App;
use Kirby\Cms\Find;
return [
[
'pattern' => '(:all)/changes/discard',
'method' => 'POST',
'action' => function (string $path) {
return Changes::discard(
model: Find::parent($path),
);
}
],
[
'pattern' => '(:all)/changes/publish',
'method' => 'POST',
'action' => function (string $path) {
return Changes::publish(
model: Find::parent($path),
input: App::instance()->request()->get()
);
}
],
[
'pattern' => '(:all)/changes/save',
'method' => 'POST',
'action' => function (string $path) {
return Changes::save(
model: Find::parent($path),
input: App::instance()->request()->get()
);
}
],
];

View file

@ -0,0 +1,146 @@
<?php
// routing pattern to match all models with files
$filePattern = '(account/|pages/[^/]+/|site/|users/[^/]+/|)files/(:any)';
$parentPattern = '(account|pages/[^/]+|site|users/[^/]+)/files';
/**
* Files Routes
*/
return [
[
'pattern' => $filePattern . '/fields/(:any)/(:all?)',
'method' => 'ALL',
'action' => function (string $parent, string $filename, string $fieldName, string|null $path = null) {
if ($file = $this->file($parent, $filename)) {
return $this->fieldApi($file, $fieldName, $path);
}
}
],
[
'pattern' => $filePattern . '/sections/(:any)',
'method' => 'GET',
'action' => function (string $path, string $filename, string $sectionName) {
return $this->file($path, $filename)->blueprint()->section($sectionName)?->toResponse();
}
],
[
'pattern' => $filePattern . '/sections/(:any)/(:all?)',
'method' => 'ALL',
'action' => function (string $parent, string $filename, string $sectionName, string|null $path = null) {
if ($file = $this->file($parent, $filename)) {
return $this->sectionApi($file, $sectionName, $path);
}
}
],
[
'pattern' => $parentPattern,
'method' => 'GET',
'action' => function (string $path) {
return $this->files($path)->sorted();
}
],
[
'pattern' => $parentPattern,
'method' => 'POST',
'action' => function (string $path) {
// move_uploaded_file() not working with unit test
// @codeCoverageIgnoreStart
return $this->upload(function ($source, $filename) use ($path) {
// move the source file to the content folder
return $this->parent($path)->createFile([
'content' => [
'sort' => $this->requestBody('sort')
],
'source' => $source,
'template' => $this->requestBody('template'),
'filename' => $filename
], true);
});
// @codeCoverageIgnoreEnd
}
],
[
'pattern' => $parentPattern . '/search',
'method' => 'GET|POST',
'action' => function (string $path) {
$files = $this->files($path);
if ($this->requestMethod() === 'GET') {
return $files->search($this->requestQuery('q'));
}
return $files->query($this->requestBody());
}
],
[
'pattern' => $parentPattern . '/sort',
'method' => 'PATCH',
'action' => function (string $path) {
return $this->files($path)->changeSort(
$this->requestBody('files'),
$this->requestBody('index')
);
}
],
[
'pattern' => $filePattern,
'method' => 'GET',
'action' => function (string $path, string $filename) {
return $this->file($path, $filename);
}
],
[
'pattern' => $filePattern,
'method' => 'PATCH',
'action' => function (string $path, string $filename) {
return $this->file($path, $filename)->update(
$this->requestBody(),
$this->language(),
true
);
}
],
[
'pattern' => $filePattern,
'method' => 'POST',
'action' => function (string $path, string $filename) {
// move the source file from the temp dir
return $this->upload(
fn ($source) => $this->file($path, $filename)->replace($source, true)
);
}
],
[
'pattern' => $filePattern,
'method' => 'DELETE',
'action' => function (string $path, string $filename) {
return $this->file($path, $filename)->delete();
}
],
[
'pattern' => $filePattern . '/name',
'method' => 'PATCH',
'action' => function (string $path, string $filename) {
return $this->file($path, $filename)->changeName($this->requestBody('name'));
}
],
[
'pattern' => $parentPattern . '/search',
'method' => 'GET|POST',
'action' => function () {
$files = $this
->site()
->index(true)
->filter('isListable', true)
->files()
->filter('isListable', true);
if ($this->requestMethod() === 'GET') {
return $files->search($this->requestQuery('q'));
}
return $files->query($this->requestBody());
}
],
];

View file

@ -0,0 +1,35 @@
<?php
// @codeCoverageIgnoreStart
return [
'routes' => function ($kirby) {
return [
[
'pattern' => 'query',
'method' => 'POST|GET',
'auth' => $kirby->option('kql.auth') !== false,
'action' => function () use ($kirby) {
$kql = '\Kirby\Kql\Kql';
if (class_exists($kql) === false) {
return [
'code' => 500,
'status' => 'error',
'message' => 'KQL plugin is not installed',
];
}
$input = $kirby->request()->get();
$result = $kql::run($input);
return [
'code' => 200,
'result' => $result,
'status' => 'ok',
];
}
]
];
}
];
// @codeCoverageIgnoreEnd

View file

@ -0,0 +1,42 @@
<?php
/**
* Roles Routes
*/
return [
[
'pattern' => 'languages',
'method' => 'GET',
'action' => function () {
return $this->kirby()->languages();
}
],
[
'pattern' => 'languages',
'method' => 'POST',
'action' => function () {
return $this->kirby()->languages()->create($this->requestBody());
}
],
[
'pattern' => 'languages/(:any)',
'method' => 'GET',
'action' => function (string $code) {
return $this->kirby()->languages()->find($code);
}
],
[
'pattern' => 'languages/(:any)',
'method' => 'PATCH',
'action' => function (string $code) {
return $this->kirby()->languages()->find($code)?->update($this->requestBody());
}
],
[
'pattern' => 'languages/(:any)',
'method' => 'DELETE',
'action' => function (string $code) {
return $this->kirby()->languages()->find($code)?->delete();
}
]
];

View file

@ -0,0 +1,129 @@
<?php
/**
* Page Routes
*/
return [
// @codeCoverageIgnoreStart
[
'pattern' => 'pages/(:any)',
'method' => 'GET',
'action' => function (string $id) {
return $this->page($id);
}
],
[
'pattern' => 'pages/(:any)',
'method' => 'PATCH',
'action' => function (string $id) {
return $this->page($id)->update($this->requestBody(), $this->language(), true);
}
],
[
'pattern' => 'pages/(:any)',
'method' => 'DELETE',
'action' => function (string $id) {
return $this->page($id)->delete($this->requestBody('force', false));
}
],
[
'pattern' => 'pages/(:any)/blueprint',
'method' => 'GET',
'action' => function (string $id) {
return $this->page($id)->blueprint();
}
],
[
'pattern' => 'pages/(:any)/blueprints',
'method' => 'GET',
'action' => function (string $id) {
return $this->page($id)->blueprints($this->requestQuery('section'));
}
],
[
'pattern' => 'pages/(:any)/children',
'method' => 'GET',
'action' => function (string $id) {
return $this->pages($id, $this->requestQuery('status'));
}
],
[
'pattern' => 'pages/(:any)/children',
'method' => 'POST',
'action' => function (string $id) {
return $this->page($id)->createChild($this->requestBody());
}
],
[
'pattern' => 'pages/(:any)/children/search',
'method' => 'GET|POST',
'action' => function (string $id) {
return $this->searchPages($id);
}
],
[
'pattern' => 'pages/(:any)/duplicate',
'method' => 'POST',
'action' => function (string $id) {
return $this->page($id)->duplicate($this->requestBody('slug'), [
'children' => $this->requestBody('children'),
'files' => $this->requestBody('files'),
]);
}
],
[
'pattern' => 'pages/(:any)/slug',
'method' => 'PATCH',
'action' => function (string $id) {
return $this->page($id)->changeSlug($this->requestBody('slug'));
}
],
[
'pattern' => 'pages/(:any)/status',
'method' => 'PATCH',
'action' => function (string $id) {
return $this->page($id)->changeStatus($this->requestBody('status'), $this->requestBody('position'));
}
],
[
'pattern' => 'pages/(:any)/template',
'method' => 'PATCH',
'action' => function (string $id) {
return $this->page($id)->changeTemplate($this->requestBody('template'));
}
],
[
'pattern' => 'pages/(:any)/title',
'method' => 'PATCH',
'action' => function (string $id) {
return $this->page($id)->changeTitle($this->requestBody('title'));
}
],
[
'pattern' => 'pages/(:any)/fields/(:any)/(:all?)',
'method' => 'ALL',
'action' => function (string $id, string $fieldName, string|null $path = null) {
if ($page = $this->page($id)) {
return $this->fieldApi($page, $fieldName, $path);
}
}
],
[
'pattern' => 'pages/(:any)/sections/(:any)',
'method' => 'GET',
'action' => function (string $id, string $sectionName) {
return $this->page($id)->blueprint()->section($sectionName)?->toResponse();
}
],
[
'pattern' => 'pages/(:any)/sections/(:any)/(:all?)',
'method' => 'ALL',
'action' => function (string $id, string $sectionName, string|null $path = null) {
if ($page = $this->page($id)) {
return $this->sectionApi($page, $sectionName, $path);
}
}
],
// @codeCoverageIgnoreEnd
];

View file

@ -0,0 +1,27 @@
<?php
/**
* Roles Routes
*/
return [
[
'pattern' => 'roles',
'method' => 'GET',
'action' => function () {
$kirby = $this->kirby();
return match ($kirby->request()->get('canBe')) {
'changed' => $kirby->roles()->canBeChanged(),
'created' => $kirby->roles()->canBeCreated(),
default => $kirby->roles()
};
}
],
[
'pattern' => 'roles/(:any)',
'method' => 'GET',
'action' => function (string $name) {
return $this->kirby()->roles()->find($name);
}
]
];

View file

@ -0,0 +1,109 @@
<?php
/**
* Site Routes
*/
return [
// @codeCoverageIgnoreStart
[
'pattern' => 'site',
'action' => function () {
return $this->site();
}
],
[
'pattern' => 'site',
'method' => 'PATCH',
'action' => function () {
return $this->site()->update($this->requestBody(), $this->language(), true);
}
],
[
'pattern' => 'site/children',
'method' => 'GET',
'action' => function () {
return $this->pages(null, $this->requestQuery('status'));
}
],
[
'pattern' => 'site/children',
'method' => 'POST',
'action' => function () {
return $this->site()->createChild($this->requestBody());
}
],
[
'pattern' => 'site/children/search',
'method' => 'GET|POST',
'action' => function () {
return $this->searchPages();
}
],
[
'pattern' => 'site/blueprint',
'method' => 'GET',
'action' => function () {
return $this->site()->blueprint();
}
],
[
'pattern' => 'site/blueprints',
'method' => 'GET',
'action' => function () {
return $this->site()->blueprints($this->requestQuery('section'));
}
],
[
'pattern' => 'site/find',
'method' => 'POST',
'action' => function () {
return $this->site()->find(false, ...$this->requestBody());
}
],
[
'pattern' => 'site/title',
'method' => 'PATCH',
'action' => function () {
return $this->site()->changeTitle($this->requestBody('title'));
}
],
[
'pattern' => 'site/search',
'method' => 'GET|POST',
'action' => function () {
$pages = $this
->site()
->index(true)
->filter('isListable', true);
if ($this->requestMethod() === 'GET') {
return $pages->search($this->requestQuery('q'));
}
return $pages->query($this->requestBody());
}
],
[
'pattern' => 'site/fields/(:any)/(:all?)',
'method' => 'ALL',
'action' => function (string $fieldName, string|null $path = null) {
return $this->fieldApi($this->site(), $fieldName, $path);
}
],
[
'pattern' => 'site/sections/(:any)',
'method' => 'GET',
'action' => function (string $sectionName) {
return $this->site()->blueprint()->section($sectionName)?->toResponse();
}
],
[
'pattern' => 'site/sections/(:any)/(:all?)',
'method' => 'ALL',
'action' => function (string $sectionName, string|null $path = null) {
return $this->sectionApi($this->site(), $sectionName, $path);
}
],
// @codeCoverageIgnoreEnd
];

View file

@ -0,0 +1,86 @@
<?php
use Kirby\Exception\Exception;
use Kirby\Exception\InvalidArgumentException;
/**
* System Routes
*/
return [
[
'pattern' => 'system',
'method' => 'GET',
'auth' => false,
'action' => function () {
$system = $this->kirby()->system();
if ($this->kirby()->user()) {
return $system;
}
$info = match ($system->isOk()) {
true => $this->resolve($system)->view('login')->toArray(),
false => $this->resolve($system)->view('troubleshooting')->toArray()
};
return [
'status' => 'ok',
'data' => $info,
'type' => 'model'
];
}
],
[
'pattern' => 'system/register',
'method' => 'POST',
'action' => function () {
return $this->kirby()->system()->register($this->requestBody('license'), $this->requestBody('email'));
}
],
[
'pattern' => 'system/install',
'method' => 'POST',
'auth' => false,
'action' => function () {
$system = $this->kirby()->system();
$auth = $this->kirby()->auth();
// csrf token check
if ($auth->type() === 'session' && $auth->csrf() === false) {
throw new InvalidArgumentException(
message: 'Invalid CSRF token'
);
}
if ($system->isOk() === false) {
throw new Exception(
message: 'The server is not setup correctly'
);
}
if ($system->isInstallable() === false) {
throw new Exception(
message: 'The Panel cannot be installed'
);
}
if ($system->isInstalled() === true) {
throw new Exception(
message: 'The Panel is already installed'
);
}
// create the first user
$user = $this->users()->create($this->requestBody());
$token = $user->login($this->requestBody('password'));
return [
'status' => 'ok',
'token' => $token,
'user' => $this->resolve($user)->view('auth')->toArray()
];
}
]
];

View file

@ -0,0 +1,24 @@
<?php
/**
* Translations Routes
*/
return [
[
'pattern' => 'translations',
'method' => 'GET',
'auth' => false,
'action' => function () {
return $this->kirby()->translations();
}
],
[
'pattern' => 'translations/(:any)',
'method' => 'GET',
'auth' => false,
'action' => function (string $code) {
return $this->kirby()->translations()->find($code);
}
]
];

View file

@ -0,0 +1,260 @@
<?php
use Kirby\Exception\Exception;
use Kirby\Filesystem\F;
use Kirby\Toolkit\Str;
/**
* User Routes
*/
return [
// @codeCoverageIgnoreStart
[
'pattern' => 'users',
'method' => 'GET',
'action' => function () {
return $this->users()->sort('username', 'asc', 'email', 'asc');
}
],
[
'pattern' => 'users',
'method' => 'POST',
'action' => function () {
return $this->users()->create($this->requestBody());
}
],
[
'pattern' => 'users/search',
'method' => 'GET|POST',
'action' => function () {
if ($this->requestMethod() === 'GET') {
return $this->users()->search($this->requestQuery('q'));
}
return $this->users()->query($this->requestBody());
}
],
[
'pattern' => [
'(account)',
'users/(:any)',
],
'method' => 'GET',
'action' => function (string $id) {
return $this->user($id);
}
],
[
'pattern' => [
'(account)',
'users/(:any)',
],
'method' => 'PATCH',
'action' => function (string $id) {
return $this->user($id)->update($this->requestBody(), $this->language(), true);
}
],
[
'pattern' => [
'(account)',
'users/(:any)',
],
'method' => 'DELETE',
'action' => function (string $id) {
return $this->user($id)->delete();
}
],
[
'pattern' => [
'(account)/avatar',
'users/(:any)/avatar',
],
'method' => 'GET',
'action' => function (string $id) {
return $this->user($id)->avatar();
}
],
// @codeCoverageIgnoreStart
[
'pattern' => [
'(account)/avatar',
'users/(:any)/avatar',
],
'method' => 'POST',
'action' => function (string $id) {
return $this->upload(
function ($source, $filename) use ($id) {
$type = F::type($filename);
if ($type !== 'image') {
throw new Exception(
key: 'file.type.invalid',
data: compact('type')
);
}
$mime = F::mime($source);
if (Str::startsWith($mime, 'image/') !== true) {
throw new Exception(
key: 'file.mime.invalid',
data: compact('mime')
);
}
// delete the old avatar
$this->user($id)->avatar()?->delete();
$props = [
'filename' => 'profile.' . F::extension($filename),
'template' => 'avatar',
'source' => $source
];
// move the source file from the temp dir
return $this->user($id)->createFile($props, true);
},
single: true
);
}
],
// @codeCoverageIgnoreEnd
[
'pattern' => [
'(account)/avatar',
'users/(:any)/avatar',
],
'method' => 'DELETE',
'action' => function (string $id) {
return $this->user($id)->avatar()->delete();
}
],
[
'pattern' => [
'(account)/blueprint',
'users/(:any)/blueprint',
],
'method' => 'GET',
'action' => function (string $id) {
return $this->user($id)->blueprint();
}
],
[
'pattern' => [
'(account)/blueprints',
'users/(:any)/blueprints',
],
'method' => 'GET',
'action' => function (string $id) {
return $this->user($id)->blueprints($this->requestQuery('section'));
}
],
[
'pattern' => [
'(account)/email',
'users/(:any)/email',
],
'method' => 'PATCH',
'action' => function (string $id) {
return $this->user($id)->changeEmail($this->requestBody('email'));
}
],
[
'pattern' => [
'(account)/language',
'users/(:any)/language',
],
'method' => 'PATCH',
'action' => function (string $id) {
return $this->user($id)->changeLanguage($this->requestBody('language'));
}
],
[
'pattern' => [
'(account)/name',
'users/(:any)/name',
],
'method' => 'PATCH',
'action' => function (string $id) {
return $this->user($id)->changeName($this->requestBody('name'));
}
],
[
'pattern' => [
'(account)/password',
'users/(:any)/password',
],
'method' => 'PATCH',
'action' => function (string $id) {
$user = $this->user($id);
// validate password of acting user unless they have logged in to reset it;
// always validate password of acting user when changing password of other users
if ($this->session()->get('kirby.resetPassword') !== true || $this->user()->is($user) !== true) {
$this->user()->validatePassword($this->requestBody('currentPassword'));
}
$result = $user->changePassword($this->requestBody('password'));
// if we changed the password of the current user…
if ($user->isLoggedIn() === true) {
// …don't allow additional resets (now the password is known again)
$this->session()->remove('kirby.resetPassword');
}
return $result;
}
],
[
'pattern' => [
'(account)/role',
'users/(:any)/role',
],
'method' => 'PATCH',
'action' => function (string $id) {
return $this->user($id)->changeRole($this->requestBody('role'));
}
],
[
'pattern' => [
'(account)/roles',
'users/(:any)/roles',
],
'action' => function (string $id) {
$kirby = $this->kirby();
$purpose = $kirby->request()->get('purpose');
return $this->user($id)->roles($purpose);
}
],
[
'pattern' => [
'(account)/fields/(:any)/(:all?)',
'users/(:any)/fields/(:any)/(:all?)',
],
'method' => 'ALL',
'action' => function (string $id, string $fieldName, string|null $path = null) {
return $this->fieldApi($this->user($id), $fieldName, $path);
}
],
[
'pattern' => [
'(account)/sections/(:any)',
'users/(:any)/sections/(:any)',
],
'method' => 'GET',
'action' => function (string $id, string $sectionName) {
if ($section = $this->user($id)->blueprint()->section($sectionName)) {
return $section->toResponse();
}
}
],
[
'pattern' => [
'(account)/sections/(:any)/(:all?)',
'users/(:any)/sections/(:any)/(:all?)',
],
'method' => 'ALL',
'action' => function (string $id, string $sectionName, string|null $path = null) {
return $this->sectionApi($this->user($id), $sectionName, $path);
}
],
// @codeCoverageIgnoreEnd
];

View file

@ -0,0 +1,16 @@
<?php
use Kirby\Toolkit\I18n;
return function () {
return [
'icon' => 'account',
'label' => I18n::translate('view.account'),
'search' => 'users',
'buttons' => require __DIR__ . '/account/buttons.php',
'dialogs' => require __DIR__ . '/account/dialogs.php',
'drawers' => require __DIR__ . '/account/drawers.php',
'dropdowns' => require __DIR__ . '/account/dropdowns.php',
'views' => require __DIR__ . '/account/views.php'
];
};

View file

@ -0,0 +1,13 @@
<?php
use Kirby\Cms\App;
use Kirby\Cms\User;
use Kirby\Panel\Ui\Buttons\ViewButton;
return [
'user.theme' => function (App $kirby, User $user) {
if ($kirby->user()->is($user) === true) {
return new ViewButton(component: 'k-theme-view-button');
}
}
];

View file

@ -0,0 +1,65 @@
<?php
use Kirby\Panel\UserTotpEnableDialog;
$dialogs = require __DIR__ . '/../users/dialogs.php';
return [
'account.changeEmail' => [
...$dialogs['user.changeEmail'],
'pattern' => '(account)/changeEmail',
],
'account.changeLanguage' => [
...$dialogs['user.changeLanguage'],
'pattern' => '(account)/changeLanguage',
],
'account.changeName' => [
...$dialogs['user.changeName'],
'pattern' => '(account)/changeName',
],
'account.changePassword' => [
...$dialogs['user.changePassword'],
'pattern' => '(account)/changePassword',
],
'account.changeRole' => [
...$dialogs['user.changeRole'],
'pattern' => '(account)/changeRole',
],
'account.delete' => [
...$dialogs['user.delete'],
'pattern' => '(account)/delete',
],
'account.fields' => [
...$dialogs['user.fields'],
'pattern' => '(account)/fields/(:any)/(:all?)',
],
'account.file.changeName' => [
...$dialogs['user.file.changeName'],
'pattern' => '(account)/files/(:any)/changeName',
],
'account.file.changeSort' => [
...$dialogs['user.file.changeSort'],
'pattern' => '(account)/files/(:any)/changeSort',
],
'account.file.changeTemplate' => [
...$dialogs['user.file.changeTemplate'],
'pattern' => '(account)/files/(:any)/changeTemplate',
],
'account.file.delete' => [
...$dialogs['user.file.delete'],
'pattern' => '(account)/files/(:any)/delete',
],
'account.file.fields' => [
...$dialogs['user.file.fields'],
'pattern' => '(account)/files/(:any)/fields/(:any)/(:all?)',
],
'account.totp.enable' => [
'pattern' => '(account)/totp/enable',
'load' => fn () => (new UserTotpEnableDialog())->load(),
'submit' => fn () => (new UserTotpEnableDialog())->submit()
],
'account.totp.disable' => [
'pattern' => '(account)/totp/disable',
...$dialogs['user.totp.disable'],
],
];

View file

@ -0,0 +1,14 @@
<?php
$drawers = require __DIR__ . '/../users/drawers.php';
return [
'account.fields' => [
...$drawers['user.fields'],
'pattern' => '(account)/fields/(:any)/(:all?)',
],
'account.file.fields' => [
...$drawers['user.file.fields'],
'pattern' => '(account)/files/(:any)/fields/(:any)/(:all?)',
],
];

View file

@ -0,0 +1,22 @@
<?php
$dropdowns = require __DIR__ . '/../users/dropdowns.php';
return [
'account' => [
...$dropdowns['user'],
'pattern' => '(account)',
],
'account.languages' => [
...$dropdowns['user.languages'],
'pattern' => '(account)/languages',
],
'account.file' => [
...$dropdowns['user.file'],
'pattern' => '(account)/files/(:any)',
],
'account.file.languages' => [
...$dropdowns['user.file.languages'],
'pattern' => '(account)/files/(:any)/languages',
]
];

View file

@ -0,0 +1,35 @@
<?php
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Toolkit\I18n;
return [
'account' => [
'pattern' => 'account',
'action' => fn () => [
'component' => 'k-account-view',
'props' => App::instance()->user()->panel()->props(),
],
],
'account.file' => [
'pattern' => 'account/files/(:any)',
'action' => function (string $filename) {
return Find::file('account', $filename)->panel()->view();
}
],
'account.password' => [
'pattern' => 'reset-password',
'action' => fn () => [
'component' => 'k-reset-password-view',
'breadcrumb' => [
[
'label' => I18n::translate('view.resetPassword')
]
],
'props' => [
'requirePassword' => App::instance()->session()->get('kirby.resetPassword') !== true
]
]
]
];

View file

@ -0,0 +1,61 @@
<?php
use Kirby\Cms\Find;
use Kirby\Panel\Field;
return [
'model' => [
'load' => function (
string $modelPath,
string $fieldName,
string|null $path = null
) {
return Field::dialog(
model: Find::parent($modelPath),
fieldName: $fieldName,
path: $path,
method: 'GET'
);
},
'submit' => function (
string $modelPath,
string $fieldName,
string|null $path = null
) {
return Field::dialog(
model: Find::parent($modelPath),
fieldName: $fieldName,
path: $path,
method: 'POST'
);
}
],
'file' => [
'load' => function (
string $modelPath,
string $filename,
string $fieldName,
string|null $path = null
) {
return Field::dialog(
model: Find::file($modelPath, $filename),
fieldName: $fieldName,
path: $path,
method: 'GET'
);
},
'submit' => function (
string $modelPath,
string $filename,
string $fieldName,
string|null $path = null
) {
return Field::dialog(
model: Find::file($modelPath, $filename),
fieldName: $fieldName,
path: $path,
method: 'POST'
);
}
],
];

View file

@ -0,0 +1,61 @@
<?php
use Kirby\Cms\Find;
use Kirby\Panel\Field;
return [
'model' => [
'load' => function (
string $modelPath,
string $fieldName,
string|null $path = null
) {
return Field::drawer(
model: Find::parent($modelPath),
fieldName: $fieldName,
path: $path,
method: 'GET'
);
},
'submit' => function (
string $modelPath,
string $fieldName,
string|null $path = null
) {
return Field::drawer(
model: Find::parent($modelPath),
fieldName: $fieldName,
path: $path,
method: 'POST'
);
}
],
'file' => [
'load' => function (
string $modelPath,
string $filename,
string $fieldName,
string|null $path = null
) {
return Field::drawer(
model: Find::file($modelPath, $filename),
fieldName: $fieldName,
path: $path,
method: 'GET'
);
},
'submit' => function (
string $modelPath,
string $filename,
string $fieldName,
string|null $path = null
) {
return Field::drawer(
model: Find::file($modelPath, $filename),
fieldName: $fieldName,
path: $path,
method: 'POST'
);
}
],
];

View file

@ -0,0 +1,14 @@
<?php
use Kirby\Cms\File;
use Kirby\Panel\Ui\Buttons\OpenButton;
use Kirby\Panel\Ui\Buttons\SettingsButton;
return [
'file.open' => function (File $file) {
return new OpenButton(link: $file->previewUrl());
},
'file.settings' => function (File $file) {
return new SettingsButton(model: $file);
}
];

View file

@ -0,0 +1,165 @@
<?php
use Kirby\Cms\Find;
use Kirby\Panel\Field;
use Kirby\Panel\Panel;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\I18n;
/**
* Shared file dialogs
* They are included in the site and
* users area to create dialogs there.
* The array keys are replaced by
* the appropriate routes in the areas.
*/
return [
'changeName' => [
'load' => function (string $path, string $filename) {
$file = Find::file($path, $filename);
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'name' => [
'label' => I18n::translate('name'),
'type' => 'slug',
'required' => true,
'icon' => 'title',
'allow' => 'a-z0-9@._-',
'after' => '.' . $file->extension(),
'preselect' => true
]
],
'submitButton' => I18n::translate('rename'),
'value' => [
'name' => $file->name(),
]
]
];
},
'submit' => function (string $path, string $filename) {
$file = Find::file($path, $filename);
$name = $file->kirby()->request()->get('name');
$renamed = $file->changeName($name);
$oldUrl = $file->panel()->url(true);
$newUrl = $renamed->panel()->url(true);
$response = [
'event' => 'file.changeName'
];
// check for a necessary redirect after the filename has changed
if (Panel::referrer() === $oldUrl && $oldUrl !== $newUrl) {
$response['redirect'] = $newUrl;
}
return $response;
}
],
'changeSort' => [
'load' => function (string $path, string $filename) {
$file = Find::file($path, $filename);
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'position' => Field::filePosition($file)
],
'submitButton' => I18n::translate('change'),
'value' => [
'position' => $file->sort()->isEmpty() ? $file->siblings(false)->count() + 1 : $file->sort()->toInt(),
]
]
];
},
'submit' => function (string $path, string $filename) {
$file = Find::file($path, $filename);
$files = $file->siblings()->sorted();
$ids = $files->keys();
$newIndex = (int)($file->kirby()->request()->get('position')) - 1;
$oldIndex = $files->indexOf($file);
array_splice($ids, $oldIndex, 1);
array_splice($ids, $newIndex, 0, $file->id());
$files->changeSort($ids);
return [
'event' => 'file.sort',
];
}
],
'changeTemplate' => [
'load' => function (string $path, string $filename) {
$file = Find::file($path, $filename);
$blueprints = $file->blueprints();
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'warning' => [
'type' => 'info',
'theme' => 'notice',
'text' => I18n::translate('file.changeTemplate.notice')
],
'template' => Field::template($blueprints, [
'required' => true
])
],
'theme' => 'notice',
'submitButton' => I18n::translate('change'),
'value' => [
'template' => $file->template()
]
]
];
},
'submit' => function (string $path, string $filename) {
$file = Find::file($path, $filename);
$template = $file->kirby()->request()->get('template');
$file->changeTemplate($template);
return [
'event' => 'file.changeTemplate',
];
}
],
'delete' => [
'load' => function (string $path, string $filename) {
$file = Find::file($path, $filename);
return [
'component' => 'k-remove-dialog',
'props' => [
'text' => I18n::template('file.delete.confirm', [
'filename' => Escape::html($file->filename())
]),
]
];
},
'submit' => function (string $path, string $filename) {
$file = Find::file($path, $filename);
$redirect = false;
$referrer = Panel::referrer();
$url = $file->panel()->url(true);
$file->delete();
// redirect to the parent model URL
// if the dialog has been opened in the file view
if ($referrer === $url) {
$redirect = $file->parent()->panel()->url(true);
}
return [
'event' => 'file.delete',
'redirect' => $redirect
];
}
],
];

View file

@ -0,0 +1,14 @@
<?php
use Kirby\Cms\Find;
use Kirby\Panel\Ui\Buttons\LanguagesDropdown;
return [
'file' => function (string $parent, string $filename) {
return Find::file($parent, $filename)->panel()->dropdown();
},
'language' => function (string $parent, string $filename) {
$file = Find::file($parent, $filename);
return (new LanguagesDropdown($file))->options();
}
];

View file

@ -0,0 +1,40 @@
<?php
use Kirby\Panel\Panel;
use Kirby\Toolkit\I18n;
return function ($kirby) {
return [
'icon' => 'settings',
'label' => I18n::translate('view.installation'),
'views' => [
'installation' => [
'pattern' => 'installation',
'auth' => false,
'action' => function () use ($kirby) {
$system = $kirby->system();
return [
'component' => 'k-installation-view',
'props' => [
'isInstallable' => $system->isInstallable(),
'isInstalled' => $system->isInstalled(),
'isOk' => $system->isOk(),
'requirements' => $system->status(),
'translations' => $kirby->translations()->values(function ($translation) {
return [
'text' => $translation->name(),
'value' => $translation->code(),
];
}),
]
];
}
],
'installation.fallback' => [
'pattern' => '(:all)',
'auth' => false,
'action' => fn () => Panel::go('installation')
]
]
];
};

View file

@ -0,0 +1,11 @@
<?php
return function () {
return [
'icon' => 'lab',
'label' => 'Lab',
'menu' => false,
'drawers' => require __DIR__ . '/lab/drawers.php',
'views' => require __DIR__ . '/lab/views.php'
];
};

View file

@ -0,0 +1,31 @@
<?php
use Kirby\Panel\Lab\Doc;
use Kirby\Panel\Lab\Docs;
return [
'lab.docs' => [
'pattern' => 'lab/docs/(:any)',
'load' => function (string $component) {
if (Docs::isInstalled() === false) {
return [
'component' => 'k-text-drawer',
'props' => [
'text' => 'The UI docs are not installed.'
]
];
}
$doc = Doc::factory($component);
return [
'component' => 'k-lab-docs-drawer',
'props' => [
'icon' => 'book',
'title' => $component,
'docs' => $doc->toArray()
]
];
},
],
];

View file

@ -0,0 +1,219 @@
<?php
use Kirby\Cms\App;
use Kirby\Panel\Lab\Category;
use Kirby\Panel\Lab\Doc;
use Kirby\Panel\Lab\Docs;
return [
'lab' => [
'pattern' => 'lab',
'action' => function () {
return [
'component' => 'k-lab-index-view',
'props' => [
'categories' => Category::all(),
'info' => Category::isInstalled() ? null : 'The default Lab examples are not installed.',
'tab' => 'examples',
],
];
}
],
'lab.docs' => [
'pattern' => 'lab/docs',
'action' => function () {
$view = [
'component' => 'k-lab-index-view',
'title' => 'Docs',
'breadcrumb' => [
[
'label' => 'Docs',
'link' => 'lab/docs'
]
]
];
// if docs are not installed, show info message
if (Docs::isInstalled() === false) {
return [
...$view,
'props' => [
'info' => 'The UI docs are not installed.',
'tab' => 'docs',
],
];
}
return [
...$view,
'props' => [
'categories' => [
['examples' => Docs::all()]
],
'tab' => 'docs',
],
];
}
],
'lab.doc' => [
'pattern' => 'lab/docs/(:any)',
'action' => function (string $component) {
$crumbs = [
[
'label' => 'Docs',
'link' => 'lab/docs'
],
[
'label' => $component,
'link' => 'lab/docs/' . $component
]
];
if (Docs::isInstalled() === false) {
return [
'component' => 'k-lab-index-view',
'title' => $component,
'breadcrumb' => $crumbs,
'props' => [
'info' => 'The UI docs are not installed.',
'tab' => 'docs',
],
];
}
$doc = Doc::factory($component);
if ($doc === null) {
return [
'component' => 'k-lab-index-view',
'title' => $component,
'breadcrumb' => $crumbs,
'props' => [
'info' => 'No UI docs found for ' . $component . '.',
'tab' => 'docs',
],
];
}
// header buttons
$buttons = [];
if ($lab = $doc->lab()) {
$buttons[] = [
'props' => [
'text' => 'Lab examples',
'icon' => 'lab',
'link' => '/lab/' . $lab
]
];
}
$buttons[] = [
'props' => [
'icon' => 'github',
'link' => $doc->source(),
'target' => '_blank'
]
];
return [
'component' => 'k-lab-docs-view',
'title' => $component,
'breadcrumb' => $crumbs,
'props' => [
'buttons' => $buttons,
'component' => $component,
'docs' => $doc->toArray(),
'lab' => $lab
]
];
}
],
'lab.vue' => [
'pattern' => [
'lab/(:any)/(:any)/index.vue',
'lab/(:any)/(:any)/(:any)/index.vue'
],
'action' => function (
string $category,
string $id,
string|null $tab = null
) {
return Category::factory($category)->example($id, $tab)->serve();
}
],
'lab.example' => [
'pattern' => 'lab/(:any)/(:any)/(:any?)',
'action' => function (
string $category,
string $id,
string|null $tab = null
) {
$category = Category::factory($category);
$example = $category->example($id, $tab);
$props = $example->props();
$vue = $example->vue();
$compiler = App::instance()->option('panel.vue.compiler', true);
if ($doc = $props['docs'] ?? null) {
$doc = Doc::factory($doc);
}
$github = $doc?->source();
if ($source = $props['source'] ?? null) {
$github ??= 'https://github.com/getkirby/kirby/tree/main/' . $source;
}
// header buttons
$buttons = [];
if ($doc) {
$buttons[] = [
'props' => [
'text' => $doc->name,
'icon' => 'book',
'drawer' => 'lab/docs/' . $doc->name
]
];
}
if ($github) {
$buttons[] = [
'props' => [
'icon' => 'github',
'link' => $github,
'target' => '_blank'
]
];
}
return [
'component' => 'k-lab-playground-view',
'breadcrumb' => [
[
'label' => $category->name(),
],
[
'label' => $example->title(),
'link' => $example->url()
]
],
'props' => [
'buttons' => $buttons,
'compiler' => $compiler,
'docs' => $doc?->name,
'examples' => $vue['examples'],
'file' => $example->module(),
'github' => $github,
'props' => $props,
'styles' => $vue['style'],
'tab' => $example->tab(),
'tabs' => array_values($example->tabs()),
'template' => $vue['template'],
'title' => $example->title(),
],
];
}
]
];

View file

@ -0,0 +1,14 @@
<?php
use Kirby\Toolkit\I18n;
return function ($kirby) {
return [
'icon' => 'translate',
'label' => I18n::translate('view.languages'),
'menu' => true,
'buttons' => require __DIR__ . '/languages/buttons.php',
'dialogs' => require __DIR__ . '/languages/dialogs.php',
'views' => require __DIR__ . '/languages/views.php'
];
};

View file

@ -0,0 +1,21 @@
<?php
use Kirby\Cms\Language;
use Kirby\Panel\Ui\Buttons\LanguageCreateButton;
use Kirby\Panel\Ui\Buttons\LanguageDeleteButton;
use Kirby\Panel\Ui\Buttons\LanguageSettingsButton;
use Kirby\Panel\Ui\Buttons\OpenButton;
return [
'languages.create' => fn () =>
new LanguageCreateButton(),
'language.open' => fn (Language $language) =>
new OpenButton(link: $language->url()),
'language.settings' => fn (Language $language) =>
new LanguageSettingsButton($language),
'language.delete' => function (Language $language) {
if ($language->isDeletable() === true) {
return new LanguageDeleteButton($language);
}
}
];

View file

@ -0,0 +1,324 @@
<?php
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Cms\Language;
use Kirby\Cms\LanguageVariable;
use Kirby\Exception\NotFoundException;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\I18n;
$languageDialogFields = [
'name' => [
'counter' => false,
'label' => I18n::translate('language.name'),
'type' => 'text',
'required' => true,
'icon' => 'title'
],
'code' => [
'label' => I18n::translate('language.code'),
'type' => 'text',
'required' => true,
'counter' => false,
'icon' => 'translate',
'width' => '1/2'
],
'direction' => [
'label' => I18n::translate('language.direction'),
'type' => 'select',
'required' => true,
'empty' => false,
'options' => [
['value' => 'ltr', 'text' => I18n::translate('language.direction.ltr')],
['value' => 'rtl', 'text' => I18n::translate('language.direction.rtl')]
],
'width' => '1/2'
],
'locale' => [
'counter' => false,
'label' => I18n::translate('language.locale'),
'type' => 'text',
],
];
$translationDialogFields = [
'key' => [
'counter' => false,
'icon' => null,
'label' => I18n::translate('language.variable.key'),
'type' => 'text'
],
'multiple' => [
'label' => I18n::translate('language.variable.multiple'),
'text' => I18n::translate('language.variable.multiple.text'),
'help' => I18n::translate('language.variable.multiple.help'),
'type' => 'toggle'
],
'value' => [
'buttons' => false,
'counter' => false,
'label' => I18n::translate('language.variable.value'),
'type' => 'textarea',
'when' => [
'multiple' => false
]
],
'entries' => [
'field' => ['type' => 'text'],
'label' => I18n::translate('language.variable.entries'),
'help' => I18n::translate('language.variable.entries.help'),
'type' => 'entries',
'min' => 1,
'when' => [
'multiple' => true
],
]
];
return [
// create language
'language.create' => [
'pattern' => 'languages/create',
'load' => function () use ($languageDialogFields) {
return [
'component' => 'k-language-dialog',
'props' => [
'fields' => $languageDialogFields,
'submitButton' => I18n::translate('language.create'),
'value' => [
'code' => '',
'direction' => 'ltr',
'locale' => '',
'name' => '',
]
]
];
},
'submit' => function () {
$kirby = App::instance();
$data = $kirby->request()->get([
'code',
'direction',
'locale',
'name'
]);
$kirby->languages()->create($data);
return [
'event' => 'language.create'
];
}
],
// delete language
'language.delete' => [
'pattern' => 'languages/(:any)/delete',
'load' => function (string $id) {
$language = Find::language($id);
return [
'component' => 'k-remove-dialog',
'props' => [
'text' => I18n::template('language.delete.confirm', [
'name' => Escape::html($language->name())
])
]
];
},
'submit' => function (string $id) {
Find::language($id)->delete();
return [
'event' => 'language.delete',
'redirect' => 'languages'
];
}
],
// update language
'language.update' => [
'pattern' => 'languages/(:any)/update',
'load' => function (string $id) use ($languageDialogFields) {
$language = Find::language($id);
$fields = $languageDialogFields;
$locale = $language->locale();
// use the first locale key if there's only one
if (count($locale) === 1) {
$locale = A::first($locale);
}
// the code of an existing language cannot be changed
$fields['code']['disabled'] = true;
// if the locale settings is more complex than just a
// single string, the text field won't do it anymore.
// Changes can only be made in the language file and
// we display a warning box instead.
if (is_array($locale) === true) {
$fields['locale'] = [
'label' => $fields['locale']['label'],
'type' => 'info',
'text' => I18n::translate('language.locale.warning')
];
}
return [
'component' => 'k-language-dialog',
'props' => [
'fields' => $fields,
'submitButton' => I18n::translate('save'),
'value' => [
'code' => $language->code(),
'direction' => $language->direction(),
'locale' => $locale,
'name' => $language->name(),
'rules' => $language->rules(),
]
]
];
},
'submit' => function (string $id) {
$kirby = App::instance();
$data = $kirby->request()->get(['direction', 'locale', 'name']);
$language = Find::language($id)->update($data);
return [
'event' => 'language.update'
];
}
],
'language.translation.create' => [
'pattern' => 'languages/(:any)/translations/create',
'load' => function (string $languageCode) use ($translationDialogFields) {
// find the language to make sure it exists
Find::language($languageCode);
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => $translationDialogFields,
'size' => 'large',
'value' => [
'multiple' => false,
]
],
];
},
'submit' => function (string $languageCode) {
$request = App::instance()->request();
$language = Find::language($languageCode);
$key = $request->get('key', '');
$multiple = $request->get('multiple', false);
$value = match ($multiple) {
true => $request->get('entries', []),
default => $request->get('value', '')
};
LanguageVariable::create($key, $value);
if ($language->isDefault() === false) {
$language->variable($key)->update($value);
}
return true;
}
],
'language.translation.delete' => [
'pattern' => 'languages/(:any)/translations/(:any)/delete',
'load' => function (string $languageCode, string $translationKey) {
$variable = Find::language($languageCode)->variable($translationKey, true);
if ($variable->exists() === false) {
throw new NotFoundException(
key: 'language.variable.notFound'
);
}
return [
'component' => 'k-remove-dialog',
'props' => [
'text' => I18n::template('language.variable.delete.confirm', [
'key' => Escape::html($variable->key())
])
],
];
},
'submit' => function (string $languageCode, string $translationKey) {
return Find::language($languageCode)->variable($translationKey, true)->delete();
}
],
'language.translation.update' => [
'pattern' => 'languages/(:any)/translations/(:any)/update',
'load' => function (string $languageCode, string $translationKey) use ($translationDialogFields) {
$language = Find::language($languageCode);
$variable = $language->variable($translationKey, true);
if ($variable->exists() === false) {
throw new NotFoundException(
key: 'language.variable.notFound'
);
}
$fields = $translationDialogFields;
// the key field cannot be changed
// the multiple field is hidden
$fields['key']['disabled'] = true;
$fields['multiple']['type'] = 'hidden';
// check if the variable has multiple values;
// ensure to use the default language for this check because
// the variable might not exist in the current language but
// already be defined in the default language with multiple values
$isVariableArray = Language::ensure('default')->variable($translationKey, true)->hasMultipleValues();
// set the correct value field
// when value is string, set value for value field
// when value is array, set value for entries field
if ($isVariableArray === true) {
$fields['entries']['autofocus'] = true;
$value = [
'entries' => $variable->value(),
'key' => $variable->key(),
'multiple' => true
];
} else {
$fields['value']['autofocus'] = true;
$value = [
'key' => $variable->key(),
'multiple' => false,
'value' => $variable->value()
];
}
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => $fields,
'size' => 'large',
'value' => $value
]
];
},
'submit' => function (string $languageCode, string $translationKey) {
$request = App::instance()->request();
$multiple = $request->get('multiple', false);
$value = match ($multiple) {
true => $request->get('entries', []),
default => $request->get('value', '')
};
Find::language($languageCode)->variable($translationKey, true)->update($value);
return true;
}
]
];

View file

@ -0,0 +1,137 @@
<?php
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Panel\Ui\Buttons\ViewButtons;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\I18n;
return [
'language' => [
'pattern' => 'languages/(:any)',
'when' => function (): bool {
return App::instance()->option('languages.variables', true) !== false;
},
'action' => function (string $code) {
$kirby = App::instance();
$language = Find::language($code);
$link = '/languages/' . $language->code();
$strings = [];
$foundation = $kirby->defaultLanguage()->translations();
$translations = $language->translations();
// TODO: update following line and adapt for update and
// delete options when `languageVariables.*` permissions available
$canUpdate = $kirby->role()?->permissions()->for('languages', 'update') === true;
ksort($foundation);
foreach ($foundation as $key => $value) {
$strings[] = [
'key' => $key,
'value' => $translations[$key] ?? null,
'options' => [
[
'click' => 'update',
'disabled' => $canUpdate === false,
'icon' => 'edit',
'text' => I18n::translate('edit'),
],
[
'click' => 'delete',
'disabled' => $canUpdate === false || $language->isDefault() === false,
'icon' => 'trash',
'text' => I18n::translate('delete'),
]
]
];
}
$next = function () use ($language) {
if ($next = $language->next()) {
return [
'link' => '/languages/' . $next->code(),
'title' => $next->name(),
];
}
};
$prev = function () use ($language) {
if ($prev = $language->prev()) {
return [
'link' => '/languages/' . $prev->code(),
'title' => $prev->name(),
];
}
};
return [
'component' => 'k-language-view',
'breadcrumb' => [
[
'label' => $name = $language->name(),
'link' => $link,
]
],
'props' => [
'buttons' => fn () =>
ViewButtons::view('language', model: $language)
->defaults('open', 'settings', 'delete')
->render(),
'deletable' => $language->isDeletable(),
'code' => Escape::html($language->code()),
'default' => $language->isDefault(),
'direction' => $language->direction(),
'id' => $language->code(),
'info' => [
[
'label' => 'Status',
'value' => I18n::translate('language.' . ($language->isDefault() ? 'default' : 'secondary')),
],
[
'label' => I18n::translate('language.code'),
'value' => $language->code(),
],
[
'label' => I18n::translate('language.locale'),
'value' => $language->locale(LC_ALL)
],
[
'label' => I18n::translate('language.direction'),
'value' => I18n::translate('language.direction.' . $language->direction()),
],
],
'name' => $name,
'next' => $next,
'prev' => $prev,
'translations' => $strings,
'url' => $language->url(),
]
];
}
],
'languages' => [
'pattern' => 'languages',
'action' => function () {
$kirby = App::instance();
return [
'component' => 'k-languages-view',
'props' => [
'buttons' => fn () =>
ViewButtons::view('languages')
->defaults('create')
->render(),
'languages' => $kirby->languages()->values(fn ($language) => [
'deletable' => $language->isDeletable(),
'default' => $language->isDefault(),
'id' => $language->code(),
'info' => Escape::html($language->code()),
'text' => Escape::html($language->name()),
]),
'variables' => $kirby->option('languages.variables', true)
]
];
}
]
];

View file

@ -0,0 +1,44 @@
<?php
use Kirby\Panel\Panel;
use Kirby\Toolkit\I18n;
return function ($kirby) {
return [
'icon' => 'user',
'label' => I18n::translate('login'),
'views' => [
'login' => [
'pattern' => 'login',
'auth' => false,
'action' => function () use ($kirby) {
$system = $kirby->system();
$status = $kirby->auth()->status();
return [
'component' => 'k-login-view',
'props' => [
'methods' => array_keys($system->loginMethods()),
'pending' => [
'email' => $status->email(),
'challenge' => $status->challenge()
]
],
];
}
],
'login.fallback' => [
'pattern' => '(:all)',
'auth' => false,
'action' => function ($path) use ($kirby) {
/**
* Store the current path in the session
* Once the user is logged in, the path will
* be used to redirect to that view again
*/
$kirby->session()->set('panel.path', $path);
Panel::go(url: 'login', refresh: 0);
}
]
]
];
};

View file

@ -0,0 +1,21 @@
<?php
use Kirby\Panel\Panel;
use Kirby\Toolkit\I18n;
return function ($kirby) {
return [
'icon' => 'user',
'label' => I18n::translate('logout'),
'views' => [
'logout' => [
'pattern' => 'logout',
'auth' => false,
'action' => function () use ($kirby) {
$kirby->auth()->logout();
Panel::go('login');
},
]
]
];
};

View file

@ -0,0 +1,11 @@
<?php
use Kirby\Toolkit\I18n;
return function () {
return [
'icon' => 'search',
'label' => I18n::translate('search'),
'views' => require __DIR__ . '/search/views.php'
];
};

View file

@ -0,0 +1,17 @@
<?php
use Kirby\Cms\App;
return [
'search' => [
'pattern' => 'search',
'action' => function () {
return [
'component' => 'k-search-view',
'props' => [
'type' => App::instance()->request()->get('type'),
]
];
}
],
];

View file

@ -0,0 +1,23 @@
<?php
use Kirby\Toolkit\I18n;
return function ($kirby) {
$blueprint = $kirby->site()->blueprint();
return [
'breadcrumbLabel' => function () use ($kirby) {
return $kirby->site()->title()->or(I18n::translate('view.site'))->toString();
},
'icon' => $blueprint->icon() ?? 'home',
'label' => $blueprint->title() ?? I18n::translate('view.site'),
'menu' => true,
'buttons' => require __DIR__ . '/site/buttons.php',
'dialogs' => require __DIR__ . '/site/dialogs.php',
'drawers' => require __DIR__ . '/site/drawers.php',
'dropdowns' => require __DIR__ . '/site/dropdowns.php',
'requests' => require __DIR__ . '/site/requests.php',
'searches' => require __DIR__ . '/site/searches.php',
'views' => require __DIR__ . '/site/views.php',
];
};

View file

@ -0,0 +1,72 @@
<?php
use Kirby\Cms\ModelWithContent;
use Kirby\Cms\Page;
use Kirby\Cms\Site;
use Kirby\Panel\Ui\Buttons\LanguagesDropdown;
use Kirby\Panel\Ui\Buttons\OpenButton;
use Kirby\Panel\Ui\Buttons\PageStatusButton;
use Kirby\Panel\Ui\Buttons\PreviewButton;
use Kirby\Panel\Ui\Buttons\SettingsButton;
use Kirby\Panel\Ui\Buttons\VersionsButton;
return [
'site.open' => function (Site $site, string $versionId = 'latest') {
$versionId = $versionId === 'compare' ? 'changes' : $versionId;
$link = $site->previewUrl($versionId);
if ($link !== null) {
return new OpenButton(
link: $link,
);
}
},
'site.preview' => function (Site $site) {
if ($site->previewUrl() !== null) {
return new PreviewButton(
link: $site->panel()->url(true) . '/preview/changes',
);
}
},
'site.versions' => function (Site $site, string $versionId = 'latest') {
return new VersionsButton(
model: $site,
versionId: $versionId
);
},
'page.open' => function (Page $page, string $versionId = 'latest') {
$versionId = $versionId === 'compare' ? 'changes' : $versionId;
$link = $page->previewUrl($versionId);
if ($link !== null) {
return new OpenButton(
link: $link,
);
}
},
'page.preview' => function (Page $page) {
if ($page->previewUrl() !== null) {
return new PreviewButton(
link: $page->panel()->url(true) . '/preview/changes',
);
}
},
'page.versions' => function (Page $page, string $versionId = 'latest') {
return new VersionsButton(
model: $page,
versionId: $versionId
);
},
'page.settings' => fn (Page $page) => new SettingsButton(model: $page),
'page.status' => fn (Page $page) => new PageStatusButton($page),
// `languages` button needs to be in site area,
// as the languages might be not loaded even in
// multilang mode when the `languages` option is deactivated
// (but content languages to switch between still can exist)
'languages' => fn (ModelWithContent $model) =>
new LanguagesDropdown($model),
// file buttons
...require __DIR__ . '/../files/buttons.php'
];

View file

@ -0,0 +1,592 @@
<?php
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Cms\PageRules;
use Kirby\Cms\Url;
use Kirby\Exception\Exception;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\PermissionException;
use Kirby\Panel\ChangesDialog;
use Kirby\Panel\Field;
use Kirby\Panel\PageCreateDialog;
use Kirby\Panel\Panel;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\I18n;
use Kirby\Toolkit\Str;
use Kirby\Uuid\Uuids;
$fields = require __DIR__ . '/../fields/dialogs.php';
$files = require __DIR__ . '/../files/dialogs.php';
return [
'page.changeSort' => [
'pattern' => 'pages/(:any)/changeSort',
'load' => function (string $id) {
$page = Find::page($id);
if ($page->blueprint()->num() !== 'default') {
throw new PermissionException(
key: 'page.sort.permission',
data: ['slug' => $page->slug()]
);
}
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'position' => Field::pagePosition($page),
],
'submitButton' => I18n::translate('change'),
'value' => [
'position' => $page->panel()->position()
]
]
];
},
'submit' => function (string $id) {
$request = App::instance()->request();
Find::page($id)->changeStatus(
'listed',
$request->get('position')
);
return [
'event' => 'page.sort',
];
}
],
'page.changeStatus' => [
'pattern' => 'pages/(:any)/changeStatus',
'load' => function (string $id) {
$page = Find::page($id);
$blueprint = $page->blueprint();
$status = $page->status();
$states = [];
$position = null;
foreach ($blueprint->status() as $key => $state) {
$states[] = [
'value' => $key,
'text' => $state['label'],
'info' => $state['text'],
];
}
if ($status === 'draft') {
$errors = $page->errors();
// switch to the error dialog if there are
// errors and the draft cannot be published
if (count($errors) > 0) {
return [
'component' => 'k-error-dialog',
'props' => [
'message' => I18n::translate('error.page.changeStatus.incomplete'),
'details' => $errors,
]
];
}
}
$fields = [
'status' => [
'label' => I18n::translate('page.changeStatus.select'),
'type' => 'radio',
'required' => true,
'options' => $states
]
];
if ($blueprint->num() === 'default') {
$fields['position'] = Field::pagePosition($page, [
'when' => [
'status' => 'listed'
]
]);
$position = $page->panel()->position();
}
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => $fields,
'submitButton' => I18n::translate('change'),
'value' => [
'status' => $status,
'position' => $position
]
]
];
},
'submit' => function (string $id) {
$request = App::instance()->request();
Find::page($id)->changeStatus(
$request->get('status'),
$request->get('position')
);
return [
'event' => 'page.changeStatus',
];
}
],
'page.changeTemplate' => [
'pattern' => 'pages/(:any)/changeTemplate',
'load' => function (string $id) {
$page = Find::page($id);
$blueprints = $page->blueprints();
if (count($blueprints) <= 1) {
throw new Exception(
key: 'page.changeTemplate.invalid',
data: ['slug' => $id]
);
}
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'notice' => [
'type' => 'info',
'theme' => 'notice',
'text' => I18n::translate('page.changeTemplate.notice')
],
'template' => Field::template($blueprints, [
'required' => true
])
],
'theme' => 'notice',
'submitButton' => I18n::translate('change'),
'value' => [
'template' => $page->intendedTemplate()->name()
]
]
];
},
'submit' => function (string $id) {
$page = Find::page($id);
$template = App::instance()->request()->get('template');
$page->changeTemplate($template);
return [
'event' => 'page.changeTemplate',
];
}
],
'page.changeTitle' => [
'pattern' => 'pages/(:any)/changeTitle',
'load' => function (string $id) {
$kirby = App::instance();
$request = $kirby->request();
$page = Find::page($id);
$permissions = $page->permissions();
$select = $request->get('select', 'title');
// build the path prefix
$path = match ($kirby->multilang()) {
true => Str::after($kirby->site()->url(), $kirby->url()) . '/',
false => '/'
};
if ($parent = $page->parent()) {
$path .= $parent->uri() . '/';
}
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'title' => Field::title([
'required' => true,
'preselect' => $select === 'title',
'disabled' => $permissions->can('changeTitle') === false
]),
'slug' => Field::slug([
'required' => true,
'preselect' => $select === 'slug',
'path' => $path,
'disabled' => $permissions->can('changeSlug') === false,
'wizard' => [
'text' => I18n::translate('page.changeSlug.fromTitle'),
'field' => 'title'
]
])
],
'autofocus' => false,
'submitButton' => I18n::translate('change'),
'value' => [
'title' => $page->title()->value(),
'slug' => $page->slug(),
]
]
];
},
'submit' => function (string $id) {
$request = App::instance()->request();
$page = Find::page($id);
$title = trim($request->get('title', ''));
$slug = trim($request->get('slug', ''));
// basic input validation before we move on
PageRules::validateTitleLength($title);
PageRules::validateSlugLength($slug);
// nothing changed
if ($page->title()->value() === $title && $page->slug() === $slug) {
return true;
}
// prepare the response
$response = [
'event' => []
];
// the page title changed
if ($page->title()->value() !== $title) {
$page = $page->changeTitle($title);
$response['event'][] = 'page.changeTitle';
}
// the slug changed
if ($page->slug() !== $slug) {
$response['event'][] = 'page.changeSlug';
$newPage = $page->changeSlug($slug);
$oldUrl = $page->panel()->url(true);
$newUrl = $newPage->panel()->url(true);
// check for a necessary redirect after the slug has changed
if (Panel::referrer() === $oldUrl && $oldUrl !== $newUrl) {
$response['redirect'] = $newUrl;
}
}
return $response;
}
],
'page.create' => [
'pattern' => 'pages/create',
'load' => function () {
$request = App::instance()->request();
$dialog = new PageCreateDialog(
parentId: $request->get('parent'),
sectionId: $request->get('section'),
slug: $request->get('slug'),
template: $request->get('template'),
title: $request->get('title'),
uuid: $request->get('uuid'),
viewId: $request->get('view'),
);
return $dialog->load();
},
'submit' => function () {
$request = App::instance()->request();
$dialog = new PageCreateDialog(
parentId: $request->get('parent'),
sectionId: $request->get('section'),
slug: $request->get('slug'),
template: $request->get('template'),
title: $request->get('title'),
uuid: $request->get('uuid'),
viewId: $request->get('view'),
);
return $dialog->submit($request->get());
}
],
'page.delete' => [
'pattern' => 'pages/(:any)/delete',
'load' => function (string $id) {
$page = Find::page($id);
$text = I18n::template('page.delete.confirm', [
'title' => Escape::html($page->title()->value())
]);
if ($page->childrenAndDrafts()->count() > 0) {
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'info' => [
'type' => 'info',
'theme' => 'negative',
'text' => I18n::translate('page.delete.confirm.subpages')
],
'check' => [
'label' => I18n::translate('page.delete.confirm.title'),
'type' => 'text',
'counter' => false
]
],
'size' => 'medium',
'submitButton' => I18n::translate('delete'),
'text' => $text,
'theme' => 'negative',
]
];
}
return [
'component' => 'k-remove-dialog',
'props' => [
'text' => $text
]
];
},
'submit' => function (string $id) {
$request = App::instance()->request();
$page = Find::page($id);
$redirect = false;
$referrer = Panel::referrer();
$url = $page->panel()->url(true);
if (
$page->childrenAndDrafts()->count() > 0 &&
$request->get('check') !== $page->title()->value()
) {
throw new InvalidArgumentException(
key: 'page.delete.confirm'
);
}
$page->delete(true);
// redirect to the parent model URL
// if the dialog has been opened in the page view
if ($referrer === $url) {
$redirect = $page->parentModel()->panel()->url(true);
}
return [
'event' => 'page.delete',
'redirect' => $redirect
];
}
],
'page.duplicate' => [
'pattern' => 'pages/(:any)/duplicate',
'load' => function (string $id) {
$page = Find::page($id);
$hasChildren = $page->hasChildren();
$hasFiles = $page->hasFiles();
$toggleWidth = '1/' . count(array_filter([$hasChildren, $hasFiles]));
$fields = [
'title' => Field::title([
'required' => true
]),
'slug' => Field::slug([
'required' => true,
'path' => $page->parent() ? '/' . $page->parent()->id() . '/' : '/',
'wizard' => [
'text' => I18n::translate('page.changeSlug.fromTitle'),
'field' => 'title'
]
])
];
if ($hasFiles === true) {
$fields['files'] = [
'label' => I18n::translate('page.duplicate.files'),
'type' => 'toggle',
'width' => $toggleWidth
];
}
if ($hasChildren === true) {
$fields['children'] = [
'label' => I18n::translate('page.duplicate.pages'),
'type' => 'toggle',
'width' => $toggleWidth
];
}
$slugAppendix = Url::slug(I18n::translate('page.duplicate.appendix'));
$titleAppendix = I18n::translate('page.duplicate.appendix');
// if the item to be duplicated already exists
// add a suffix at the end of slug and title
$duplicateSlug = $page->slug() . '-' . $slugAppendix;
$siblingKeys = $page->parentModel()->childrenAndDrafts()->pluck('uid');
if (in_array($duplicateSlug, $siblingKeys, true) === true) {
$suffixCounter = 2;
$newSlug = $duplicateSlug . $suffixCounter;
while (in_array($newSlug, $siblingKeys, true) === true) {
$newSlug = $duplicateSlug . ++$suffixCounter;
}
$slugAppendix .= $suffixCounter;
$titleAppendix .= ' ' . $suffixCounter;
}
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => $fields,
'submitButton' => I18n::translate('duplicate'),
'value' => [
'children' => false,
'files' => false,
'slug' => $page->slug() . '-' . $slugAppendix,
'title' => $page->title() . ' ' . $titleAppendix
]
]
];
},
'submit' => function (string $id) {
$request = App::instance()->request();
$newPage = Find::page($id)->duplicate($request->get('slug'), [
'children' => (bool)$request->get('children'),
'files' => (bool)$request->get('files'),
'title' => (string)$request->get('title'),
]);
return [
'event' => 'page.duplicate',
'redirect' => $newPage->panel()->url(true)
];
}
],
'page.fields' => [
...$fields['model'],
'pattern' => '(pages/[^/]+)/fields/(:any)/(:all?)',
],
'page.file.changeName' => [
...$files['changeName'],
'pattern' => '(pages/[^/]+)/files/(:any)/changeName',
],
'page.file.changeSort' => [
...$files['changeSort'],
'pattern' => '(pages/[^/]+)/files/(:any)/changeSort',
],
'page.file.changeTemplate' => [
...$files['changeTemplate'],
'pattern' => '(pages/[^/]+)/files/(:any)/changeTemplate',
],
'page.file.delete' => [
...$files['delete'],
'pattern' => '(pages/[^/]+)/files/(:any)/delete',
],
'page.file.fields' => [
...$fields['file'],
'pattern' => '(pages/[^/]+)/files/(:any)/fields/(:any)/(:all?)',
],
'page.move' => [
'pattern' => 'pages/(:any)/move',
'load' => function (string $id) {
$page = Find::page($id);
$parent = $page->parentModel();
if (Uuids::enabled() === false) {
$parentId = $parent?->id() ?? '/';
} else {
$parentId = $parent?->uuid()->toString() ?? 'site://';
}
return [
'component' => 'k-page-move-dialog',
'props' => [
'value' => [
'move' => $page->panel()->url(true),
'parent' => $parentId
]
]
];
},
'submit' => function (string $id) {
$kirby = App::instance();
$parentId = $kirby->request()->get('parent');
$parent = (empty($parentId) === true || $parentId === '/' || $parentId === 'site://') ? $kirby->site() : Find::page($parentId);
$oldPage = Find::page($id);
$newPage = $oldPage->move($parent);
return [
'event' => 'page.move',
'redirect' => $newPage->panel()->url(true)
];
}
],
'site.changeTitle' => [
'pattern' => 'site/changeTitle',
'load' => function () {
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'title' => Field::title([
'required' => true,
'preselect' => true
])
],
'submitButton' => I18n::translate('rename'),
'value' => [
'title' => App::instance()->site()->title()->value()
]
]
];
},
'submit' => function () {
$kirby = App::instance();
$kirby->site()->changeTitle($kirby->request()->get('title'));
return [
'event' => 'site.changeTitle',
];
}
],
'site.fields' => [
...$fields['model'],
'pattern' => '(site)/fields/(:any)/(:all?)',
],
'site.file.changeName' => [
...$files['changeName'],
'pattern' => '(site)/files/(:any)/changeName',
],
'site.file.changeSort' => [
...$files['changeSort'],
'pattern' => '(site)/files/(:any)/changeSort',
],
'site.file.changeTemplate' => [
...$files['changeTemplate'],
'pattern' => '(site)/files/(:any)/changeTemplate',
],
'site.file.delete' => [
...$files['delete'],
'pattern' => '(site)/files/(:any)/delete',
],
'site.file.fields' => [
...$fields['file'],
'pattern' => '(site)/files/(:any)/fields/(:any)/(:all?)',
],
'changes' => [
'pattern' => 'changes',
'load' => function () {
return (new ChangesDialog())->load();
},
],
];

View file

@ -0,0 +1,22 @@
<?php
$fields = require __DIR__ . '/../fields/drawers.php';
return [
'page.fields' => [
...$fields['model'],
'pattern' => '(pages/[^/]+)/fields/(:any)/(:all?)',
],
'page.file.fields' => [
...$fields['file'],
'pattern' => '(pages/[^/]+)/files/(:any)/fields/(:any)/(:all?)',
],
'site.fields' => [
...$fields['model'],
'pattern' => '(site)/fields/(:any)/(:all?)',
],
'site.file.fields' => [
...$fields['file'],
'pattern' => '(site)/files/(:any)/fields/(:any)/(:all?)',
],
];

View file

@ -0,0 +1,46 @@
<?php
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Panel\Ui\Buttons\LanguagesDropdown;
$files = require __DIR__ . '/../files/dropdowns.php';
return [
'page' => [
'pattern' => 'pages/(:any)',
'options' => function (string $path) {
return Find::page($path)->panel()->dropdown();
}
],
'page.languages' => [
'pattern' => 'pages/(:any)/languages',
'options' => function (string $path) {
$page = Find::page($path);
return (new LanguagesDropdown($page))->options();
}
],
'page.file' => [
'pattern' => '(pages/[^/]+)/files/(:any)',
'options' => $files['file']
],
'page.file.languages' => [
'pattern' => '(pages/[^/]+)/files/(:any)/languages',
'options' => $files['language']
],
'site.languages' => [
'pattern' => 'site/languages',
'options' => function () {
$site = App::instance()->site();
return (new LanguagesDropdown($site))->options();
}
],
'site.file' => [
'pattern' => '(site)/files/(:any)',
'options' => $files['file']
],
'site.file.languages' => [
'pattern' => '(site)/files/(:any)/languages',
'options' => $files['language']
]
];

View file

@ -0,0 +1,25 @@
<?php
use Kirby\Cms\App;
use Kirby\Panel\Controller\PageTree;
return [
'tree' => [
'pattern' => 'site/tree',
'action' => function () {
return (new PageTree())->children(
parent: App::instance()->request()->get('parent'),
moving: App::instance()->request()->get('move')
);
}
],
'tree.parents' => [
'pattern' => 'site/tree/parents',
'action' => function () {
return (new PageTree())->parents(
page: App::instance()->request()->get('page'),
includeSite: App::instance()->request()->get('root') === 'true',
);
}
]
];

View file

@ -0,0 +1,17 @@
<?php
use Kirby\Panel\Controller\Search;
use Kirby\Toolkit\I18n;
return [
'pages' => [
'label' => I18n::translate('pages'),
'icon' => 'page',
'query' => fn (string|null $query, int $limit, int $page) => Search::pages($query, $limit, $page)
],
'files' => [
'label' => I18n::translate('files'),
'icon' => 'image',
'query' => fn (string|null $query, int $limit, int $page) => Search::files($query, $limit, $page)
]
];

View file

@ -0,0 +1,98 @@
<?php
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Exception\PermissionException;
use Kirby\Panel\Ui\Buttons\ViewButtons;
use Kirby\Toolkit\I18n;
return [
'page' => [
'pattern' => 'pages/(:any)',
'action' => fn (string $path) => Find::page($path)->panel()->view()
],
'page.file' => [
'pattern' => 'pages/(:any)/files/(:any)',
'action' => function (string $id, string $filename) {
return Find::file('pages/' . $id, $filename)->panel()->view();
}
],
'page.preview' => [
'pattern' => 'pages/(:any)/preview/(changes|latest|compare)',
'action' => function (string $path, string $versionId) {
$page = Find::page($path);
$view = $page->panel()->view();
$src = [
'latest' => $page->previewUrl('latest'),
'changes' => $page->previewUrl('changes'),
];
if ($src['latest'] === null) {
throw new PermissionException('The preview is not available');
}
return [
'component' => 'k-preview-view',
'props' => [
...$view['props'],
'back' => $view['props']['link'],
'buttons' => fn () =>
ViewButtons::view('page.preview', model: $page)
->defaults(
'page.versions',
'languages',
)
->bind(['versionId' => $versionId])
->render(),
'src' => $src,
'versionId' => $versionId,
],
'title' => $view['props']['title'] . ' | ' . I18n::translate('preview'),
];
}
],
'site' => [
'pattern' => 'site',
'action' => fn () => App::instance()->site()->panel()->view()
],
'site.file' => [
'pattern' => 'site/files/(:any)',
'action' => function (string $filename) {
return Find::file('site', $filename)->panel()->view();
}
],
'site.preview' => [
'pattern' => 'site/preview/(changes|latest|compare)',
'action' => function (string $versionId) {
$site = App::instance()->site();
$view = $site->panel()->view();
$src = [
'latest' => $site->previewUrl('latest'),
'changes' => $site->previewUrl('changes'),
];
if ($src['latest'] === null) {
throw new PermissionException('The preview is not available');
}
return [
'component' => 'k-preview-view',
'props' => [
...$view['props'],
'back' => $view['props']['link'],
'buttons' => fn () =>
ViewButtons::view('site.preview', model: $site)
->defaults(
'site.versions',
'languages'
)
->bind(['versionId' => $versionId])
->render(),
'src' => $src,
'versionId' => $versionId
],
'title' => I18n::translate('view.site') . ' | ' . I18n::translate('preview'),
];
}
],
];

View file

@ -0,0 +1,13 @@
<?php
use Kirby\Toolkit\I18n;
return function ($kirby) {
return [
'icon' => 'settings',
'label' => I18n::translate('view.system'),
'menu' => true,
'dialogs' => require __DIR__ . '/system/dialogs.php',
'views' => require __DIR__ . '/system/views.php'
];
};

View file

@ -0,0 +1,135 @@
<?php
use Kirby\Cms\App;
use Kirby\Exception\LogicException;
use Kirby\Panel\Field;
use Kirby\Toolkit\I18n;
return [
// license key
'license' => [
'load' => function () {
$kirby = App::instance();
$license = $kirby->system()->license();
$obfuscated = $kirby->user()->isAdmin() === false;
$status = $license->status();
$renewable = $status->renewable();
return [
'component' => 'k-license-dialog',
'props' => [
'license' => [
'code' => $license->code($obfuscated),
'icon' => $status->icon(),
'info' => $status->info($license->renewal('Y-m-d', 'date')),
'theme' => $status->theme(),
'type' => $license->label(),
],
'cancelButton' => $renewable,
'submitButton' => $renewable ? [
'icon' => 'refresh',
'text' => I18n::translate('renew'),
'theme' => 'love',
] : false,
]
];
},
'submit' => function () {
// @codeCoverageIgnoreStart
$response = App::instance()->system()->license()->upgrade();
// the upgrade is still needed
if ($response['status'] === 'upgrade') {
return [
'redirect' => $response['url']
];
}
// the upgrade has already been completed
if ($response['status'] === 'complete') {
return [
'event' => 'system.renew',
'message' => I18n::translate('license.success')
];
}
throw new LogicException(message: 'The upgrade failed');
// @codeCoverageIgnoreEnd
}
],
'license/remove' => [
'load' => function () {
return [
'component' => 'k-remove-dialog',
'props' => [
'text' => I18n::translate('license.remove.text'),
'size' => 'medium',
'submitButton' => [
'icon' => 'trash',
'text' => I18n::translate('remove'),
'theme' => 'negative',
],
]
];
},
'submit' => function () {
// @codeCoverageIgnoreStart
App::instance()->system()->license()->delete();
return true;
// @codeCoverageIgnoreEnd
}
],
// license registration
'registration' => [
'load' => function () {
$system = App::instance()->system();
$local = $system->isLocal();
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'domain' => [
'label' => I18n::translate('license.activate.label'),
'type' => 'info',
'theme' => $local ? 'warning' : 'info',
'text' => I18n::template('license.activate.' . ($local ? 'local' : 'domain'), ['host' => $system->indexUrl()])
],
'license' => [
'label' => I18n::translate('license.code.label'),
'type' => 'text',
'required' => true,
'counter' => false,
'placeholder' => 'K-',
'help' => I18n::translate('license.code.help') . ' ' . '<a href="https://getkirby.com/buy" target="_blank">' . I18n::translate('license.buy') . ' &rarr;</a>'
],
'email' => Field::email(['required' => true])
],
'submitButton' => [
'icon' => 'key',
'text' => I18n::translate('activate'),
'theme' => 'love',
],
'value' => [
'license' => null,
'email' => null
]
]
];
},
'submit' => function () {
// @codeCoverageIgnoreStart
$kirby = App::instance();
$kirby->system()->register(
$kirby->request()->get('license'),
$kirby->request()->get('email')
);
return [
'event' => 'system.register',
'message' => I18n::translate('license.success')
];
// @codeCoverageIgnoreEnd
}
],
];

View file

@ -0,0 +1,139 @@
<?php
use Kirby\Cms\App;
use Kirby\Panel\Ui\Buttons\ViewButtons;
use Kirby\Toolkit\I18n;
return [
'system' => [
'pattern' => 'system',
'action' => function () {
$kirby = App::instance();
$system = $kirby->system();
$updateStatus = $system->updateStatus();
$license = $system->license();
$debugMode = $kirby->option('debug', false) === true;
$isLocal = $system->isLocal();
$environment = [
[
'label' => $license->status()->label(),
'value' => $license->label(),
'theme' => $license->status()->theme(),
'icon' => $license->status()->icon(),
'dialog' => $license->status()->dialog()
],
[
'label' => $updateStatus?->label() ?? I18n::translate('version'),
'value' => $kirby->version(),
'link' => $updateStatus?->url() ??
'https://github.com/getkirby/kirby/releases/tag/' . $kirby->version(),
'theme' => $updateStatus?->theme(),
'icon' => $updateStatus?->icon() ?? 'info'
],
[
'label' => 'PHP',
'value' => phpversion(),
'icon' => 'code'
],
[
'label' => I18n::translate('server'),
'value' => $system->serverSoftwareShort() ?? '?',
'icon' => 'server'
]
];
$exceptions = $updateStatus?->exceptionMessages() ?? [];
$plugins = $system->plugins()->values(function ($plugin) use (&$exceptions) {
$authors = $plugin->authorsNames();
$updateStatus = $plugin->updateStatus();
$version = $updateStatus?->toArray();
$version ??= $plugin->version() ?? '';
if ($updateStatus !== null) {
$exceptions = [
...$exceptions,
...$updateStatus->exceptionMessages()
];
}
return [
'author' => empty($authors) ? '' : $authors,
'license' => $plugin->license()->toArray(),
'name' => [
'text' => $plugin->name() ?? '',
'href' => $plugin->link(),
],
'status' => $plugin->license()->status()->toArray(),
'version' => $version,
];
});
$security = $updateStatus?->messages() ?? [];
if ($isLocal === true) {
$security[] = [
'id' => 'local',
'icon' => 'info',
'theme' => 'info',
'text' => I18n::translate('system.issues.local')
];
}
if ($debugMode === true) {
$security[] = [
'id' => 'debug',
'icon' => $isLocal ? 'info' : 'alert',
'theme' => $isLocal ? 'info' : 'negative',
'text' => I18n::translate('system.issues.debug'),
'link' => 'https://getkirby.com/security/debug'
];
}
if (
$isLocal === false &&
$kirby->environment()->https() !== true
) {
$security[] = [
'id' => 'https',
'text' => I18n::translate('system.issues.https'),
'link' => 'https://getkirby.com/security/https'
];
}
if ($kirby->option('panel.vue.compiler', null) === null) {
$security[] = [
'id' => 'vue-compiler',
'link' => 'https://getkirby.com/security/vue-compiler',
'text' => I18n::translate('system.issues.vue.compiler'),
'theme' => 'notice'
];
}
// sensitive URLs
if ($isLocal === false) {
$sensitive = [
'content' => $system->exposedFileUrl('content'),
'git' => $system->exposedFileUrl('git'),
'kirby' => $system->exposedFileUrl('kirby'),
'site' => $system->exposedFileUrl('site')
];
}
return [
'component' => 'k-system-view',
'props' => [
'buttons' => fn () =>
ViewButtons::view('system')->render(),
'environment' => $environment,
'exceptions' => $debugMode ? $exceptions : [],
'info' => $system->info(),
'plugins' => $plugins,
'security' => $security,
'urls' => $sensitive ?? []
]
];
}
],
];

View file

@ -0,0 +1,18 @@
<?php
use Kirby\Toolkit\I18n;
return function ($kirby) {
return [
'icon' => 'users',
'label' => I18n::translate('view.users'),
'search' => 'users',
'menu' => true,
'buttons' => require __DIR__ . '/users/buttons.php',
'dialogs' => require __DIR__ . '/users/dialogs.php',
'drawers' => require __DIR__ . '/users/drawers.php',
'dropdowns' => require __DIR__ . '/users/dropdowns.php',
'searches' => require __DIR__ . '/users/searches.php',
'views' => require __DIR__ . '/users/views.php'
];
};

View file

@ -0,0 +1,20 @@
<?php
use Kirby\Cms\User;
use Kirby\Panel\Ui\Buttons\SettingsButton;
use Kirby\Panel\Ui\Buttons\ViewButton;
use Kirby\Toolkit\I18n;
return [
'users.create' => function (User $user, string|null $role = null) {
return new ViewButton(
dialog: 'users/create?role=' . $role,
disabled: $user->kirby()->roles()->canBeCreated()->count() < 1,
icon: 'add',
text: I18n::translate('user.create'),
);
},
'user.settings' => function (User $user) {
return new SettingsButton(model: $user);
}
];

View file

@ -0,0 +1,360 @@
<?php
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Cms\UserRules;
use Kirby\Exception\Exception;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Panel\Field;
use Kirby\Panel\Panel;
use Kirby\Panel\UserTotpDisableDialog;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\I18n;
$fields = require __DIR__ . '/../fields/dialogs.php';
$files = require __DIR__ . '/../files/dialogs.php';
return [
'user.create' => [
'pattern' => 'users/create',
'load' => function () {
$kirby = App::instance();
$roles = $kirby->roles()->canBeCreated();
// get default value for role
if ($role = $kirby->request()->get('role')) {
$role = $roles->find($role)?->id();
}
// get role field definition, incl. available role options
$roles = Field::role(
roles: $roles,
props: ['required' => true]
);
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'name' => Field::username(),
'email' => Field::email([
'link' => false,
'required' => true
]),
'password' => Field::password([
'autocomplete' => 'new-password'
]),
'translation' => Field::translation([
'required' => true
]),
'role' => $roles
],
'submitButton' => I18n::translate('create'),
'value' => [
'name' => '',
'email' => '',
'password' => '',
'translation' => $kirby->panelLanguage(),
'role' => $role ?: $roles['options'][0]['value'] ?? null
]
]
];
},
'submit' => function () {
$kirby = App::instance();
$kirby->users()->create([
'name' => $kirby->request()->get('name'),
'email' => $kirby->request()->get('email'),
'password' => $kirby->request()->get('password'),
'language' => $kirby->request()->get('translation'),
'role' => $kirby->request()->get('role')
]);
return [
'event' => 'user.create'
];
}
],
'user.changeEmail' => [
'pattern' => 'users/(:any)/changeEmail',
'load' => function (string $id) {
$user = Find::user($id);
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'email' => [
'label' => I18n::translate('email'),
'required' => true,
'type' => 'email',
'preselect' => true
]
],
'submitButton' => I18n::translate('change'),
'value' => [
'email' => $user->email()
]
]
];
},
'submit' => function (string $id) {
$request = App::instance()->request();
Find::user($id)->changeEmail($request->get('email'));
return [
'event' => 'user.changeEmail'
];
}
],
'user.changeLanguage' => [
'pattern' => 'users/(:any)/changeLanguage',
'load' => function (string $id) {
$user = Find::user($id);
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'translation' => Field::translation(['required' => true])
],
'submitButton' => I18n::translate('change'),
'value' => [
'translation' => $user->language()
]
]
];
},
'submit' => function (string $id) {
$request = App::instance()->request();
Find::user($id)->changeLanguage($request->get('translation'));
return [
'event' => 'user.changeLanguage',
'reload' => [
'globals' => '$translation'
]
];
}
],
'user.changeName' => [
'pattern' => 'users/(:any)/changeName',
'load' => function (string $id) {
$user = Find::user($id);
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'name' => Field::username([
'preselect' => true
])
],
'submitButton' => I18n::translate('rename'),
'value' => [
'name' => $user->name()->value()
]
]
];
},
'submit' => function (string $id) {
$request = App::instance()->request();
Find::user($id)->changeName($request->get('name'));
return [
'event' => 'user.changeName'
];
}
],
'user.changePassword' => [
'pattern' => 'users/(:any)/changePassword',
'load' => function (string $id) {
$kirby = App::instance();
$user = Find::user($id);
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'currentPassword' => Field::password([
'label' => I18n::translate('user.changePassword.' . ($kirby->user()->is($user) ? 'current' : 'own')),
'autocomplete' => 'current-password',
'help' => I18n::translate('account') . ': ' . App::instance()->user()->email(),
]),
'line' => [
'type' => 'line',
],
'password' => Field::password([
'label' => I18n::translate('user.changePassword.new'),
'autocomplete' => 'new-password',
'help' => I18n::translate('account') . ': ' . $user->email(),
]),
'passwordConfirmation' => Field::password([
'label' => I18n::translate('user.changePassword.new.confirm'),
'autocomplete' => 'new-password'
])
],
'submitButton' => I18n::translate('change'),
]
];
},
'submit' => function (string $id) {
$kirby = App::instance();
$request = $kirby->request();
$user = Find::user($id);
$currentPassword = $request->get('currentPassword');
$password = $request->get('password');
$passwordConfirmation = $request->get('passwordConfirmation');
// validate the current password of the acting user
try {
$kirby->user()->validatePassword($currentPassword);
} catch (Exception) {
// catching and re-throwing exception to avoid automatic
// sign-out of current user from the Panel
throw new InvalidArgumentException([
'key' => 'user.password.wrong'
]);
}
// validate the new password
UserRules::validPassword($user, $password ?? '');
// compare passwords
if ($password !== $passwordConfirmation) {
throw new InvalidArgumentException(
key: 'user.password.notSame'
);
}
// change password if everything's fine
$user->changePassword($password);
return [
'event' => 'user.changePassword'
];
}
],
'user.changeRole' => [
'pattern' => 'users/(:any)/changeRole',
'load' => function (string $id) {
$user = Find::user($id);
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'role' => Field::role(
roles: $user->roles(),
props: [
'label' => I18n::translate('user.changeRole.select'),
'required' => true,
]
)
],
'submitButton' => I18n::translate('user.changeRole'),
'value' => [
'role' => $user->role()->name()
]
]
];
},
'submit' => function (string $id) {
$request = App::instance()->request();
$user = Find::user($id)->changeRole($request->get('role'));
return [
'event' => 'user.changeRole',
'user' => $user->toArray()
];
}
],
'user.delete' => [
'pattern' => 'users/(:any)/delete',
'load' => function (string $id) {
$user = Find::user($id);
$i18nPrefix = $user->isLoggedIn() ? 'account' : 'user';
return [
'component' => 'k-remove-dialog',
'props' => [
'text' => I18n::template($i18nPrefix . '.delete.confirm', [
'email' => Escape::html($user->email())
])
]
];
},
'submit' => function (string $id) {
$user = Find::user($id);
$redirect = false;
$referrer = Panel::referrer();
$url = $user->panel()->url(true);
$user->delete();
// redirect to the users view
// if the dialog has been opened in the user view
if ($referrer === $url) {
$redirect = '/users';
}
// logout the user if they deleted themselves
if ($user->isLoggedIn()) {
$redirect = '/logout';
}
return [
'event' => 'user.delete',
'redirect' => $redirect
];
}
],
'user.fields' => [
...$fields['model'],
'pattern' => '(users/[^/]+)/fields/(:any)/(:all?)',
],
'user.file.changeName' => [
...$files['changeName'],
'pattern' => '(users/[^/]+)/files/(:any)/changeName',
],
'user.file.changeSort' => [
...$files['changeSort'],
'pattern' => '(users/[^/]+)/files/(:any)/changeSort',
],
'user.file.changeTemplate' => [
...$files['changeTemplate'],
'pattern' => '(users/[^/]+)/files/(:any)/changeTemplate',
],
'user.file.delete' => [
...$files['delete'],
'pattern' => '(users/[^/]+)/files/(:any)/delete',
],
'user.file.fields' => [
...$fields['file'],
'pattern' => '(users/[^/]+)/files/(:any)/fields/(:any)/(:all?)',
],
'user.totp.disable' => [
'pattern' => 'users/(:any)/totp/disable',
'load' => fn (string $id) => (new UserTotpDisableDialog($id))->load(),
'submit' => fn (string $id) => (new UserTotpDisableDialog($id))->submit()
],
];

View file

@ -0,0 +1,14 @@
<?php
$fields = require __DIR__ . '/../fields/drawers.php';
return [
'user.fields' => [
...$fields['model'],
'pattern' => '(users/[^/]+)/fields/(:any)/(:all?)',
],
'user.file.fields' => [
...$fields['file'],
'pattern' => '(users/[^/]+)/files/(:any)/fields/(:any)/(:all?)',
],
];

View file

@ -0,0 +1,29 @@
<?php
use Kirby\Cms\Find;
use Kirby\Panel\Ui\Buttons\LanguagesDropdown;
$files = require __DIR__ . '/../files/dropdowns.php';
return [
'user' => [
'pattern' => 'users/(:any)',
'options' => fn (string $id) =>
Find::user($id)->panel()->dropdown()
],
'user.languages' => [
'pattern' => 'users/(:any)/languages',
'options' => function (string $id) {
$user = Find::user($id);
return (new LanguagesDropdown($user))->options();
}
],
'user.file' => [
'pattern' => '(users/[^/]+)/files/(:any)',
'options' => $files['file']
],
'user.file.languages' => [
'pattern' => '(users/[^/]+)/files/(:any)/languages',
'options' => $files['language']
]
];

View file

@ -0,0 +1,12 @@
<?php
use Kirby\Panel\Controller\Search;
use Kirby\Toolkit\I18n;
return [
'users' => [
'label' => I18n::translate('users'),
'icon' => 'users',
'query' => fn (string|null $query, int $limit, int $page) => Search::users($query, $limit, $page)
]
];

View file

@ -0,0 +1,65 @@
<?php
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Panel\Collector\UsersCollector;
use Kirby\Panel\Ui\Buttons\ViewButtons;
use Kirby\Panel\Ui\Item\UserItem;
return [
'users' => [
'pattern' => 'users',
'action' => function () {
$kirby = App::instance();
$role = $kirby->request()->get('role');
$roles = $kirby->roles()->toArray(fn ($role) => [
'id' => $role->id(),
'title' => $role->title(),
]);
return [
'component' => 'k-users-view',
'props' => [
'buttons' => fn () =>
ViewButtons::view('users')
->defaults('create')
->bind(['role' => $role])
->render(),
'role' => function () use ($roles, $role) {
if ($role) {
return $roles[$role] ?? null;
}
},
'roles' => array_values($roles),
'users' => function () use ($kirby, $role) {
$collector = new UsersCollector(
limit: 20,
page: $kirby->request()->get('page', 1),
role: $role,
sortBy: 'username asc',
);
$users = $collector->models(paginated: true);
return [
'data' => $users->values(fn ($user) => (new UserItem(user: $user))->props()),
'pagination' => $users->pagination()->toArray()
];
},
]
];
}
],
'user' => [
'pattern' => 'users/(:any)',
'action' => function (string $id) {
return Find::user($id)->panel()->view();
}
],
'user.file' => [
'pattern' => 'users/(:any)/files/(:any)',
'action' => function (string $id, string $filename) {
return Find::file('users/' . $id, $filename)->panel()->view();
}
],
];

View file

@ -0,0 +1,2 @@
<?php /** @var \Kirby\Cms\Block $block */ ?>
<pre><code class="language-<?= $block->language()->or('text') ?>"><?= $block->code()->html(false) ?></code></pre>

View file

@ -0,0 +1,59 @@
name: field.blocks.code.name
icon: code
wysiwyg: true
preview: code
fields:
code:
label: field.blocks.code.name
type: textarea
placeholder: field.blocks.code.placeholder
buttons: false
font: monospace
language:
label: field.blocks.code.language
type: select
default: text
options:
bash: Bash
basic: BASIC
c: C
clojure: Clojure
cpp: C++
csharp: C#
css: CSS
diff: Diff
elixir: Elixir
elm: Elm
erlang: Erlang
go: Go
graphql: GraphQL
haskell: Haskell
html: HTML
java: Java
js: JavaScript
json: JSON
latext: LaTeX
less: Less
lisp: Lisp
lua: Lua
makefile: Makefile
markdown: Markdown
markup: Markup
objectivec: Objective-C
pascal: Pascal
perl: Perl
php: PHP
text: Plain Text
python: Python
r: R
ruby: Ruby
rust: Rust
sass: Sass
scss: SCSS
shell: Shell
sql: SQL
swift: Swift
typescript: TypeScript
vbnet: VB.net
xml: XML
yaml: YAML

View file

@ -0,0 +1,20 @@
<?php
/** @var \Kirby\Cms\Block $block */
$caption = $block->caption();
$crop = $block->crop()->isTrue();
$ratio = $block->ratio()->or('auto');
?>
<figure<?= Html::attr(['data-ratio' => $ratio, 'data-crop' => $crop], null, ' ') ?>>
<ul>
<?php foreach ($block->images()->toFiles() as $image): ?>
<li>
<?= $image ?>
</li>
<?php endforeach ?>
</ul>
<?php if ($caption->isNotEmpty()): ?>
<figcaption>
<?= $caption ?>
</figcaption>
<?php endif ?>
</figure>

View file

@ -0,0 +1,40 @@
name: field.blocks.gallery.name
icon: dashboard
preview: gallery
fields:
images:
label: field.blocks.gallery.images.label
type: files
query: model.images
multiple: true
layout: cards
size: small
empty: field.blocks.gallery.images.empty
uploads:
template: blocks/image
image:
ratio: 1/1
caption:
label: field.blocks.image.caption
type: writer
icon: text
inline: true
ratio:
label: field.blocks.image.ratio
type: select
placeholder: Auto
width: 1/2
options:
1/1: "1:1"
16/9: "16:9"
10/8: "10:8"
21/9: "21:9"
7/5: "7:5"
4/3: "4:3"
5/3: "5:3"
3/2: "3:2"
3/1: "3:1"
crop:
label: field.blocks.image.crop
type: toggle
width: 1/2

View file

@ -0,0 +1,2 @@
<?php /** @var \Kirby\Cms\Block $block */ ?>
<<?= $level = $block->level()->or('h2') ?>><?= $block->text() ?></<?= $level ?>>

View file

@ -0,0 +1,35 @@
name: field.blocks.heading.name
icon: title
wysiwyg: true
preview: heading
fields:
level:
label: field.blocks.heading.level
type: toggles
empty: false
default: "h2"
labels: false
options:
- value: h1
icon: h1
text: H1
- value: h2
icon: h2
text: H2
- value: h3
icon: h3
text: H3
- value: h4
icon: h4
text: H4
- value: h5
icon: h5
text: H5
- value: h6
icon: h6
text: H6
text:
label: field.blocks.heading.text
type: writer
inline: true
placeholder: field.blocks.heading.placeholder

View file

@ -0,0 +1,35 @@
<?php
/** @var \Kirby\Cms\Block $block */
$alt = $block->alt();
$caption = $block->caption();
$crop = $block->crop()->isTrue();
$link = $block->link();
$ratio = $block->ratio()->or('auto');
$src = null;
if ($block->location() == 'web') {
$src = $block->src()->esc();
} elseif ($image = $block->image()->toFile()) {
$alt = $alt->or($image->alt());
$src = $image->url();
}
?>
<?php if ($src): ?>
<figure<?= Html::attr(['data-ratio' => $ratio, 'data-crop' => $crop], null, ' ') ?>>
<?php if ($link->isNotEmpty()): ?>
<a href="<?= Str::esc($link->toUrl()) ?>">
<img src="<?= $src ?>" alt="<?= $alt->esc() ?>">
</a>
<?php else: ?>
<img src="<?= $src ?>" alt="<?= $alt->esc() ?>">
<?php endif ?>
<?php if ($caption->isNotEmpty()): ?>
<figcaption>
<?= $caption ?>
</figcaption>
<?php endif ?>
</figure>
<?php endif ?>

View file

@ -0,0 +1,61 @@
name: field.blocks.image.name
icon: image
preview: image
fields:
location:
label: field.blocks.image.location
type: radio
columns: 2
default: "kirby"
required: true
options:
kirby: "{{ t('field.blocks.image.location.internal') }}"
web: "{{ t('field.blocks.image.location.external') }}"
image:
label: field.blocks.image.name
type: files
query: model.images
multiple: false
image:
back: black
uploads:
template: blocks/image
when:
location: kirby
src:
label: field.blocks.image.url
type: url
when:
location: web
alt:
label: field.blocks.image.alt
type: text
icon: title
caption:
label: field.blocks.image.caption
type: writer
icon: text
inline: true
link:
label: field.blocks.image.link
type: text
icon: url
ratio:
label: field.blocks.image.ratio
type: select
placeholder: Auto
width: 1/2
options:
1/1: "1:1"
16/9: "16:9"
10/8: "10:8"
21/9: "21:9"
7/5: "7:5"
4/3: "4:3"
5/3: "5:3"
3/2: "3:2"
3/1: "3:1"
crop:
label: field.blocks.image.crop
type: toggle
width: 1/2

View file

@ -0,0 +1 @@
<hr />

View file

@ -0,0 +1,4 @@
name: field.blocks.line.name
icon: divider
preview: line
wysiwyg: true

View file

@ -0,0 +1,2 @@
<?php /** @var \Kirby\Cms\Block $block */ ?>
<?= $block->text();

View file

@ -0,0 +1,8 @@
name: field.blocks.list.name
icon: list-bullet
wysiwyg: true
preview: list
fields:
text:
label: field.blocks.list.name
type: list

View file

@ -0,0 +1,2 @@
<?php /** @var \Kirby\Cms\Block $block */ ?>
<?= $block->text()->kt();

View file

@ -0,0 +1,11 @@
name: field.blocks.markdown.name
icon: markdown
preview: markdown
wysiwyg: true
fields:
text:
label: field.blocks.markdown.label
placeholder: field.blocks.markdown.placeholder
type: textarea
buttons: false
font: monospace

View file

@ -0,0 +1,9 @@
<?php /** @var \Kirby\Cms\Block $block */ ?>
<blockquote>
<?= $block->text() ?>
<?php if ($block->citation()->isNotEmpty()): ?>
<footer>
<?= $block->citation() ?>
</footer>
<?php endif ?>
</blockquote>

View file

@ -0,0 +1,17 @@
name: field.blocks.quote.name
icon: quote
wysiwyg: true
preview: quote
fields:
text:
label: field.blocks.quote.text.label
placeholder: field.blocks.quote.text.placeholder
type: writer
inline: true
icon: quote
citation:
label: field.blocks.quote.citation.label
placeholder: field.blocks.quote.citation.placeholder
type: writer
inline: true
icon: user

View file

@ -0,0 +1,3 @@
name: Table
icon: menu
preview: table

View file

@ -0,0 +1,2 @@
<?php /** @var \Kirby\Cms\Block $block */ ?>
<?= $block->text();

View file

@ -0,0 +1,9 @@
name: field.blocks.text.name
icon: text
wysiwyg: true
preview: text
fields:
text:
type: writer
nodes: false
placeholder: field.blocks.text.placeholder

View file

@ -0,0 +1,32 @@
<?php
use Kirby\Cms\Html;
/** @var \Kirby\Cms\Block $block */
$caption = $block->caption();
if (
$block->location() == 'kirby' &&
$video = $block->video()->toFile()
) {
$url = $video->url();
$attrs = array_filter([
'autoplay' => $block->autoplay()->toBool(),
'controls' => $block->controls()->toBool(),
'loop' => $block->loop()->toBool(),
'muted' => $block->muted()->toBool() || $block->autoplay()->toBool(),
'playsinline' => $block->autoplay()->toBool(),
'poster' => $block->poster()->toFile()?->url(),
'preload' => $block->preload()->value(),
]);
} else {
$url = $block->url();
}
?>
<?php if ($video = Html::video($url, [], $attrs ?? [])): ?>
<figure>
<?= $video ?>
<?php if ($caption->isNotEmpty()): ?>
<figcaption><?= $caption ?></figcaption>
<?php endif ?>
</figure>
<?php endif ?>

View file

@ -0,0 +1,78 @@
name: field.blocks.video.name
icon: video
preview: video
fields:
location:
label: field.blocks.video.location
type: radio
columns: 2
default: "web"
options:
kirby: "{{ t('field.blocks.image.location.internal') }}"
web: "{{ t('field.blocks.image.location.external') }}"
url:
label: field.blocks.video.url.label
type: url
placeholder: field.blocks.video.url.placeholder
when:
location: web
video:
label: field.blocks.video.name
type: files
query: model.videos
multiple: false
# you might want to add a template for videos here
when:
location: kirby
poster:
label: field.blocks.video.poster
type: files
query: model.images
multiple: false
image:
back: black
uploads:
template: blocks/image
when:
location: kirby
caption:
label: field.blocks.video.caption
type: writer
inline: true
autoplay:
label: field.blocks.video.autoplay
type: toggle
width: 1/3
when:
location: kirby
muted:
label: field.blocks.video.muted
type: toggle
width: 1/3
default: true
when:
location: kirby
loop:
label: field.blocks.video.loop
type: toggle
width: 1/3
when:
location: kirby
controls:
label: field.blocks.video.controls
type: toggle
width: 1/3
default: true
when:
location: kirby
preload:
label: field.blocks.video.preload
type: select
width: 2/3
default: auto
options:
- auto
- metadata
- none
when:
location: kirby

View file

@ -0,0 +1,446 @@
<?php
use Kirby\Cms\App;
use Kirby\Cms\Collection;
use Kirby\Cms\File;
use Kirby\Cms\FileVersion;
use Kirby\Cms\ModelWithContent;
use Kirby\Cms\Page;
use Kirby\Cms\User;
use Kirby\Content\PlainTextStorage;
use Kirby\Content\Storage;
use Kirby\Data\Data;
use Kirby\Email\PHPMailer as Emailer;
use Kirby\Exception\NotFoundException;
use Kirby\Filesystem\Asset;
use Kirby\Filesystem\F;
use Kirby\Filesystem\Filename;
use Kirby\Http\Uri;
use Kirby\Http\Url;
use Kirby\Image\Darkroom;
use Kirby\Session\SessionStore;
use Kirby\Template\Snippet;
use Kirby\Template\Template;
use Kirby\Text\Markdown;
use Kirby\Text\SmartyPants;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Str;
use Kirby\Uuid\Uuid;
return [
/**
* Used by the `css()` helper
*
* @param string $url Relative or absolute URL
* @param string|array $options An array of attributes for the link tag or a media attribute string
*/
'css' => fn (App $kirby, string $url, $options = null): string => $url,
/**
* Add your own email provider
*/
'email' => function (
App $kirby,
array $props = [],
bool $debug = false
) {
return new Emailer($props, $debug);
},
/**
* Modify URLs for file objects
*
* @param \Kirby\Cms\File $file The original file object
*/
'file::url' => function (
App $kirby,
File $file
): string {
return $file->mediaUrl();
},
/**
* Adapt file characteristics
*
* @param array $options All thumb options (width, height, crop, blur, grayscale)
*/
'file::version' => function (
App $kirby,
File|Asset $file,
array $options = []
): File|Asset|FileVersion {
// if file is not resizable, return
if ($file->isResizable() === false) {
return $file;
}
// create url and root
$mediaRoot = $file->mediaDir();
$template = $mediaRoot . '/{{ name }}{{ attributes }}.{{ extension }}';
$thumbRoot = (new Filename($file->root(), $template, $options))->toString();
$thumbName = basename($thumbRoot);
// check if the thumb already exists
if (file_exists($thumbRoot) === false) {
// if not, create job file
$job = $mediaRoot . '/.jobs/' . $thumbName . '.json';
try {
Data::write(
$job,
[...$options, 'filename' => $file->filename()]
);
} catch (Throwable) {
// if thumb doesn't exist yet and job file cannot
// be created, return
return $file;
}
}
return new FileVersion([
'modifications' => $options,
'original' => $file,
'root' => $thumbRoot,
'url' => $file->mediaUrl($thumbName),
]);
},
/**
* Used by the `js()` helper
*
* @param string $url Relative or absolute URL
* @param string|array $options An array of attributes for the link tag or a media attribute string
*/
'js' => fn (App $kirby, string $url, $options = null): string => $url,
/**
* Add your own Markdown parser
*
* @param string $text Text to parse
* @param array $options Markdown options
*/
'markdown' => function (
App $kirby,
string|null $text = null,
array $options = []
): string {
static $markdown;
static $config;
// if the config options have changed or the component is called for the first time,
// (re-)initialize the parser object
if ($config !== $options) {
$markdown = new Markdown($options);
$config = $options;
}
return $markdown->parse($text, $options['inline'] ?? false);
},
/**
* Add your own search engine
*
* @param \Kirby\Cms\Collection $collection Collection of searchable models
*/
'search' => function (
App $kirby,
Collection $collection,
string|null $query = null,
string|array $params = []
): Collection {
if (is_string($params) === true) {
$params = ['fields' => Str::split($params, '|')];
}
$collection = clone $collection;
$query = trim($query ?? '');
$options = [
'fields' => [],
'minlength' => 2,
'score' => [],
'words' => false,
...$params
];
// empty or too short search query
if (Str::length($query) < $options['minlength']) {
return $collection->limit(0);
}
$words = preg_replace('/(\s)/u', ',', $query);
$words = Str::split($words, ',', $options['minlength']);
if (empty($options['stopwords']) === false) {
$words = array_diff($words, $options['stopwords']);
}
// returns an empty collection if there is no search word
if (empty($words) === true) {
return $collection->limit(0);
}
$words = A::map(
$words,
fn ($value) => Str::wrap(preg_quote($value), $options['words'] ? '\b' : '')
);
$exact = preg_quote($query);
if ($options['words']) {
$exact = '(\b' . $exact . '\b)';
}
$query = Str::lower($query);
$preg = '!(' . implode('|', $words) . ')!iu';
$scores = [];
$results = $collection->filter(function ($item) use ($query, $exact, $preg, $options, &$scores) {
$data = $item->content()->toArray();
$keys = array_keys($data);
$keys[] = 'id';
if ($item instanceof User) {
$keys[] = 'name';
$keys[] = 'email';
$keys[] = 'role';
} elseif ($item instanceof Page) {
// apply the default score for pages
$options['score'] = [
'id' => 64,
'title' => 64,
...$options['score']
];
}
if (empty($options['fields']) === false) {
$fields = array_map('strtolower', $options['fields']);
$keys = array_intersect($keys, $fields);
}
$scoring = [
'hits' => 0,
'score' => 0
];
foreach ($keys as $key) {
$score = $options['score'][$key] ?? 1;
$value = $data[$key] ?? (string)$item->$key();
$lowerValue = Str::lower($value);
// check for exact matches
if ($query == $lowerValue) {
$scoring['score'] += 16 * $score;
$scoring['hits'] += 1;
// check for exact beginning matches
} elseif (
$options['words'] === false &&
Str::startsWith($lowerValue, $query) === true
) {
$scoring['score'] += 8 * $score;
$scoring['hits'] += 1;
// check for exact query matches
} elseif ($matches = preg_match_all('!' . $exact . '!ui', $value, $r)) {
$scoring['score'] += 2 * $score;
$scoring['hits'] += $matches;
}
// check for any match
if ($matches = preg_match_all($preg, $value, $r)) {
$scoring['score'] += $matches * $score;
$scoring['hits'] += $matches;
}
}
$scores[$item->id()] = $scoring;
return $scoring['hits'] > 0;
});
return $results->sort(
fn ($item) => $scores[$item->id()]['score'],
'desc'
);
},
/**
* Add your own session store
*/
'session::store' => function (App $kirby): string|SessionStore {
return $kirby->root('sessions');
},
/**
* Add your own SmartyPants parser
*
* @param string $text Text to parse
* @param array $options SmartyPants options
*/
'smartypants' => function (
App $kirby,
string|null $text = null,
array $options = []
): string {
static $smartypants;
static $config;
// if the config options have changed or the component is called for the first time,
// (re-)initialize the parser object
if ($config !== $options) {
$smartypants = new Smartypants($options);
$config = $options;
}
return $smartypants->parse($text);
},
/**
* Add your own snippet loader
*
* @param string|array $name Snippet name
* @param array $data Data array for the snippet
*/
'snippet' => function (
App $kirby,
string|array|null $name,
array $data = [],
bool $slots = false
): Snippet|string {
return Snippet::factory($name, $data, $slots);
},
/**
* Create a new storage object for the given model
*/
'storage' => function (
App $kirby,
ModelWithContent $model
): Storage {
return new PlainTextStorage(model: $model);
},
/**
* Add your own template engine
*
* @param string $name Template name
* @param string $type Extension type
* @param string $defaultType Default extension type
* @return \Kirby\Template\Template
*/
'template' => function (
App $kirby,
string $name,
string $type = 'html',
string $defaultType = 'html'
) {
return new Template($name, $type, $defaultType);
},
/**
* Add your own thumb generator
*
* @param string $src Root of the original file
* @param string $dst Template string for the root to the desired destination
* @param array $options All thumb options that should be applied: `width`, `height`, `crop`, `blur`, `grayscale`
*/
'thumb' => function (
App $kirby,
string $src,
string $dst,
array $options
): string {
$darkroom = Darkroom::factory(
$kirby->option('thumbs.driver', 'gd'),
$kirby->option('thumbs', [])
);
$options = $darkroom->preprocess($src, $options);
$root = (new Filename($src, $dst, $options))->toString();
F::copy($src, $root, true);
$darkroom->process($root, $options);
return $root;
},
/**
* Modify all URLs
*
* @param string|null $path URL path
* @param array|string|null $options Array of options for the Uri class
* @throws \Kirby\Exception\NotFoundException If an invalid UUID was passed
*/
'url' => function (
App $kirby,
string|null $path = null,
$options = null
): string {
$language = null;
// get language from simple string option
if (is_string($options) === true) {
$language = $options;
$options = null;
}
// get language from array
if (is_array($options) === true && isset($options['language']) === true) {
$language = $options['language'];
unset($options['language']);
}
// get a language url for the linked page, if the page can be found
if ($kirby->multilang() === true) {
$parts = Str::split($path, '#');
if ($parts[0] ?? null) {
$page = $kirby->site()->find($parts[0]);
} else {
$page = $kirby->site()->page();
}
if ($page) {
$path = $page->url($language);
if (isset($parts[1]) === true) {
$path .= '#' . $parts[1];
}
}
}
// keep relative urls
if (
$path !== null &&
(str_starts_with($path, './') || str_starts_with($path, '../'))
) {
return $path;
}
// support UUIDs
if (
$path !== null &&
Uuid::is($path, ['page', 'file']) === true
) {
$model = Uuid::for($path)->model();
if ($model === null) {
throw new NotFoundException(
message: 'The model could not be found for "' . $path . '" uuid'
);
}
$path = $model->url();
}
$url = Url::makeAbsolute($path, $kirby->url());
if ($options === null) {
return $url;
}
return (new Uri($url, $options))->toString();
},
];

View file

@ -0,0 +1,61 @@
<?php
use Kirby\Toolkit\A;
use Kirby\Toolkit\Str;
return [
'mixins' => ['min', 'options'],
'props' => [
/**
* Unset inherited props
*/
'after' => null,
'before' => null,
'icon' => null,
'placeholder' => null,
/**
* Arranges the checkboxes in the given number of columns
*/
'columns' => function (int $columns = 1) {
return $columns;
},
/**
* Default value for the field, which will be used when a page/file/user is created
*/
'default' => function ($default = null) {
return Str::split($default, ',');
},
/**
* Maximum number of checked boxes
*/
'max' => function (int|null $max = null) {
return $max;
},
/**
* Minimum number of checked boxes
*/
'min' => function (int|null $min = null) {
return $min;
},
'value' => function ($value = null) {
return Str::split($value, ',');
},
],
'computed' => [
'default' => function () {
return $this->sanitizeOptions($this->default);
},
'value' => function () {
return $this->sanitizeOptions($this->value);
},
],
'save' => function ($value): string {
return A::join($value, ', ');
},
'validations' => [
'options',
'max',
'min'
]
];

View file

@ -0,0 +1,153 @@
<?php
use Kirby\Cms\Helpers;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Field\FieldOptions;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\Str;
return [
'props' => [
/**
* Unset inherited props
*/
'after' => null,
'before' => null,
/**
* Whether to allow alpha transparency in the color
*/
'alpha' => function (bool $alpha = false) {
return $alpha;
},
/**
* The CSS format (hex, rgb, hsl) to display and store the value
*/
'format' => function (string $format = 'hex'): string {
if (in_array($format, ['hex', 'hsl', 'rgb'], true) === false) {
throw new InvalidArgumentException(
message: 'Unsupported format for color field (supported: hex, rgb, hsl)'
);
}
return $format;
},
/**
* Change mode to disable the color picker (`input`) or to only
* show the `options` as toggles
*/
'mode' => function (string $mode = 'picker'): string {
if (in_array($mode, ['picker', 'input', 'options'], true) === false) {
throw new InvalidArgumentException(
message: 'Unsupported mode for color field (supported: picker, input, options)'
);
}
return $mode;
},
/**
* List of colors that will be shown as buttons
* to directly select them
*/
'options' => function (array $options = []): array {
return $options;
}
],
'computed' => [
'default' => function (): string {
return Str::lower($this->default);
},
'options' => function (): array {
// resolve options to support manual arrays
// alongside api and query options
$props = FieldOptions::polyfill($this->props);
$options = FieldOptions::factory([
'text' => '{{ item.value }}',
'value' => '{{ item.key }}',
...$props['options']
]);
$options = $options->render($this->model());
if (empty($options) === true) {
return [];
}
if (
is_numeric($options[0]['value']) ||
$options[0]['value'] === $options[0]['text']
) {
// simple array of values
// or value=text (from Options class)
$options = A::map($options, fn ($option) => [
'value' => $option['text']
]);
} elseif ($this->isColor($options[0]['text'])) {
// @deprecated 4.0.0
// TODO: Remove in Kirby 6
Helpers::deprecated('Color field "' . $this->name . '": the text => value notation for options has been deprecated and will be removed in Kirby 6. Please rewrite your options as value => text.');
$options = A::map($options, fn ($option) => [
'value' => $option['text'],
// ensure that any HTML in the new text is escaped
'text' => Escape::html($option['value'])
]);
} else {
$options = A::map($options, fn ($option) => [
'value' => $option['value'],
'text' => $option['text']
]);
}
return $options;
}
],
'methods' => [
'isColor' => function (string $value): bool {
return
$this->isHex($value) ||
$this->isRgb($value) ||
$this->isHsl($value);
},
'isHex' => function (string $value): bool {
return preg_match('/^#([\da-f]{3,4}){1,2}$/i', $value) === 1;
},
'isHsl' => function (string $value): bool {
return preg_match('/^hsla?\(\s*(\d{1,3}\.?\d*)(deg|rad|grad|turn)?(?:,|\s)+(\d{1,3})%(?:,|\s)+(\d{1,3})%(?:,|\s|\/)*(\d*(?:\.\d+)?)(%?)\s*\)?$/i', $value) === 1;
},
'isRgb' => function (string $value): bool {
return preg_match('/^rgba?\(\s*(\d{1,3})(%?)(?:,|\s)+(\d{1,3})(%?)(?:,|\s)+(\d{1,3})(%?)(?:,|\s|\/)*(\d*(?:\.\d+)?)(%?)\s*\)?$/i', $value) === 1;
},
],
'validations' => [
'color' => function ($value) {
if (empty($value) === true) {
return true;
}
if ($this->format === 'hex' && $this->isHex($value) === false) {
throw new InvalidArgumentException(
key: 'validation.color',
data: ['format' => 'hex']
);
}
if ($this->format === 'rgb' && $this->isRgb($value) === false) {
throw new InvalidArgumentException(
key: 'validation.color',
data: ['format' => 'rgb']
);
}
if ($this->format === 'hsl' && $this->isHsl($value) === false) {
throw new InvalidArgumentException(
key: 'validation.color',
data: ['format' => 'hsl']
);
}
}
]
];

View file

@ -0,0 +1,154 @@
<?php
use Kirby\Exception\Exception;
use Kirby\Form\Field;
use Kirby\Toolkit\Date;
use Kirby\Toolkit\I18n;
use Kirby\Toolkit\Str;
return [
'mixins' => ['datetime'],
'props' => [
/**
* Unset inherited props
*/
'placeholder' => null,
/**
* Activate/deactivate the dropdown calendar
*/
'calendar' => function (bool $calendar = true) {
return $calendar;
},
/**
* Default date when a new page/file/user gets created
*/
'default' => function (string|null $default = null): string {
return $this->toDatetime($default) ?? '';
},
/**
* Custom format (dayjs tokens: `DD`, `MM`, `YYYY`) that is
* used to display the field in the Panel
*/
'display' => function ($display = 'YYYY-MM-DD') {
return I18n::translate($display, $display);
},
/**
* Changes the calendar icon to something custom
*/
'icon' => function (string $icon = 'calendar') {
return $icon;
},
/**
* Latest date, which can be selected/saved (Y-m-d)
*/
'max' => function (string|null $max = null): string|null {
return Date::optional($max);
},
/**
* Earliest date, which can be selected/saved (Y-m-d)
*/
'min' => function (string|null $min = null): string|null {
return Date::optional($min);
},
/**
* Round to the nearest: sub-options for `unit` (day) and `size` (1)
*/
'step' => function ($step = null) {
return $step;
},
/**
* Pass `true` or an array of time field options to show the time selector.
*/
'time' => function ($time = false) {
return $time;
},
/**
* Must be a parseable date string
*/
'value' => function ($value = null) {
return $value;
}
],
'computed' => [
'display' => function () {
if ($this->display) {
return Str::upper($this->display);
}
},
'format' => function () {
return $this->props['format'] ?? ($this->time === false ? 'Y-m-d' : 'Y-m-d H:i:s');
},
'time' => function () {
if ($this->time === false) {
return false;
}
$props = is_array($this->time) ? $this->time : [];
$props['model'] = $this->model();
$field = new Field('time', $props);
return $field->toArray();
},
'step' => function () {
if ($this->time === false || empty($this->time['step']) === true) {
return Date::stepConfig($this->step, [
'size' => 1,
'unit' => 'day'
]);
}
return Date::stepConfig($this->time['step'], [
'size' => 5,
'unit' => 'minute'
]);
},
'value' => function (): string {
return $this->toDatetime($this->value) ?? '';
},
],
'validations' => [
'date',
'minMax' => function ($value) {
if (!$value = Date::optional($value)) {
return true;
}
$min = Date::optional($this->min);
$max = Date::optional($this->max);
$format = $this->time === false ? 'd.m.Y' : 'd.m.Y H:i';
if ($min && $max && $value->isBetween($min, $max) === false) {
throw new Exception(
key: 'validation.date.between',
data: [
'min' => $min->format($format),
'max' => $max->format($format)
]
);
}
if ($min && $value->isMin($min) === false) {
throw new Exception(
key: 'validation.date.after',
data: ['date' => $min->format($format)]
);
}
if ($max && $value->isMax($max) === false) {
throw new Exception(
key: 'validation.date.before',
data: ['date' => $max->format($format)]
);
}
return true;
},
]
];

View file

@ -0,0 +1,40 @@
<?php
use Kirby\Toolkit\I18n;
return [
'extends' => 'text',
'props' => [
/**
* Unset inherited props
*/
'converter' => null,
'counter' => null,
/**
* Sets the HTML5 autocomplete mode for the input
*/
'autocomplete' => function (string $autocomplete = 'email') {
return $autocomplete;
},
/**
* Changes the email icon to something custom
*/
'icon' => function (string $icon = 'email') {
return $icon;
},
/**
* Custom placeholder text, when the field is empty.
*/
'placeholder' => function ($value = null) {
return I18n::translate($value, $value) ?? I18n::translate('email.placeholder');
}
],
'validations' => [
'minlength',
'maxlength',
'email'
]
];

View file

@ -0,0 +1,141 @@
<?php
use Kirby\Cms\ModelWithContent;
use Kirby\Data\Data;
use Kirby\Toolkit\A;
return [
'mixins' => [
'filepicker',
'layout',
'min',
'picker',
'upload'
],
'props' => [
/**
* Unset inherited props
*/
'after' => null,
'before' => null,
'autofocus' => null,
'icon' => null,
'placeholder' => null,
/**
* Sets the file(s), which are selected by default when a new page is created
*/
'default' => function ($default = null) {
return $default;
},
'value' => function ($value = null) {
return $value;
}
],
'computed' => [
'parentModel' => function () {
if (
is_string($this->parent) === true &&
$model = $this->model()->query(
$this->parent,
ModelWithContent::class
)
) {
return $model;
}
return $this->model();
},
'parent' => function () {
return $this->parentModel->apiUrl(true);
},
'query' => function () {
return $this->query ?? $this->parentModel::CLASS_ALIAS . '.files';
},
'default' => function () {
return $this->toFiles($this->default);
},
'value' => function () {
return $this->toFiles($this->value);
},
],
'methods' => [
'fileResponse' => function ($file) {
return $file->panel()->pickerData([
'image' => $this->image,
'info' => $this->info ?? false,
'layout' => $this->layout,
'model' => $this->model(),
'text' => $this->text,
]);
},
'toFiles' => function ($value = null) {
$files = [];
foreach (Data::decode($value, 'yaml') as $id) {
if (is_array($id) === true) {
$id = $id['uuid'] ?? $id['id'] ?? null;
}
if (
$id !== null &&
($file = $this->kirby()->file($id, $this->model()))
) {
$files[] = $this->fileResponse($file);
}
}
return $files;
}
],
'api' => function () {
return [
[
'pattern' => '/',
'action' => function () {
$field = $this->field();
return $field->filepicker([
'image' => $field->image(),
'info' => $field->info(),
'layout' => $field->layout(),
'limit' => $field->limit(),
'page' => $this->requestQuery('page'),
'query' => $field->query(),
'search' => $this->requestQuery('search'),
'text' => $field->text()
]);
}
],
[
'pattern' => 'upload',
'method' => 'POST',
'action' => function () {
$field = $this->field();
$uploads = $field->uploads();
// move_uploaded_file() not working with unit test
// @codeCoverageIgnoreStart
return $field->upload($this, $uploads, function ($file, $parent) use ($field) {
return $file->panel()->pickerData([
'image' => $field->image(),
'info' => $field->info(),
'layout' => $field->layout(),
'model' => $field->model(),
'text' => $field->text(),
]);
});
// @codeCoverageIgnoreEnd
}
]
];
},
'save' => function ($value = null) {
return A::pluck($value, $this->store);
},
'validations' => [
'max',
'min'
]
];

View file

@ -0,0 +1,5 @@
<?php
return [
'save' => false
];

View file

@ -0,0 +1,19 @@
<?php
return [
'save' => false,
'props' => [
/**
* Unset inherited props
*/
'after' => null,
'autofocus' => null,
'before' => null,
'default' => null,
'disabled' => null,
'icon' => null,
'placeholder' => null,
'required' => null,
'translate' => null
]
];

Some files were not shown because too many files have changed in this diff Show more