Merge pull request #567 from getformwork/feature/history-and-preview-updates

Add pages history and live preview
This commit is contained in:
Giuseppe Criscione 2024-10-03 22:00:22 +02:00 committed by GitHub
commit b7b24d0657
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 410 additions and 71 deletions

View File

@ -132,10 +132,6 @@ final class App
$response->send();
if ($this->config()->get('system.statistics.enabled') && $this->site()->currentPage() !== null && !$this->site()->currentPage()->isErrorPage()) {
$this->container->get(Statistics::class)->trackVisit();
}
return $response;
}

View File

@ -13,6 +13,7 @@ use Formwork\Pages\Page;
use Formwork\Pages\Site;
use Formwork\Router\RouteParams;
use Formwork\Router\Router;
use Formwork\Statistics\Statistics;
use Formwork\Utils\FileSystem;
use Formwork\View\ViewFactory;
@ -23,7 +24,7 @@ class PageController extends AbstractController
parent::__construct();
}
public function load(RouteParams $routeParams, ViewFactory $viewFactory): Response
public function load(RouteParams $routeParams, ViewFactory $viewFactory, Statistics $statistics): Response
{
if ($this->site->get('maintenance.enabled') && !$this->app->panel()?->isLoggedIn()) {
if ($this->site->get('maintenance.page') !== null) {
@ -68,6 +69,10 @@ class PageController extends AbstractController
}
}
if ($this->config->get('system.statistics.enabled')) {
$statistics->trackVisit();
}
if ($page->isPublished() && $page->routable()) {
return $this->getPageResponse($page);
}

View File

@ -0,0 +1,68 @@
<?php
namespace Formwork\Panel\ContentHistory;
use Formwork\Parsers\Json;
use Formwork\Utils\Arr;
use Formwork\Utils\FileSystem;
class ContentHistory
{
public const HISTORY_FILENAME = '.history';
public const HISTORY_DEFAULT_LIMIT = 1;
protected ContentHistoryItemCollection $items;
public function __construct(
protected string $path,
protected int $limit = self::HISTORY_DEFAULT_LIMIT
) {
}
public function path(): string
{
return $this->path;
}
public function exists(): bool
{
return FileSystem::exists(FileSystem::joinPaths($this->path, self::HISTORY_FILENAME));
}
public function items(): ContentHistoryItemCollection
{
if (isset($this->items)) {
return $this->items;
}
if (!$this->exists()) {
return $this->items = new ContentHistoryItemCollection();
}
$items = Json::parse(FileSystem::read(FileSystem::joinPaths($this->path, self::HISTORY_FILENAME)));
return $this->items = new ContentHistoryItemCollection(Arr::map($items, fn ($item) => ContentHistoryItem::fromArray($item)));
}
public function lastItem(): ?ContentHistoryItem
{
return $this->items()->last();
}
public function isJustCreated(): bool
{
return $this->lastItem()?->event() === ContentHistoryEvent::Created;
}
public function update(ContentHistoryEvent $contentHistoryEvent, string $user, int $timestamp): void
{
$this->items()->add(new ContentHistoryItem($contentHistoryEvent, $user, $timestamp));
if ($this->items()->count() > $this->limit) {
$this->items = $this->items()->slice(-$this->limit);
}
}
public function save(): void
{
$data = $this->items()->map(fn ($item) => $item->toArray())->toArray();
FileSystem::write(FileSystem::joinPaths($this->path, self::HISTORY_FILENAME), Json::encode($data));
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Formwork\Panel\ContentHistory;
enum ContentHistoryEvent: string
{
case Created = 'created';
case Edited = 'edited';
}

View File

@ -0,0 +1,54 @@
<?php
namespace Formwork\Panel\ContentHistory;
use Formwork\Data\Contracts\ArraySerializable;
class ContentHistoryItem implements ArraySerializable
{
final public function __construct(
protected ContentHistoryEvent $contentHistoryEvent,
protected string $user,
protected int $time
) {
}
public function event(): ContentHistoryEvent
{
return $this->contentHistoryEvent;
}
public function user(): string
{
return $this->user;
}
public function time(): int
{
return $this->time;
}
/**
* @return array{event: string, user: string, time: int}
*/
public function toArray(): array
{
return [
'event' => $this->contentHistoryEvent->value,
'user' => $this->user,
'time' => $this->time,
];
}
/**
* @param array{event: string, user: string, time: int} $data
*/
public static function fromArray(array $data): static
{
return new static(
ContentHistoryEvent::from($data['event']),
$data['user'],
$data['time']
);
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Formwork\Panel\ContentHistory;
use Formwork\Data\AbstractCollection;
class ContentHistoryItemCollection extends AbstractCollection
{
protected bool $associative = false;
protected ?string $dataType = ContentHistoryItem::class;
protected bool $mutable = true;
}

View File

@ -16,10 +16,13 @@ use Formwork\Http\Response;
use Formwork\Images\Image;
use Formwork\Pages\Page;
use Formwork\Pages\Site;
use Formwork\Panel\ContentHistory\ContentHistory;
use Formwork\Panel\ContentHistory\ContentHistoryEvent;
use Formwork\Parsers\Yaml;
use Formwork\Router\RouteParams;
use Formwork\Schemes\Schemes;
use Formwork\Utils\Arr;
use Formwork\Utils\Constraint;
use Formwork\Utils\Date;
use Formwork\Utils\FileSystem;
use Formwork\Utils\MimeType;
@ -169,13 +172,22 @@ class PagesController extends AbstractController
$data = $this->request->input();
// Validate fields against data
$fields->setValues($data, null)->validate();
$fields->setValues($data, null);
$forceUpdate = false;
if ($this->request->query()->has('publish')) {
$fields->setValues(['published' => Constraint::isTruthy($this->request->query()->get('publish'))]);
$forceUpdate = true;
}
$fields->validate();
$error = false;
// Update the page
try {
$page = $this->updatePage($page, $data, $fields);
$page = $this->updatePage($page, $data, $fields, force: $forceUpdate);
} catch (TranslatedException $e) {
$error = true;
$this->panel()->notify($e->getTranslatedMessage(), 'error');
@ -218,6 +230,10 @@ class PagesController extends AbstractController
$this->modal('renameFile');
$contentHistory = $page->path()
? new ContentHistory($page->path())
: null;
return new Response($this->view('pages.editor', [
'title' => $this->translate('panel.pages.editPage', $page->title()),
'page' => $page,
@ -225,9 +241,42 @@ class PagesController extends AbstractController
'templates' => $this->site()->templates()->keys(),
'parents' => $this->site()->descendants()->sortBy('relativePath'),
'currentLanguage' => $routeParams->get('language', $page->language()?->code()),
'history' => $contentHistory,
]));
}
/**
* Pages@preview action
*/
public function preview(RouteParams $routeParams): Response
{
$page = $this->site()->findPage($routeParams->get('page'));
if ($page === null) {
$this->panel()->notify($this->translate('panel.pages.page.cannotPreview.pageNotFound'), 'error');
return $this->redirectToReferer(default: '/pages/');
}
$this->site()->setCurrentPage($page);
// Load data from POST variables
$requestData = $this->request->input();
// Validate fields against data
$page->fields()->setValues($requestData)->validate();
if ($page->template()->name() !== ($template = $requestData->get('template'))) {
$page->reload(['template' => $this->site()->templates()->get($template)]);
}
if ($page->parent() !== ($parent = $this->resolveParent($requestData->get('parent')))) {
$this->panel()->notify($this->translate('panel.pages.page.cannotPreview.parentChanged'), 'error');
return $this->redirectToReferer(default: '/pages/');
}
return new Response($page->render(), $page->responseStatus(), $page->headers());
}
/**
* Pages@reorder action
*/
@ -547,13 +596,18 @@ class PagesController extends AbstractController
FileSystem::write($path . $filename, $fileContent);
$contentHistory = new ContentHistory($path);
$contentHistory->update(ContentHistoryEvent::Created, $this->user()->username(), time());
$contentHistory->save();
return $this->site()->retrievePage($path);
}
/**
* Update a page
*/
protected function updatePage(Page $page, RequestData $requestData, FieldCollection $fieldCollection): Page
protected function updatePage(Page $page, RequestData $requestData, FieldCollection $fieldCollection, bool $force = false): Page
{
if ($page->contentFile() === null) {
throw new RuntimeException('Unexpected missing content file');
@ -597,7 +651,7 @@ class PagesController extends AbstractController
$differ = $frontmatter !== $page->contentFile()->frontmatter() || $content !== $page->data()['content'] || $language !== $page->language();
if ($differ) {
if ($force || $differ) {
$filename = $requestData->get('template');
$filename .= empty($language) ? '' : '.' . $language;
$filename .= $this->config->get('system.content.extension');
@ -615,6 +669,11 @@ class PagesController extends AbstractController
FileSystem::write($page->path() . $filename, $fileContent);
FileSystem::touch($this->site()->path());
$contentHistory = new ContentHistory($page->path());
$contentHistory->update(ContentHistoryEvent::Edited, $this->user()->username(), time());
$contentHistory->save();
// Update page with the new data
$page->reload();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -113,6 +113,12 @@ return [
'methods' => ['GET', 'POST'],
],
'panel.pages.preview' => [
'path' => '/pages/{page}/preview/',
'action' => 'Formwork\Panel\Controllers\PagesController@preview',
'methods' => ['POST'],
],
'panel.pages.edit.lang' => [
'path' => '/pages/{page}/edit/language/{language}/',
'action' => 'Formwork\Panel\Controllers\PagesController@edit',

View File

@ -154,3 +154,18 @@
margin-right: $focusring-width;
}
}
.button-indicator {
position: relative;
&::after {
position: absolute;
top: 0;
right: 0;
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background-color: var(--color-accent-500);
content: " ";
}
}

View File

@ -7,34 +7,34 @@ import { serializeForm } from "../utils/forms";
export class Form {
inputs: Inputs;
originalData: string;
element: HTMLFormElement;
constructor(form: HTMLFormElement) {
this.element = form;
this.inputs = new Inputs(form);
// Serialize after inputs are loaded
this.originalData = serializeForm(form);
const handleBeforeunload = (event: Event) => {
if (this.hasChanged()) {
event.preventDefault();
event.returnValue = false;
}
};
const removeBeforeUnload = () => {
window.removeEventListener("beforeunload", handleBeforeunload);
};
window.addEventListener("beforeunload", handleBeforeunload);
form.addEventListener("submit", removeBeforeUnload);
const hasChanged = (checkFileInputs = true) => {
const fileInputs = $$("input[type=file]", form) as NodeListOf<HTMLInputElement>;
if (checkFileInputs === true && fileInputs.length > 0) {
for (const fileInput of Array.from(fileInputs)) {
if (fileInput.files && fileInput.files.length > 0) {
return true;
}
}
}
return serializeForm(form) !== this.originalData;
};
$$('a[href]:not([href^="#"]):not([target="_blank"]):not([target^="formwork-"])').forEach((element: HTMLAnchorElement) => {
element.addEventListener("click", (event) => {
if (hasChanged()) {
if (this.hasChanged()) {
event.preventDefault();
app.modals["changesModal"].show(undefined, (modal) => {
const continueCommand = $("[data-command=continue]", modal.element);
@ -48,7 +48,7 @@ export class Form {
$$("input[type=file][data-auto-upload]", form).forEach((element) => {
element.addEventListener("change", () => {
if (!hasChanged(false)) {
if (!this.hasChanged(false)) {
form.requestSubmit($("[type=submit]", form));
}
});
@ -56,17 +56,6 @@ export class Form {
registerModalExceptions();
function handleBeforeunload(event: Event) {
if (hasChanged()) {
event.preventDefault();
event.returnValue = false;
}
}
function removeBeforeUnload() {
window.removeEventListener("beforeunload", handleBeforeunload);
}
function registerModalExceptions() {
const changesModal = document.getElementById("changesModal");
const deletePageModal = document.getElementById("deletePageModal");
@ -99,4 +88,18 @@ export class Form {
}
}
}
hasChanged(checkFileInputs: boolean = true) {
const fileInputs = $$("input[type=file]", this.element) as NodeListOf<HTMLInputElement>;
if (checkFileInputs === true && fileInputs.length > 0) {
for (const fileInput of Array.from(fileInputs)) {
if (fileInput.files && fileInput.files.length > 0) {
return true;
}
}
}
return serializeForm(this.element) !== this.originalData;
}
}

View File

@ -41,7 +41,8 @@ export class Inputs {
element.addEventListener("click", () => {
const target = document.getElementById(targetId) as HTMLInputElement;
target.value = "";
target.dispatchEvent(new Event("change"));
target.dispatchEvent(new Event("input", { bubbles: true }));
target.dispatchEvent(new Event("change", { bubbles: true }));
});
}
});

View File

@ -66,6 +66,8 @@ export class DateInput {
if (dateInput !== null) {
inputValues[dateInput.id] = date;
dateInput.value = formatDateTime(date);
dateInput.dispatchEvent(new Event("input", { bubbles: true }));
dateInput.dispatchEvent(new Event("change", { bubbles: true }));
}
},
} satisfies DateInputOptions;
@ -103,6 +105,8 @@ export class DateInput {
case "Backspace":
input.value = "";
input.blur();
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
break;
case "Escape":
input.blur();

View File

@ -96,6 +96,8 @@ export class DurationInput {
seconds = Math.min(seconds, parseInt(hiddenInput.max));
}
hiddenInput.value = `${Math.round(seconds / TIME_INTERVALS[options.unit])}`;
hiddenInput.dispatchEvent(new Event("input", { bubbles: true }));
hiddenInput.dispatchEvent(new Event("change", { bubbles: true }));
}
function updateInnerInputs() {

View File

@ -106,6 +106,8 @@ export class EditorInput {
"changes",
debounce(() => {
textarea.value = editor.getValue();
textarea.dispatchEvent(new Event("input", { bubbles: true }));
textarea.dispatchEvent(new Event("change", { bubbles: true }));
if (editor.historySize().undo < 1) {
($("[data-command=undo]") as HTMLButtonElement).disabled = true;
} else {

View File

@ -34,6 +34,8 @@ export class ImagePicker {
const selectedThumbnailFilename = selectedThumbnail.dataset.filename;
if (target && selectedThumbnailFilename) {
target.value = selectedThumbnailFilename;
target.dispatchEvent(new Event("input", { bubbles: true }));
target.dispatchEvent(new Event("change", { bubbles: true }));
}
}
});

View File

@ -352,7 +352,8 @@ export class SelectInput {
function setCurrent(item: HTMLElement) {
select.value = item.dataset.value as string;
labelInput.value = item.innerText;
select.dispatchEvent(new Event("change"));
select.dispatchEvent(new Event("input", { bubbles: true }));
select.dispatchEvent(new Event("change", { bubbles: true }));
}
function getCurrent() {

View File

@ -233,6 +233,8 @@ export class TagInput {
function updateTags() {
hiddenInput.value = tags.join(", ");
hiddenInput.dispatchEvent(new Event("input", { bubbles: true }));
hiddenInput.dispatchEvent(new Event("change", { bubbles: true }));
updatePlaceholder();
}

View File

@ -8,10 +8,11 @@ import Sortable from "sortablejs";
export class Pages {
constructor() {
const commandExpandAllPages = $("[data-command=expand-all-pages]");
const commandCollapseAllPages = $("[data-command=collapse-all-pages]");
const commandReorderPages = $("[data-command=reorder-pages]");
const commandChangeSlug = $("[data-command=change-slug]");
const commandExpandAllPages = $("[data-command=expand-all-pages]") as HTMLButtonElement;
const commandCollapseAllPages = $("[data-command=collapse-all-pages]") as HTMLButtonElement;
const commandReorderPages = $("[data-command=reorder-pages]") as HTMLButtonElement;
const commandPreview = $("[data-command=preview]") as HTMLButtonElement;
const commandChangeSlug = $("[data-command=change-slug]") as HTMLButtonElement;
const searchInput = $(".page-search");
@ -168,6 +169,29 @@ export class Pages {
});
}
if (commandPreview) {
const editorForm = app.forms["page-editor-form"];
const pageParent = $("#page-parent", editorForm.element) as HTMLInputElement;
const previousParent = pageParent.value;
if (editorForm) {
editorForm.element.addEventListener(
"input",
debounce(() => {
if (pageParent.value !== previousParent) {
// Prevent preview if the parent page has changed
commandPreview.disabled = true;
commandPreview.classList.remove("button-indicator");
return;
}
commandPreview.disabled = false;
commandPreview.classList.toggle("button-indicator", editorForm.hasChanged());
}, 500),
);
}
}
if (slugModal && commandChangeSlug) {
commandChangeSlug.addEventListener("click", () => {
app.modals["slugModal"].show(undefined, (modal) => {
@ -204,7 +228,7 @@ export class Pages {
if (slug.length > 0) {
const route = ($(".page-route-inner") as HTMLElement).innerHTML;
($("#newSlug", slugModal) as HTMLInputElement).value = slug;
($("#slug", slugModal) as HTMLInputElement).value = slug;
($("#slug") as HTMLInputElement).value = slug;
($(".page-route-inner") as HTMLElement).innerHTML = route.replace(/\/[a-z0-9-]+\/$/, `/${slug}/`);
}

View File

@ -188,6 +188,8 @@ panel.pages.file.info.uri: URI
panel.pages.file.position: Position
panel.pages.file.preview: Vorschau
panel.pages.files: Dateien
panel.pages.history.event.created: Seite erstellt von %s %s.
panel.pages.history.event.edited: Seite bearbeitet von %s %s.
panel.pages.languages: Sprachen
panel.pages.languages.addLanguage: "%s hinzufügen"
panel.pages.languages.editLanguage: "%s bearbeiten"
@ -227,6 +229,8 @@ panel.pages.page.cannotEdit.varMissing: Seite kann nicht bearbeitet werden, es f
panel.pages.page.cannotGetFileInfo.fileNotFound: Dateiinfo kann nicht abgerufen werden, Datei nicht gefunden
panel.pages.page.cannotGetFileInfo.pageNotFound: Dateiinfo kann nicht abgerufen werden, Seite nicht gefunden
panel.pages.page.cannotMove: Seite kann nicht verschoben werden
panel.pages.page.cannotPreview.pageNotFound: Vorschau der Seite nicht möglich, Seite nicht gefunden
panel.pages.page.cannotPreview.parentChanged: Vorschau der Seite nicht möglich, übergeordnete Seite wurde geändert
panel.pages.page.cannotRenameFile.fileAlreadyExists: Datei kann nicht umbenannt werden, eine Datei mit demselben Namen existiert bereits
panel.pages.page.cannotRenameFile.fileNotFound: Datei kann nicht umbenannt werden, Datei nicht gefunden
panel.pages.page.cannotRenameFile.pageNotFound: Datei kann nicht umbenannt werden, Seite nicht gefunden
@ -263,10 +267,12 @@ panel.pages.preview: Vorschau
panel.pages.previewFile: Vorschau
panel.pages.previous: Vorherige Seite
panel.pages.previousFile: Vorherige Datei
panel.pages.publish: Veröffentlichen
panel.pages.renameFile: Datei umbenennen
panel.pages.renameFile.name: Name
panel.pages.replaceFile: Datei ersetzen
panel.pages.save: Speichern
panel.pages.saveOnly: Speichern ohne zu veröffentlichen
panel.pages.status.notPublished: Nicht veröffentlicht
panel.pages.status.notRoutable: Nicht routbar
panel.pages.status.published: Veröffentlicht
@ -275,6 +281,7 @@ panel.pages.summary: Zusammenfassung
panel.pages.template: Vorlage
panel.pages.text: Text
panel.pages.toggleChildren: Untergeordnete Seiten umschalten
panel.pages.viewPage: Seite anzeigen
panel.panel: Administrationspanel
panel.register.createUser: Formwork ist installiert, aber es wurden keine Benutzer gefunden. Bitte registrieren Sie jetzt einen Benutzer.
panel.register.register: Neuen Benutzer registrieren

View File

@ -188,6 +188,8 @@ panel.pages.file.info.uri: URI
panel.pages.file.position: Position
panel.pages.file.preview: Preview
panel.pages.files: Files
panel.pages.history.event.created: Page created by %s %s.
panel.pages.history.event.edited: Page edited by %s %s.
panel.pages.languages: Languages
panel.pages.languages.addLanguage: Add %s
panel.pages.languages.editLanguage: Edit %s
@ -227,6 +229,8 @@ panel.pages.page.cannotEdit.varMissing: Cannot edit page, missing a variable
panel.pages.page.cannotGetFileInfo.fileNotFound: Cannot get file info, file not found
panel.pages.page.cannotGetFileInfo.pageNotFound: Cannot get file info, page not found
panel.pages.page.cannotMove: Cannot move page
panel.pages.page.cannotPreview.pageNotFound: Cannot preview page, page not found
panel.pages.page.cannotPreview.parentChanged: Cannot preview page, parent page has been changed
panel.pages.page.cannotRenameFile.fileAlreadyExists: Cannot rename file, a file with the same name already exists
panel.pages.page.cannotRenameFile.fileNotFound: Cannot rename file, file not found
panel.pages.page.cannotRenameFile.pageNotFound: Cannot rename file, page not found
@ -263,10 +267,12 @@ panel.pages.preview: Preview
panel.pages.previewFile: Preview
panel.pages.previous: Previous page
panel.pages.previousFile: Previous file
panel.pages.publish: Publish
panel.pages.renameFile: Rename file
panel.pages.renameFile.name: Name
panel.pages.replaceFile: Replace file
panel.pages.save: Save
panel.pages.saveOnly: Save without publishing
panel.pages.status.notPublished: Not published
panel.pages.status.notRoutable: Not routable
panel.pages.status.published: Published
@ -275,6 +281,7 @@ panel.pages.summary: Summary
panel.pages.template: Template
panel.pages.text: Text
panel.pages.toggleChildren: Toggle children pages
panel.pages.viewPage: View page
panel.panel: Administration panel
panel.register.createUser: Formwork is installed but no users were found. Please register a user now.
panel.register.register: Register new user

View File

@ -188,6 +188,8 @@ panel.pages.file.info.uri: URI
panel.pages.file.position: Posición
panel.pages.file.preview: Vista previa
panel.pages.files: Archivos
panel.pages.history.event.created: Página creada por %s %s.
panel.pages.history.event.edited: Página editada por %s %s.
panel.pages.languages: Idiomas
panel.pages.languages.addLanguage: Añadir %s
panel.pages.languages.editLanguage: Editar %s
@ -227,6 +229,8 @@ panel.pages.page.cannotEdit.varMissing: No se puede editar la página, falta una
panel.pages.page.cannotGetFileInfo.fileNotFound: No se pueden obtener las información del archivo, archivo no encontrado
panel.pages.page.cannotGetFileInfo.pageNotFound: No se pueden obtener las información del archivo, página no encontrada
panel.pages.page.cannotMove: No se puede mover la página
panel.pages.page.cannotPreview.pageNotFound: No se puede previsualizar la página, página no encontrada
panel.pages.page.cannotPreview.parentChanged: No se puede previsualizar la página, la página principal ha sido cambiada
panel.pages.page.cannotRenameFile.fileAlreadyExists: No se puede renombrar el archivo, ya existe un archivo con el mismo nombre
panel.pages.page.cannotRenameFile.fileNotFound: No se puede renombrar el archivo, archivo no encontrado
panel.pages.page.cannotRenameFile.pageNotFound: No se puede renombrar el archivo, página no encontrada
@ -263,10 +267,12 @@ panel.pages.preview: Vista previa
panel.pages.previewFile: Vista previa
panel.pages.previous: Página anterior
panel.pages.previousFile: Archivo anterior
panel.pages.publish: Publicar
panel.pages.renameFile: Renombrar archivo
panel.pages.renameFile.name: Nombre
panel.pages.replaceFile: Reemplazar archivo
panel.pages.save: Guardar
panel.pages.saveOnly: Guardar sin publicar
panel.pages.status.notPublished: No publicado
panel.pages.status.notRoutable: No enrutable
panel.pages.status.published: Publicado
@ -275,6 +281,7 @@ panel.pages.summary: Resumen
panel.pages.template: Plantilla
panel.pages.text: Texto
panel.pages.toggleChildren: Alternar páginas secundarias
panel.pages.viewPage: Ver página
panel.panel: Panel de administración
panel.register.createUser: Formwork está instalado pero no se encontraron usuarios. Por favor, registra un usuario ahora.
panel.register.register: Registrar nuevo usuario

View File

@ -188,6 +188,8 @@ panel.pages.file.info.uri: URI
panel.pages.file.position: Position
panel.pages.file.preview: Aperçu
panel.pages.files: Fichiers
panel.pages.history.event.created: Page créée par %s %s.
panel.pages.history.event.edited: Page modifiée par %s %s.
panel.pages.languages: Langues
panel.pages.languages.addLanguage: Ajouter %s
panel.pages.languages.editLanguage: Éditer %s
@ -227,6 +229,8 @@ panel.pages.page.cannotEdit.varMissing: Impossible de modifier la page, il manqu
panel.pages.page.cannotGetFileInfo.fileNotFound: Impossible de récupérer les informations sur le fichier, fichier non trouvé
panel.pages.page.cannotGetFileInfo.pageNotFound: Impossible de récupérer les informations sur le fichier, page non trouvée
panel.pages.page.cannotMove: Impossible de déplacer la page
panel.pages.page.cannotPreview.pageNotFound: Impossible de prévisualiser la page, page non trouvée
panel.pages.page.cannotPreview.parentChanged: Impossible de prévisualiser la page, la page parente a été modifiée
panel.pages.page.cannotRenameFile.fileAlreadyExists: Impossible de renommer le fichier, un fichier portant le même nom existe déjà
panel.pages.page.cannotRenameFile.fileNotFound: Impossible de renommer le fichier, fichier introuvable
panel.pages.page.cannotRenameFile.pageNotFound: Impossible de renommer le fichier, page introuvable
@ -263,10 +267,12 @@ panel.pages.preview: Aperçu
panel.pages.previewFile: Aperçu
panel.pages.previous: Page précédente
panel.pages.previousFile: Fichier précédent
panel.pages.publish: Publier
panel.pages.renameFile: Renommer le fichier
panel.pages.renameFile.name: Nom
panel.pages.replaceFile: Remplacer le fichier
panel.pages.save: Enregistrer
panel.pages.saveOnly: Enregistrer sans publier
panel.pages.status.notPublished: Brouillon
panel.pages.status.notRoutable: Inaccessible
panel.pages.status.published: Publié
@ -275,6 +281,7 @@ panel.pages.summary: Résumé
panel.pages.template: Modèle
panel.pages.text: Texte
panel.pages.toggleChildren: Afficher/Masquer les sous-pages
panel.pages.viewPage: Voir la page
panel.panel: Panneau dadministration
panel.register.createUser: Le compte panel est installé mais aucun utilisateur na été trouvé. Veuillez enregistrer un utilisateur maintenant.
panel.register.register: Enregistrer un nouvel utilisateur

View File

@ -188,6 +188,8 @@ panel.pages.file.info.uri: URI
panel.pages.file.position: Posizione
panel.pages.file.preview: Anteprima
panel.pages.files: File
panel.pages.history.event.created: Pagina creata da %s %s.
panel.pages.history.event.edited: Pagina modificata da %s %s.
panel.pages.languages: Lingue
panel.pages.languages.addLanguage: Aggiungi %s
panel.pages.languages.editLanguage: Modifica %s
@ -227,6 +229,8 @@ panel.pages.page.cannotEdit.varMissing: Impossibile modificare la pagina, manca
panel.pages.page.cannotGetFileInfo.fileNotFound: Impossibile ottenere le informazioni sul file, file non trovato
panel.pages.page.cannotGetFileInfo.pageNotFound: Impossibile ottenere le informazioni sul file, pagina non trovata
panel.pages.page.cannotMove: Impossibile spostare la pagina
panel.pages.page.cannotPreview.pageNotFound: Impossibile visualizzare lanteprima, pagina non trovata
panel.pages.page.cannotPreview.parentChanged: Impossible visualizzare lanteprima, la pagina superiore è stata modificata
panel.pages.page.cannotRenameFile.fileAlreadyExists: Impossibile rinominare il file, un file con lo stesso nome esiste già
panel.pages.page.cannotRenameFile.fileNotFound: Impossibile rinominare il file, file non trovato
panel.pages.page.cannotRenameFile.pageNotFound: Impossibile rinominare il file, pagina non trovata
@ -263,10 +267,12 @@ panel.pages.preview: Anteprima
panel.pages.previewFile: Anteprima
panel.pages.previous: Pagina precedente
panel.pages.previousFile: File precedente
panel.pages.publish: Pubblica
panel.pages.renameFile: Rinomina file
panel.pages.renameFile.name: Nome
panel.pages.replaceFile: Sostituisci file
panel.pages.save: Salva
panel.pages.saveOnly: Salva senza pubblicare
panel.pages.status.notPublished: Non pubblicato
panel.pages.status.notRoutable: Non raggiungibile
panel.pages.status.published: Pubblicato
@ -275,6 +281,7 @@ panel.pages.summary: Riassunto
panel.pages.template: Template
panel.pages.text: Testo
panel.pages.toggleChildren: Mostra/nascondi sottopagine
panel.pages.viewPage: Visualizza pagina
panel.panel: Pannello di amministrazione
panel.register.createUser: Formwork è installato ma non è stato trovato alcun utente. Registrane uno ora.
panel.register.register: Registra nuovo utente

View File

@ -188,6 +188,8 @@ panel.pages.file.info.uri: URI
panel.pages.file.position: Posição
panel.pages.file.preview: Pré-visualização
panel.pages.files: Ficheiros
panel.pages.history.event.created: Página criada por %s %s.
panel.pages.history.event.edited: Página editada por %s %s.
panel.pages.languages: Idiomas
panel.pages.languages.addLanguage: Criar %s
panel.pages.languages.editLanguage: Editar %s
@ -227,6 +229,8 @@ panel.pages.page.cannotEdit.varMissing: Não é possível editar a página, falt
panel.pages.page.cannotGetFileInfo.fileNotFound: Não é possível obter informações do arquivo, arquivo não encontrado
panel.pages.page.cannotGetFileInfo.pageNotFound: Não é possível obter informações do arquivo, página não encontrada
panel.pages.page.cannotMove: Não é possível mover a página
panel.pages.page.cannotPreview.pageNotFound: Não é possível pré-visualizar a página, página não encontrada
panel.pages.page.cannotPreview.parentChanged: Não é possível pré-visualizar a página, a página mãe foi alterada
panel.pages.page.cannotRenameFile.fileAlreadyExists: Não é possível renomear o arquivo, um arquivo com o mesmo nome já existe
panel.pages.page.cannotRenameFile.fileNotFound: Não é possível renomear o arquivo, arquivo não encontrado
panel.pages.page.cannotRenameFile.pageNotFound: Não é possível renomear o arquivo, página não encontrada
@ -263,10 +267,12 @@ panel.pages.preview: Preview
panel.pages.previewFile: Preview
panel.pages.previous: Página anterior
panel.pages.previousFile: Ficheiro anterior
panel.pages.publish: Publicar
panel.pages.renameFile: Renomear arquivo
panel.pages.renameFile.name: Nome
panel.pages.replaceFile: Substituir ficheiro
panel.pages.save: Guardar
panel.pages.saveOnly: Guardar sem publicar
panel.pages.status.notPublished: Não publicado
panel.pages.status.notRoutable: Não roteável
panel.pages.status.published: Publicado
@ -275,6 +281,7 @@ panel.pages.summary: Resumo
panel.pages.template: Template
panel.pages.text: Texto
panel.pages.toggleChildren: Expandir páginas
panel.pages.viewPage: Ver página
panel.panel: Painel de administração
panel.register.createUser: O Formwork está instalado, mas nenhum utilizador foi encontrado. Por favor, registre um agora.
panel.register.register: Registrar novo utilizador

View File

@ -188,6 +188,8 @@ panel.pages.file.info.uri: URI
panel.pages.file.position: Позиция
panel.pages.file.preview: Просмотр
panel.pages.files: Файлы
panel.pages.history.event.created: Страница создана %s %s.
panel.pages.history.event.edited: Страница изменена %s %s.
panel.pages.languages: Языки
panel.pages.languages.addLanguage: Добавлять %s
panel.pages.languages.editLanguage: Редактировать %s
@ -227,6 +229,8 @@ panel.pages.page.cannotEdit.varMissing: Не могу редактировать
panel.pages.page.cannotGetFileInfo.fileNotFound: Невозможно получить информацию о файле, файл не найден
panel.pages.page.cannotGetFileInfo.pageNotFound: Невозможно получить информацию о файле, страница не найдена
panel.pages.page.cannotMove: Невозможно переместить страницу
panel.pages.page.cannotPreview.pageNotFound: Невозможно показать предварительный просмотр, страница не найдена
panel.pages.page.cannotPreview.parentChanged: Невозможно показать предварительный просмотр, родительская страница изменена
panel.pages.page.cannotRenameFile.fileAlreadyExists: Невозможно переименовать файл, файл с таким именем уже существует
panel.pages.page.cannotRenameFile.fileNotFound: Невозможно переименовать файл, файл не найден
panel.pages.page.cannotRenameFile.pageNotFound: Невозможно переименовать файл, страница не найдена
@ -263,10 +267,12 @@ panel.pages.preview: Предварительный просмотр
panel.pages.previewFile: Предварительный просмотр
panel.pages.previous: Предыдущая страница
panel.pages.previousFile: Предыдущий файл
panel.pages.publish: Опубликовать
panel.pages.renameFile: Переименовать файл
panel.pages.renameFile.name: Имя
panel.pages.replaceFile: Заменить файл
panel.pages.save: Сохранить
panel.pages.saveOnly: Сохранить без публикации
panel.pages.status.notPublished: Не Опубликовано
panel.pages.status.notRoutable: Не маршрутизируемый
panel.pages.status.published: Опубликованный
@ -275,6 +281,7 @@ panel.pages.summary: Сводка
panel.pages.template: Шаблон
panel.pages.text: Текст
panel.pages.toggleChildren: Переключить дочерние страницы
panel.pages.viewPage: Просмотреть страницу
panel.panel: Панель администрирования
panel.register.createUser: Formwork Администратор установлен, но не было найдено ни одного пользователя. Пожалуйста зарегистрировать пользователя в настоящее время.
panel.register.register: Регистрация нового пользователя

View File

@ -1,5 +1,6 @@
<?php $this->layout('panel') ?>
<form method="post" data-form="page-editor-form" enctype="multipart/form-data">
<input type="submit" <?= $this->attr(['hidden' => true, 'aria-hidden' => 'true', 'data-command' => 'save', 'formaction' => $history?->isJustCreated() ? '?publish=false' : null]) ?>>
<div class="header">
<div class="min-w-0 flex-grow-1">
<div class="header-title"><?= $this->icon($page->get('icon', 'page')) ?> <?= $this->escape($page->title()) ?></div>
@ -21,7 +22,8 @@
<div>
<a class="<?= $this->classes(['button', 'button-link', 'show-from-md', 'disabled' => !$page->previousSibling()]) ?>" role="button" <?php if ($page->previousSibling()) : ?>href="<?= $panel->uri('/pages/' . trim($page->previousSibling()->route(), '/') . '/edit/') ?>" <?php endif ?> title="<?= $this->translate('panel.pages.previous') ?>" aria-label="<?= $this->translate('panel.pages.previous') ?>"><?= $this->icon('chevron-left') ?></a>
<a class="<?= $this->classes(['button', 'button-link', 'show-from-md', 'disabled' => !$page->nextSibling()]) ?>" role="button" <?php if ($page->nextSibling()) : ?>href="<?= $panel->uri('/pages/' . trim($page->nextSibling()->route(), '/') . '/edit/') ?>" <?php endif ?> title="<?= $this->translate('panel.pages.next') ?>" aria-label="<?= $this->translate('panel.pages.next') ?>"><?= $this->icon('chevron-right') ?></a>
<a class="<?= $this->classes(['button', 'button-link', 'disabled' => !$page->published() || !$page->routable()]) ?>" role="button" <?php if ($page->published() && $page->routable()) : ?>href="<?= $page->uri(includeLanguage: $currentLanguage ?: true) ?>" <?php endif ?> target="formwork-preview-<?= $page->uid() ?>" title="<?= $this->translate('panel.pages.preview') ?>" aria-label="<?= $this->translate('panel.pages.preview') ?>"><?= $this->icon('eye') ?></a>
<a class="<?= $this->classes(['button', 'button-link', 'disabled' => !$page->published() || !$page->routable()]) ?>" role="button" <?php if ($page->published() && $page->routable()) : ?>href="<?= $page->uri(includeLanguage: $currentLanguage ?: true) ?>" <?php endif ?> target="formwork-view-page-<?= $page->uid() ?>" title="<?= $this->translate('panel.pages.viewPage') ?>" aria-label="<?= $this->translate('panel.pages.viewPage') ?>"><?= $this->icon('arrow-right-up-box') ?></a>
<button type="submit" class="<?= $this->classes(['button', 'button-link']) ?>" data-command="preview" formaction="<?= $panel->uri('/pages/' . trim($page->route(), '/') . '/preview/') ?>" formtarget=" formwork-preview-<?= $page->uid() ?>" title="<?= $this->translate('panel.pages.preview') ?>" aria-label="<?= $this->translate('panel.pages.preview') ?>"><?= $this->icon('eye') ?></button>
<?php if ($panel->user()->permissions()->has('pages.delete')) : ?>
<button type="button" class="button button-link" data-modal="deletePageModal" data-modal-action="<?= $panel->uri('/pages/' . trim($page->route(), '/') . '/delete/' . ($currentLanguage ? 'language/' . $currentLanguage . '/' : '')) ?>" title="<?= $this->translate('panel.pages.deletePage') ?>" aria-label="<?= $this->translate('panel.pages.deletePage') ?>" <?php if (!$page->isDeletable()) : ?> disabled<?php endif ?>><?= $this->icon('trash') ?></button>
<?php endif ?>
@ -35,11 +37,32 @@
</div>
</div>
<?php endif ?>
<button type="submit" class="button button-accent mb-0" data-command="save"><?= $this->icon('check-circle') ?> <?= $this->translate('panel.pages.save') ?></button>
<?php if ($history?->isJustCreated()): ?>
<div class="dropdown mb-0">
<div class="button-group">
<button type="submit" class="button button-accent" formaction="?publish=true"><?= $this->icon('check-circle') ?> <?= $this->translate('panel.pages.publish') ?></button>
<button type="button" class="button button-accent dropdown-button caret" data-dropdown="dropdown-save-options"></button>
</div>
<div class="dropdown-menu" id="dropdown-save-options">
<button type="submit" class="dropdown-item" formaction="?publish=false"><?= $this->translate('panel.pages.saveOnly') ?></button>
</div>
</div>
<?php else: ?>
<button type="submit" class="button button-accent mb-0"><?= $this->icon('check-circle') ?> <?= $this->translate('panel.pages.save') ?></button>
<?php endif ?>
</div>
</div>
<div>
<?php $this->insert('fields', ['fields' => $fields]) ?>
</div>
<input type="hidden" name="csrf-token" value="<?= $csrfToken ?>">
<?php if ($history !== null && !$history->items()->isEmpty()): ?>
<div class="text-size-sm text-color-gray-medium"><?= $this->icon('clock-rotate-left') ?>
<?= $this->translate(
'panel.pages.history.event.' . $history->lastItem()->event()->value,
'<a href="' . $panel->uri('/users/' . $history->lastItem()->user() . '/profile/') . '">' . $history->lastItem()->user() . '</a>',
'<span title="' . $this->datetime($history->lastItem()->time()) . '">' . $this->timedistance($history->lastItem()->time()) . '</span>'
) ?>
</div>
<?php endif ?>
</form>

View File

@ -52,7 +52,7 @@
<span class="page-status-label"><?= $this->translate('panel.pages.status.' . $page->status()) ?></span>
</div>
<div class="pages-tree-item-cell page-actions">
<a class="<?= $this->classes(['button', 'button-link', 'disabled' => !$page->published() || !$page->routable()]) ?>" role="button" <?php if ($page->published() && $page->routable()) : ?>href="<?= $page->uri(includeLanguage: false) ?>" <?php endif ?> target="formwork-preview-<?= $page->uid() ?>" title="<?= $this->translate('panel.pages.preview') ?>" aria-label="<?= $this->translate('panel.pages.preview') ?>"><?= $this->icon('eye') ?></a>
<a class="<?= $this->classes(['button', 'button-link', 'disabled' => !$page->published() || !$page->routable()]) ?>" role="button" <?php if ($page->published() && $page->routable()) : ?>href="<?= $page->uri(includeLanguage: false) ?>" <?php endif ?> target="formwork-view-page-<?= $page->uid() ?>" title="<?= $this->translate('panel.pages.viewPage') ?>" aria-label="<?= $this->translate('panel.pages.viewPage') ?>"><?= $this->icon('arrow-right-up-box') ?></a>
<?php if ($panel->user()->permissions()->has('pages.delete')) : ?>
<button type="button" class="button button-link" data-modal="deletePageModal" data-modal-action="<?= $panel->uri('/pages/' . trim($page->route(), '/') . '/delete/') ?>" title="<?= $this->translate('panel.pages.deletePage') ?>" aria-label="<?= $this->translate('panel.pages.deletePage') ?>" <?php if (!$page->isDeletable()) : ?> disabled<?php endif ?>><?= $this->icon('trash') ?></button>
<?php endif ?>