From a7a8927c1fc4ff491fca0a5ddc60cedf2a144021 Mon Sep 17 00:00:00 2001 From: Steven Le Date: Wed, 17 Dec 2025 08:12:37 -0800 Subject: [PATCH 1/2] feat: add release archiving --- .changeset/calm-sloths-archive-releases.md | 5 + .../ReleaseStatusBadge/ReleaseStatusBadge.tsx | 14 ++ .../ui/pages/ReleasePage/ReleasePage.tsx | 147 +++++++++++++----- .../ui/pages/ReleasesPage/ReleasesPage.css | 8 + .../ui/pages/ReleasesPage/ReleasesPage.tsx | 21 ++- packages/root-cms/ui/utils/release.ts | 53 ++++++- 6 files changed, 207 insertions(+), 41 deletions(-) create mode 100644 .changeset/calm-sloths-archive-releases.md diff --git a/.changeset/calm-sloths-archive-releases.md b/.changeset/calm-sloths-archive-releases.md new file mode 100644 index 000000000..8ba057912 --- /dev/null +++ b/.changeset/calm-sloths-archive-releases.md @@ -0,0 +1,5 @@ +--- +'@blinkk/root-cms': minor +--- + +feat: add release archiving controls in CMS diff --git a/packages/root-cms/ui/components/ReleaseStatusBadge/ReleaseStatusBadge.tsx b/packages/root-cms/ui/components/ReleaseStatusBadge/ReleaseStatusBadge.tsx index 3ff22559c..7e7932b32 100644 --- a/packages/root-cms/ui/components/ReleaseStatusBadge/ReleaseStatusBadge.tsx +++ b/packages/root-cms/ui/components/ReleaseStatusBadge/ReleaseStatusBadge.tsx @@ -13,6 +13,20 @@ export interface ReleaseStatusBadgeProps { export function ReleaseStatusBadge(props: ReleaseStatusBadgeProps) { const release = props.release; + if (release.archivedAt) { + return ( + + + Archived + + + ); + } if (release.scheduledAt) { return ( { const release = props.release; const [publishLoading, setPublishLoading] = useState(false); + const [archiveLoading, setArchiveLoading] = useState(false); const modals = useModals(); const modalTheme = useModalTheme(); @@ -196,6 +199,40 @@ ReleasePage.PublishStatus = (props: { props.onAction('cancel-schedule'); } + function onArchiveClicked() { + const modalId = modals.openConfirmModal({ + ...modalTheme, + title: `Archive release: ${release.id}`, + children: ( + + Archived releases will be hidden from the active list and cannot be + published or scheduled until they are unarchived. + + ), + labels: {confirm: 'Archive', cancel: 'Cancel'}, + cancelProps: {size: 'xs'}, + confirmProps: {color: 'red', size: 'xs'}, + onConfirm: async () => { + setArchiveLoading(true); + await notifyErrors(async () => { + await archiveRelease(release.id); + props.onAction('archive'); + }); + setArchiveLoading(false); + modals.closeModal(modalId); + }, + }); + } + + async function onUnarchiveClicked() { + setArchiveLoading(true); + await notifyErrors(async () => { + await unarchiveRelease(release.id); + props.onAction('unarchive'); + }); + setArchiveLoading(false); + } + return (
Status @@ -213,53 +250,93 @@ ReleasePage.PublishStatus = (props: {
- {!release.scheduledAt && ( - - + + )} + {release.scheduledAt ? ( + + + + ) : ( + + + + )} + - {release.publishedAt ? 'Re-publish' : 'Publish'} - - + + + )} - {release.scheduledAt ? ( - - - - ) : ( + {release.archivedAt && ( )} diff --git a/packages/root-cms/ui/pages/ReleasesPage/ReleasesPage.css b/packages/root-cms/ui/pages/ReleasesPage/ReleasesPage.css index f85c6708c..fde60e7d9 100644 --- a/packages/root-cms/ui/pages/ReleasesPage/ReleasesPage.css +++ b/packages/root-cms/ui/pages/ReleasesPage/ReleasesPage.css @@ -28,3 +28,11 @@ .ReleasesPage__ReleasesTable__publishStatus__icon { color: #12b886; } + +.ReleasesPage__ReleasesTable__controls { + margin-top: 16px; +} + +.ReleasesPage__row--archived { + opacity: 0.6; +} diff --git a/packages/root-cms/ui/pages/ReleasesPage/ReleasesPage.tsx b/packages/root-cms/ui/pages/ReleasesPage/ReleasesPage.tsx index ed2d111f1..5b40242f6 100644 --- a/packages/root-cms/ui/pages/ReleasesPage/ReleasesPage.tsx +++ b/packages/root-cms/ui/pages/ReleasesPage/ReleasesPage.tsx @@ -1,4 +1,4 @@ -import {Button, Loader, Table} from '@mantine/core'; +import {Button, Loader, Switch, Table} from '@mantine/core'; import {useEffect, useState} from 'preact/hooks'; import {Heading} from '../../components/Heading/Heading.js'; import {ReleaseStatusBadge} from '../../components/ReleaseStatusBadge/ReleaseStatusBadge.js'; @@ -36,16 +36,18 @@ export function ReleasesPage() { ReleasesPage.ReleasesTable = () => { const [loading, setLoading] = useState(true); const [tableData, setTableData] = useState([]); + const [includeArchived, setIncludeArchived] = useState(false); async function init() { - const releases = await listReleases(); + setLoading(true); + const releases = await listReleases({includeArchived}); setTableData(releases); setLoading(false); } useEffect(() => { init(); - }, []); + }, [includeArchived]); return (
@@ -62,7 +64,10 @@ ReleasesPage.ReleasesTable = () => { {tableData.map((release) => ( - + {release.id} @@ -84,6 +89,14 @@ ReleasesPage.ReleasesTable = () => { )} +
+ setIncludeArchived(event.currentTarget.checked)} + label="Show archived" + /> +
); }; diff --git a/packages/root-cms/ui/utils/release.ts b/packages/root-cms/ui/utils/release.ts index 1abdd25c5..2e0b96eaa 100644 --- a/packages/root-cms/ui/utils/release.ts +++ b/packages/root-cms/ui/utils/release.ts @@ -28,6 +28,8 @@ export interface Release { scheduledBy?: string; publishedAt?: Timestamp; publishedBy?: string; + archivedAt?: Timestamp; + archivedBy?: string; } const COLLECTION_ID = 'Releases'; @@ -54,7 +56,9 @@ export async function addRelease(id: string, release: Partial) { logAction('release.create', {metadata: {releaseId: id}}); } -export async function listReleases(): Promise { +export async function listReleases( + options: {includeArchived?: boolean} = {} +): Promise { const projectId = window.__ROOT_CTX.rootConfig.projectId; const db = window.firebase.db; const colRef = collection(db, 'Projects', projectId, COLLECTION_ID); @@ -64,7 +68,10 @@ export async function listReleases(): Promise { querySnapshot.forEach((doc) => { res.push(doc.data() as Release); }); - return res; + if (options.includeArchived) { + return res; + } + return res.filter((release) => !release.archivedAt); } export async function getRelease(id: string) { @@ -100,6 +107,9 @@ export async function publishRelease(id: string) { if (!release) { throw new Error(`release not found: ${id}`); } + if (release.archivedAt) { + throw new Error(`release is archived: ${id}`); + } const docIds = release.docIds || []; const dataSourceIds = release.dataSourceIds || []; if (docIds.length === 0 && dataSourceIds.length === 0) { @@ -138,6 +148,9 @@ export async function scheduleRelease( if (!release) { throw new Error(`release not found: ${id}`); } + if (release.archivedAt) { + throw new Error(`release is archived: ${id}`); + } if (typeof timestamp === 'number') { timestamp = Timestamp.fromMillis(timestamp); @@ -172,3 +185,39 @@ export async function cancelScheduledRelease(id: string) { }); logAction('release.unschedule', {metadata: {releaseId: id}}); } + +export async function archiveRelease(id: string) { + const release = await getRelease(id); + if (!release) { + throw new Error(`release not found: ${id}`); + } + + const projectId = window.__ROOT_CTX.rootConfig.projectId; + const db = window.firebase.db; + const docRef = doc(db, 'Projects', projectId, COLLECTION_ID, id); + + await updateDoc(docRef, { + archivedAt: serverTimestamp(), + archivedBy: window.firebase.user.email, + scheduledAt: deleteField(), + scheduledBy: deleteField(), + }); + logAction('release.archive', {metadata: {releaseId: id}}); +} + +export async function unarchiveRelease(id: string) { + const release = await getRelease(id); + if (!release) { + throw new Error(`release not found: ${id}`); + } + + const projectId = window.__ROOT_CTX.rootConfig.projectId; + const db = window.firebase.db; + const docRef = doc(db, 'Projects', projectId, COLLECTION_ID, id); + + await updateDoc(docRef, { + archivedAt: deleteField(), + archivedBy: deleteField(), + }); + logAction('release.unarchive', {metadata: {releaseId: id}}); +} From 653e3c5aa22974d87ba21c3526bea1a1724c7ce4 Mon Sep 17 00:00:00 2001 From: Steven Le Date: Wed, 17 Dec 2025 11:38:00 -0800 Subject: [PATCH 2/2] chore: update changeset --- .changeset/calm-sloths-archive-releases.md | 5 ----- .changeset/odd-parrots-kiss.md | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 .changeset/calm-sloths-archive-releases.md create mode 100644 .changeset/odd-parrots-kiss.md diff --git a/.changeset/calm-sloths-archive-releases.md b/.changeset/calm-sloths-archive-releases.md deleted file mode 100644 index 8ba057912..000000000 --- a/.changeset/calm-sloths-archive-releases.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@blinkk/root-cms': minor ---- - -feat: add release archiving controls in CMS diff --git a/.changeset/odd-parrots-kiss.md b/.changeset/odd-parrots-kiss.md new file mode 100644 index 000000000..fc9dd98ab --- /dev/null +++ b/.changeset/odd-parrots-kiss.md @@ -0,0 +1,5 @@ +--- +'@blinkk/root-cms': patch +--- + +feat: add release archiving (#872)