feat: stage edits to published articles as drafts (#14842)

This commit is contained in:
Sivin Varghese
2026-07-16 12:44:10 +05:30
committed by GitHub
parent f1a07657e9
commit 0fccb7dacd
21 changed files with 1280 additions and 59 deletions
@@ -4,8 +4,12 @@ import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useAlert, useTrack } from 'dashboard/composables';
import { PORTALS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { buildPortalArticleURL } from 'dashboard/helper/portalHelper';
import {
buildPortalArticleURL,
ARTICLE_STATUSES,
} from 'dashboard/helper/portalHelper';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { rendersIdentically } from 'dashboard/helper/articleDiffHelper';
import ArticleEditor from 'dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue';
@@ -40,13 +44,60 @@ const articleLink = computed(() => {
);
});
// On a published article, title/content edits stage into draft_* columns (kept
// off the live site). Anywhere else they save straight to the live record — and
// we drop any leftover draft (e.g. left behind when the card/bulk menu moved a
// published article to draft) so a later publish can't resurrect stale content.
const stageDraftFields = values => {
if (article.value?.status !== ARTICLE_STATUSES.PUBLISHED) {
const hasStaleDraft =
article.value?.draftTitle != null || article.value?.draftContent != null;
if (!hasStaleDraft) return values;
// The editor is showing the staged draft, so promote both fields to the live
// record (the field being autosaved wins) before dropping the drafts —
// otherwise saving one field would snap the other back to the old live value.
return {
...values,
title: values.title ?? article.value.draftTitle ?? article.value.title,
content:
values.content ?? article.value.draftContent ?? article.value.content,
draft_title: null,
draft_content: null,
};
}
const staged = { ...values };
['title', 'content'].forEach(field => {
if (field in staged) {
staged[`draft_${field}`] = staged[field];
delete staged[field];
}
});
// Clear the draft when it matches the live version (a revert, or a body edit
// the renderer ignores like a blank line) so it doesn't leave a "pending
// changes" badge with nothing to compare. The title is shown as raw escaped
// text, so compare it exactly; only the body is Markdown, so compare its render.
const liveTitle = article.value.title ?? '';
const liveContent = article.value.content ?? '';
const nextTitle = staged.draft_title ?? article.value.draftTitle ?? liveTitle;
const nextContent =
staged.draft_content ?? article.value.draftContent ?? liveContent;
if (nextTitle === liveTitle && rendersIdentically(liveContent, nextContent)) {
staged.draft_title = null;
staged.draft_content = null;
}
return staged;
};
const saveArticle = async ({ ...values }) => {
isUpdating.value = true;
try {
await store.dispatch('articles/update', {
portalSlug,
articleId: articleSlug,
...values,
...stageDraftFields(values),
});
isSaved.value = true;
} catch (error) {