Add content history

This commit is contained in:
Giuseppe Criscione 2024-09-17 21:42:28 +02:00
parent 1acd27a3d5
commit 85550ca4be
13 changed files with 227 additions and 5 deletions

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,6 +241,7 @@ class PagesController extends AbstractController
'templates' => $this->site()->templates()->keys(),
'parents' => $this->site()->descendants()->sortBy('relativePath'),
'currentLanguage' => $routeParams->get('language', $page->language()?->code()),
'history' => $contentHistory,
]));
}
@ -547,13 +564,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 +619,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 +637,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();

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"
@ -263,10 +265,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

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
@ -263,10 +265,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

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
@ -263,10 +265,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

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
@ -263,10 +265,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é

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
@ -263,10 +265,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

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
@ -263,10 +265,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

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
@ -263,10 +265,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: Опубликованный

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>
@ -35,11 +36,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>