Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ten-tables-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@blinkk/root-cms': patch
---

feat: add release archiving and status filters
45 changes: 45 additions & 0 deletions docs/scripts/fix_release_timestamps.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {loadRootConfig} from '@blinkk/root/node';
import {RootCMSClient} from '@blinkk/root-cms';
import {Timestamp} from 'firebase-admin/firestore';

async function main() {
const rootConfig = await loadRootConfig(process.cwd());
const client = new RootCMSClient(rootConfig);
const db = client.db;
const releases = await db.collection('Projects/www/Releases').get();

for (const doc of releases.docs) {
const data = doc.data();
let changed = false;

// Check createdAt field
if (data.createdAt && !(data.createdAt instanceof Timestamp)) {
if (data.createdAt._seconds && data.createdAt._nanoseconds) {
data.createdAt = new Timestamp(
data.createdAt._seconds,
data.createdAt._nanoseconds
);
changed = true;
}
}

// Check publishedAt field
if (data.publishedAt && !(data.publishedAt instanceof Timestamp)) {
if (data.publishedAt._seconds && data.publishedAt._nanoseconds) {
data.publishedAt = new Timestamp(
data.publishedAt._seconds,
data.publishedAt._nanoseconds
);
changed = true;
}
}

if (changed) {
console.log(`Updating release ${doc.id}`);
await doc.ref.set(data);
}
}
console.log('Done');
}

