mirror of
https://github.com/getformwork/formwork.git
synced 2025-01-17 21:49:04 +01:00
Add content history
This commit is contained in:
parent
1acd27a3d5
commit
85550ca4be
68
formwork/src/Panel/ContentHistory/ContentHistory.php
Normal file
68
formwork/src/Panel/ContentHistory/ContentHistory.php
Normal 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));
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Formwork\Panel\ContentHistory;
|
||||
|
||||
enum ContentHistoryEvent: string
|
||||
{
|
||||
case Created = 'created';
|
||||
case Edited = 'edited';
|
||||
}
|
54
formwork/src/Panel/ContentHistory/ContentHistoryItem.php
Normal file
54
formwork/src/Panel/ContentHistory/ContentHistoryItem.php
Normal 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']
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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é
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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: Опубликованный
|
||||
|
@ -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>
|
Loading…
x
Reference in New Issue
Block a user