feat #58 : wizard multi-étapes création/édition d'article
Remplace le formulaire unique par un wizard 5 étapes (création) et
6 étapes (édition) avec auto-sauvegarde en brouillon, détection de
tags depuis le texte (TagSuggester), aperçu SEO, diff avant validation
et plan Markdown dynamique dans l'éditeur.
Détail des changements :
- ArticleManager : +6 méthodes (updatePartialMeta, saveDraftOverlay,
getDraftOverlay, hasDraftOverlay, discardDraftOverlay, commitDraftOverlay)
- .htaccess : routes /new/{uuid}/{1-5} et /edit/{uuid}/{1-6}
- index.php : cases create et edit réécrits en switch($step),
nouveau case autosave_draft et edit_discard_draft
- assets/js/wizard.js : autosave debounce, auto-resize textarea,
scroll curseur, plan TOC dynamique, toggle pills tags
- templates/wizard/ : nav.php + step1..6.php
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -228,6 +228,126 @@ class ArticleManager
|
||||
return true;
|
||||
}
|
||||
|
||||
public function updatePartialMeta(string $uuid, array $updates): void
|
||||
{
|
||||
if (!$this->isValidUuid($uuid)) {
|
||||
return;
|
||||
}
|
||||
$dir = $this->dataDir . '/' . $uuid;
|
||||
$raw = @file_get_contents($dir . '/meta.json');
|
||||
if ($raw === false) {
|
||||
return;
|
||||
}
|
||||
$meta = json_decode($raw, true);
|
||||
if (!is_array($meta)) {
|
||||
return;
|
||||
}
|
||||
foreach ($updates as $key => $value) {
|
||||
$meta[$key] = $value;
|
||||
}
|
||||
$meta['updated_at'] = date('Y-m-d H:i:s');
|
||||
$this->writeMeta($dir, $meta);
|
||||
}
|
||||
|
||||
public function saveDraftOverlay(string $uuid, array $metaFields, ?string $content = null): void
|
||||
{
|
||||
if (!$this->isValidUuid($uuid)) {
|
||||
return;
|
||||
}
|
||||
$dir = $this->dataDir . '/' . $uuid;
|
||||
$existing = [];
|
||||
$raw = @file_get_contents($dir . '/draft_overlay.json');
|
||||
if ($raw !== false) {
|
||||
$existing = json_decode($raw, true) ?? [];
|
||||
}
|
||||
$overlay = array_merge($existing, $metaFields);
|
||||
$overlay['_updated_at'] = date('Y-m-d H:i:s');
|
||||
file_put_contents(
|
||||
$dir . '/draft_overlay.json',
|
||||
json_encode($overlay, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n"
|
||||
);
|
||||
if ($content !== null) {
|
||||
file_put_contents($dir . '/draft_overlay.md', $content);
|
||||
}
|
||||
}
|
||||
|
||||
public function getDraftOverlay(string $uuid): ?array
|
||||
{
|
||||
if (!$this->isValidUuid($uuid)) {
|
||||
return null;
|
||||
}
|
||||
$dir = $this->dataDir . '/' . $uuid;
|
||||
if (!file_exists($dir . '/draft_overlay.json')) {
|
||||
return null;
|
||||
}
|
||||
$article = $this->getByUuid($uuid);
|
||||
if (!$article) {
|
||||
return null;
|
||||
}
|
||||
$raw = file_get_contents($dir . '/draft_overlay.json');
|
||||
if ($raw === false) {
|
||||
return null;
|
||||
}
|
||||
$overlay = json_decode($raw, true);
|
||||
if (!is_array($overlay)) {
|
||||
return null;
|
||||
}
|
||||
$merged = $article;
|
||||
foreach ($overlay as $key => $value) {
|
||||
if (!str_starts_with($key, '_')) {
|
||||
$merged[$key] = $value;
|
||||
}
|
||||
}
|
||||
if (file_exists($dir . '/draft_overlay.md')) {
|
||||
$c = file_get_contents($dir . '/draft_overlay.md');
|
||||
if ($c !== false) {
|
||||
$merged['content'] = $c;
|
||||
}
|
||||
}
|
||||
return $merged;
|
||||
}
|
||||
|
||||
public function hasDraftOverlay(string $uuid): bool
|
||||
{
|
||||
if (!$this->isValidUuid($uuid)) {
|
||||
return false;
|
||||
}
|
||||
return file_exists($this->dataDir . '/' . $uuid . '/draft_overlay.json');
|
||||
}
|
||||
|
||||
public function discardDraftOverlay(string $uuid): void
|
||||
{
|
||||
if (!$this->isValidUuid($uuid)) {
|
||||
return;
|
||||
}
|
||||
$dir = $this->dataDir . '/' . $uuid;
|
||||
@unlink($dir . '/draft_overlay.json');
|
||||
@unlink($dir . '/draft_overlay.md');
|
||||
}
|
||||
|
||||
public function commitDraftOverlay(string $uuid, string $revisionComment = ''): void
|
||||
{
|
||||
$draft = $this->getDraftOverlay($uuid);
|
||||
if (!$draft) {
|
||||
return;
|
||||
}
|
||||
$this->update(
|
||||
$uuid,
|
||||
$draft['title'],
|
||||
$draft['content'],
|
||||
(bool)$draft['published'],
|
||||
$draft['slug'] ?? '',
|
||||
$draft['published_at'] ?? '',
|
||||
$revisionComment,
|
||||
$draft['seo_title'] ?? '',
|
||||
$draft['seo_description'] ?? '',
|
||||
$draft['og_image'] ?? '',
|
||||
$draft['category'] ?? '',
|
||||
$draft['tags'] ?? []
|
||||
);
|
||||
$this->discardDraftOverlay($uuid);
|
||||
}
|
||||
|
||||
public function addFileMeta(string $uuid, string $filename, string $author, string $sourceUrl, string $title = '', array $extraMeta = []): void
|
||||
{
|
||||
if (!$this->isValidUuid($uuid)) {
|
||||
|
||||
Reference in New Issue
Block a user