main();
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,21 @@ export interface ReleaseStatusBadgeProps {

export function ReleaseStatusBadge(props: ReleaseStatusBadgeProps) {
const release = props.release;
if (release.scheduledAt) {
if (testIsValidTimestamp(release.archivedAt)) {
return (
<Tooltip
{...TOOLTIP_PROPS}
label={`Archived ${timeDiff(release.archivedAt)} by ${
release.archivedBy
}`}
>
<Badge size="xs" color="gray" variant="filled">
Archived
</Badge>
</Tooltip>
);
}
if (testIsValidTimestamp(release.scheduledAt)) {
return (
<Tooltip
{...TOOLTIP_PROPS}
Expand All @@ -33,7 +47,7 @@ export function ReleaseStatusBadge(props: ReleaseStatusBadgeProps) {
</Tooltip>
);
}
if (release.publishedAt) {
if (testIsValidTimestamp(release.publishedAt)) {
return (
<Tooltip
{...TOOLTIP_PROPS}
Expand All @@ -58,11 +72,15 @@ export function ReleaseStatusBadge(props: ReleaseStatusBadgeProps) {
);
}

function testIsValidTimestamp(ts: any): ts is Timestamp {
return Boolean(ts && ts.toMillis);
}

function timeDiff(ts: Timestamp | null) {
// Since we're using server timestamps, firestore doesn't always return the
// timestamp right away since the db save is happening asynchronously. In
// these cases, assume that the update happened very recently.
if (!ts) {
if (!ts?.toMillis) {
return getTimeAgo(new Date().getTime());
}
return getTimeAgo(ts.toMillis());
Expand Down
4 changes: 4 additions & 0 deletions packages/root-cms/ui/pages/ReleasePage/ReleasePage.css
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,7 @@
border-top: 1px solid var(--color-border);
padding: 4px 8px;
}

.ReleasePage__footer {
margin-top: 40px;
}
170 changes: 127 additions & 43 deletions packages/root-cms/ui/pages/ReleasePage/ReleasePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import {
} from '@mantine/core';
import {useModals} from '@mantine/modals';
import {showNotification, updateNotification} from '@mantine/notifications';
import {IconSettings} from '@tabler/icons-preact';
import {
IconArchive,
IconArchiveOff,
IconRestore,
IconSettings,
} from '@tabler/icons-preact';
import {useEffect, useState} from 'preact/hooks';
import {ConditionalTooltip} from '../../components/ConditionalTooltip/ConditionalTooltip.js';
import {DocPreviewCard} from '../../components/DocPreviewCard/DocPreviewCard.js';
Expand All @@ -26,6 +31,8 @@ import {
cancelScheduledRelease,
getRelease,
publishRelease,
archiveRelease,
unarchiveRelease,
} from '../../utils/release.js';
import {timestamp} from '../../utils/time.js';
import './ReleasePage.css';
Expand All @@ -36,6 +43,12 @@ export function ReleasePage(props: {id: string}) {
const [updated, setUpdated] = useState(0);
const id = props.id;

const modals = useModals();
const modalTheme = useModalTheme();
const {roles} = useProjectRoles();
const currentUserEmail = window.firebase.user.email || '';
const canPublish = testCanPublish(roles, currentUserEmail);

async function init() {
setLoading(true);
await notifyErrors(async () => {
Expand All @@ -55,6 +68,48 @@ export function ReleasePage(props: {id: string}) {
init();
}

function onArchiveClicked() {
modals.openConfirmModal({
...modalTheme,
title: `Archive release: ${id}`,
children: (
<Text size="body-sm" weight="semi-bold">
Are you sure you want to archive this release?
</Text>
),
labels: {confirm: 'Archive', cancel: 'Cancel'},
cancelProps: {size: 'xs'},
confirmProps: {color: 'dark', size: 'xs'},
closeOnConfirm: true,
onConfirm: async () => {
await notifyErrors(async () => {
await archiveRelease(id);
});
},
});
}

function onUnarchiveClicked() {
modals.openConfirmModal({
...modalTheme,
title: `Unarchive release: ${id}`,
children: (
<Text size="body-sm" weight="semi-bold">
Are you sure you want to unarchive this release?
</Text>
),
labels: {confirm: 'Unarchive', cancel: 'Cancel'},
cancelProps: {size: 'xs'},
confirmProps: {color: 'dark', size: 'xs'},
closeOnConfirm: true,
onConfirm: async () => {
await notifyErrors(async () => {
await unarchiveRelease(id);
});
},
});
}

return (
<Layout>
<div className="ReleasePage">
Expand Down Expand Up @@ -103,6 +158,34 @@ export function ReleasePage(props: {id: string}) {
key={`data-sources-${updated}`}
/>
)}
{release && canPublish && (
<div className="ReleasePage__footer">
{release.archivedAt ? (
<Button
className="ReleasePage__footer__unarchive"
variant="default"
size="xs"
leftIcon={<IconRestore size={16} />}
onClick={() => onUnarchiveClicked()}
disabled={!canPublish}
>
Unarchive Release
</Button>
) : (
<Button
className="ReleasePage__footer__archive"
variant="filled"
color="red"
size="xs"
leftIcon={<IconArchive size={16} />}
onClick={() => onArchiveClicked()}
disabled={!canPublish}
>
Archive Release
</Button>
)}
</div>
)}
</>
)}
</div>
Expand Down Expand Up @@ -220,7 +303,7 @@ ReleasePage.PublishStatus = (props: {
</td>
<td>
<div className="ReleasePage__PublishStatus__actions">
{!release.scheduledAt && (
{!release.archivedAt && !release.scheduledAt && (
<ConditionalTooltip
label="You don't have access to publish this release"
condition={!canPublish}
Expand All @@ -244,53 +327,54 @@ ReleasePage.PublishStatus = (props: {
</Tooltip>
</ConditionalTooltip>
)}
{release.scheduledAt ? (
<ConditionalTooltip
label="You don't have access to manage scheduled releases"
condition={!canPublish}
>
<Tooltip
label="Cancel the scheduled release"
position="bottom"
withArrow
disabled={!canPublish}
{!release.archivedAt &&
(release.scheduledAt ? (
<ConditionalTooltip
label="You don't have access to manage scheduled releases"
condition={!canPublish}
>
<Button
variant="default"
size="xs"
compact
onClick={() => onCancelScheduleClicked()}
<Tooltip
label="Cancel the scheduled release"
position="bottom"
withArrow
disabled={!canPublish}
>
Cancel Schedule
</Button>
</Tooltip>
</ConditionalTooltip>
) : (
<ConditionalTooltip
label="You don't have access to manage scheduled releases"
condition={!canPublish}
>
<Tooltip
label="Schedule the release to be published at a future date"
position="bottom"
withArrow
wrapLines
width={180}
disabled={!canPublish}
<Button
variant="default"
size="xs"
compact
onClick={() => onCancelScheduleClicked()}
disabled={!canPublish}
>
Cancel Schedule
</Button>
</Tooltip>
</ConditionalTooltip>
) : (
<ConditionalTooltip
label="You don't have access to manage scheduled releases"
condition={!canPublish}
>
<Button
variant="default"
size="xs"
compact
onClick={() => onScheduleClicked()}
<Tooltip
label="Schedule the release to be published at a future date"
position="bottom"
withArrow
wrapLines
width={180}
disabled={!canPublish}
>
Schedule
</Button>
</Tooltip>
</ConditionalTooltip>
)}
<Button
variant="default"
size="xs"
compact
onClick={() => onScheduleClicked()}
disabled={!canPublish}
>
Schedule
</Button>
</Tooltip>
</ConditionalTooltip>
))}
</div>
</td>
</tr>
Expand Down
6 changes: 6 additions & 0 deletions packages/root-cms/ui/pages/ReleasesPage/ReleasesPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,9 @@
.ReleasesPage__ReleasesTable__publishStatus__icon {
color: #12b886;
}

.ReleasesPage__ReleasesTable__filters {
display: flex;
justify-content: flex-end;
margin-bottom: 20px;
}
Loading