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,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();
}
],
];