diff --git a/packages/core/dev-test/index.js b/packages/core/dev-test/index.js index e77b98f3..39e1a80b 100644 --- a/packages/core/dev-test/index.js +++ b/packages/core/dev-test/index.js @@ -11,7 +11,7 @@ const PostPreview = ({ entry, widgetFor }) => { ); }; -const PostPreviewCard = ({ entry, theme }) => { +const PostPreviewCard = ({ entry, theme, hasLocalBackup }) => { const date = new Date(entry.data.date); const month = date.getMonth() + 1; @@ -70,19 +70,53 @@ const PostPreviewCard = ({ entry, theme }) => { 'div', { style: { - backgroundColor: entry.data.draft === true ? 'blue' : 'green', - color: 'white', - border: 'none', - padding: '2px 6px', - textAlign: 'center', - textDecoration: 'none', - display: 'inline-block', - cursor: 'pointer', - borderRadius: '4px', - fontSize: '14px', + display: 'flex', + alignItems: 'center', + whiteSpace: 'no-wrap', + gap: '8px', }, }, - entry.data.draft === true ? 'Draft' : 'Published', + hasLocalBackup + ? h( + 'div', + { + style: { + border: '2px solid rgb(147, 197, 253)', + borderRadius: '50%', + color: 'rgb(147, 197, 253)', + height: '18px', + width: '18px', + fontWeight: 'bold', + fontSize: '11px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + textAlign: 'center', + }, + title: 'Has local backup' + }, + 'i', + ) + : null, + h( + 'div', + { + style: { + backgroundColor: + entry.data.draft === true ? 'rgb(37, 99, 235)' : 'rgb(22, 163, 74)', + color: 'white', + border: 'none', + padding: '2px 6px', + textAlign: 'center', + textDecoration: 'none', + display: 'inline-block', + cursor: 'pointer', + borderRadius: '4px', + fontSize: '14px', + }, + }, + entry.data.draft === true ? 'Draft' : 'Published', + ), ), ), ), diff --git a/packages/core/src/backend.ts b/packages/core/src/backend.ts index 119d4cb0..2259b2f9 100644 --- a/packages/core/src/backend.ts +++ b/packages/core/src/backend.ts @@ -27,6 +27,7 @@ import { getPathDepth, localForage, } from './lib/util'; +import { getEntryBackupKey } from './lib/util/backup.util'; import { selectAllowDeletion, selectAllowNewEntries, @@ -47,6 +48,7 @@ import createEntry from './valueObjects/createEntry'; import type { BackendClass, BackendInitializer, + BackupEntry, BaseField, Collection, CollectionFile, @@ -104,15 +106,6 @@ export class LocalStorageAuthStore { } } -function getEntryBackupKey(collectionName?: string, slug?: string) { - const baseKey = 'backup'; - if (!collectionName) { - return baseKey; - } - const suffix = slug ? `.${slug}` : ''; - return `${baseKey}.${collectionName}${suffix}`; -} - export function getEntryField(field: string, entry: Entry): string { const value = get(entry.data, field); if (value) { @@ -254,13 +247,6 @@ export interface MediaFile { isDirectory?: boolean; } -interface BackupEntry { - raw: string; - path: string; - mediaFiles: MediaFile[]; - i18n?: Record; -} - function collectionDepth(collection: Collection) { let depth; depth = diff --git a/packages/core/src/components/collections/entries/EntryCard.tsx b/packages/core/src/components/collections/entries/EntryCard.tsx index b97178ca..3b1a03ff 100644 --- a/packages/core/src/components/collections/entries/EntryCard.tsx +++ b/packages/core/src/components/collections/entries/EntryCard.tsx @@ -1,14 +1,17 @@ +import { Info as InfoIcon } from '@styled-icons/material-outlined/Info'; import get from 'lodash/get'; -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { VIEW_STYLE_LIST } from '@staticcms/core/constants/views'; import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset'; import { getFieldPreview, getPreviewCard } from '@staticcms/core/lib/registry'; +import { getEntryBackupKey } from '@staticcms/core/lib/util/backup.util'; import { selectEntryCollectionTitle, selectFields, selectTemplateName, } from '@staticcms/core/lib/util/collection.util'; +import localForage from '@staticcms/core/lib/util/localForage'; import { isNullish } from '@staticcms/core/lib/util/null.util'; import { selectConfig } from '@staticcms/core/reducers/selectors/config'; import { selectTheme } from '@staticcms/core/reducers/selectors/globalUI'; @@ -22,7 +25,15 @@ import TableRow from '../../common/table/TableRow'; import useWidgetsFor from '../../common/widget/useWidgetsFor'; import type { ViewStyle } from '@staticcms/core/constants/views'; -import type { Collection, Entry, FileOrImageField, MediaField } from '@staticcms/core/interface'; +import type { + BackupEntry, + Collection, + Entry, + FileOrImageField, + MediaField, + TranslatedProps, +} from '@staticcms/core/interface'; +import type { FC } from 'react'; export interface EntryCardProps { entry: Entry; @@ -33,14 +44,15 @@ export interface EntryCardProps { summaryFields: string[]; } -const EntryCard = ({ +const EntryCard: FC> = ({ collection, entry, collectionLabel, viewStyle = VIEW_STYLE_LIST, imageFieldName, summaryFields, -}: EntryCardProps) => { + t, +}) => { const entryData = entry.data; const path = useMemo( @@ -86,6 +98,31 @@ const EntryCard = ({ const theme = useAppSelector(selectTheme); + const [hasLocalBackup, setHasLocalBackup] = useState(false); + useEffect(() => { + let alive = true; + + const checkLocalBackup = async () => { + const key = getEntryBackupKey(collection.name, entry.slug); + const backup = await localForage.getItem(key); + + if (alive) { + setHasLocalBackup(Boolean(backup)); + } + }; + + checkLocalBackup(); + + // Check again after small delay to ensure we capture the draft just made from the editor + setTimeout(() => { + checkLocalBackup(); + }, 250); + + return () => { + alive = false; + }; + }, [collection.name, entry.slug]); + if (viewStyle === VIEW_STYLE_LIST) { return ( ); })} + + {hasLocalBackup ? ( + + ) : null} + ); } @@ -144,6 +194,7 @@ const EntryCard = ({ widgetFor={widgetFor} widgetsFor={widgetsFor} theme={theme} + hasLocalBackup={hasLocalBackup} /> @@ -154,7 +205,22 @@ const EntryCard = ({ {image && imageField ? : null} - {summary} + +
+
{summary}
+ {hasLocalBackup ? ( + + ) : null} +
+
); diff --git a/packages/core/src/components/collections/entries/EntryListing.tsx b/packages/core/src/components/collections/entries/EntryListing.tsx index e5bc00df..c8c83d41 100644 --- a/packages/core/src/components/collections/entries/EntryListing.tsx +++ b/packages/core/src/components/collections/entries/EntryListing.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useMemo } from 'react'; +import { translate } from 'react-polyglot'; import { Waypoint } from 'react-waypoint'; import { selectFields, selectInferredField } from '@staticcms/core/lib/util/collection.util'; @@ -7,8 +8,15 @@ import Table from '../../common/table/Table'; import EntryCard from './EntryCard'; import type { ViewStyle } from '@staticcms/core/constants/views'; -import type { Collection, Collections, Entry, Field } from '@staticcms/core/interface'; +import type { + Collection, + Collections, + Entry, + Field, + TranslatedProps, +} from '@staticcms/core/interface'; import type Cursor from '@staticcms/core/lib/util/Cursor'; +import type { FC } from 'react'; export interface BaseEntryListingProps { entries: Entry[]; @@ -30,14 +38,15 @@ export type EntryListingProps = | SingleCollectionEntryListingProps | MultipleCollectionEntryListingProps; -const EntryListing = ({ +const EntryListing: FC> = ({ entries, page, cursor, viewStyle, handleCursorActions, + t, ...otherProps -}: EntryListingProps) => { +}) => { const hasMore = useMemo(() => cursor?.actions?.has('append_next'), [cursor?.actions]); const handleLoadMore = useCallback(() => { @@ -95,6 +104,7 @@ const EntryListing = ({ entry={entry} key={entry.slug} summaryFields={summaryFields} + t={t} /> )); } @@ -116,10 +126,11 @@ const EntryListing = ({ collectionLabel={collectionLabel} key={entry.slug} summaryFields={summaryFields} + t={t} /> ) : null; }); - }, [entries, inferFields, isSingleCollectionInList, otherProps, summaryFields, viewStyle]); + }, [entries, inferFields, isSingleCollectionInList, otherProps, summaryFields, t, viewStyle]); const summaryFieldHeaders = useMemo(() => { if ('collection' in otherProps) { @@ -143,7 +154,9 @@ const EntryListing = ({ <> {renderedCards} @@ -171,4 +184,4 @@ const EntryListing = ({ ); }; -export default EntryListing; +export default translate()(EntryListing) as FC; diff --git a/packages/core/src/components/common/button/Button.tsx b/packages/core/src/components/common/button/Button.tsx index da98f9ed..3bdb8693 100644 --- a/packages/core/src/components/common/button/Button.tsx +++ b/packages/core/src/components/common/button/Button.tsx @@ -8,7 +8,7 @@ import type { CSSProperties, FC, MouseEventHandler, ReactNode, Ref } from 'react export interface BaseBaseProps { variant?: 'contained' | 'outlined' | 'text'; - color?: 'primary' | 'secondary' | 'success' | 'error'; + color?: 'primary' | 'secondary' | 'success' | 'error' | 'warning'; size?: 'medium' | 'small'; rounded?: boolean | 'no-padding'; className?: string; diff --git a/packages/core/src/components/common/button/useButtonClassNames.tsx b/packages/core/src/components/common/button/useButtonClassNames.tsx index fbb84c60..ea4a658c 100644 --- a/packages/core/src/components/common/button/useButtonClassNames.tsx +++ b/packages/core/src/components/common/button/useButtonClassNames.tsx @@ -11,18 +11,21 @@ const classes: Record< secondary: 'btn-contained-secondary', success: 'btn-contained-success', error: 'btn-contained-error', + warning: 'btn-contained-warning', }, outlined: { primary: 'btn-outlined-primary', secondary: 'btn-outlined-secondary', success: 'btn-outlined-success', error: 'btn-outlined-error', + warning: 'btn-outlined-warning', }, text: { primary: 'btn-text-primary', secondary: 'btn-text-secondary', success: 'btn-text-success', error: 'btn-text-error', + warning: 'btn-text-warning', }, }; diff --git a/packages/core/src/components/common/card/CardContent.tsx b/packages/core/src/components/common/card/CardContent.tsx index 51a4a831..50b89ead 100644 --- a/packages/core/src/components/common/card/CardContent.tsx +++ b/packages/core/src/components/common/card/CardContent.tsx @@ -7,7 +7,7 @@ interface CardContentProps { } const CardContent = ({ children }: CardContentProps) => { - return

{children}

; + return

{children}

; }; export default CardContent; diff --git a/packages/core/src/components/common/confirm/Confirm.tsx b/packages/core/src/components/common/confirm/Confirm.tsx index 85fa9770..c31290f0 100644 --- a/packages/core/src/components/common/confirm/Confirm.tsx +++ b/packages/core/src/components/common/confirm/Confirm.tsx @@ -13,7 +13,7 @@ interface ConfirmProps { body: string | { key: string; options?: Record }; cancel?: string | { key: string; options?: Record }; confirm?: string | { key: string; options?: Record }; - color?: 'success' | 'error' | 'primary'; + color?: 'success' | 'error' | 'warning' | 'primary'; } export interface ConfirmDialogProps extends ConfirmProps { @@ -120,7 +120,7 @@ const ConfirmDialog = ({ t }: TranslateProps) => { gap-2 " > -