feat: local backup enhancements (#714)
This commit is contained in:
parent
39bb9647b2
commit
804c09415b
@ -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,7 +70,40 @@ const PostPreviewCard = ({ entry, theme }) => {
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
backgroundColor: entry.data.draft === true ? 'blue' : 'green',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
whiteSpace: 'no-wrap',
|
||||
gap: '8px',
|
||||
},
|
||||
},
|
||||
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',
|
||||
@ -86,6 +119,7 @@ const PostPreviewCard = ({ entry, theme }) => {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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<string, { raw: string }>;
|
||||
}
|
||||
|
||||
function collectionDepth<EF extends BaseField>(collection: Collection<EF>) {
|
||||
let depth;
|
||||
depth =
|
||||
|
@ -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<TranslatedProps<EntryCardProps>> = ({
|
||||
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<BackupEntry>(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 (
|
||||
<TableRow
|
||||
@ -129,6 +166,19 @@ const EntryCard = ({
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
<TableCell key="unsavedChanges" to={path} shrink>
|
||||
{hasLocalBackup ? (
|
||||
<InfoIcon
|
||||
className="
|
||||
w-5
|
||||
h-5
|
||||
text-blue-600
|
||||
dark:text-blue-300
|
||||
"
|
||||
title={t('ui.localBackup.hasLocalBackup')}
|
||||
/>
|
||||
) : null}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@ -144,6 +194,7 @@ const EntryCard = ({
|
||||
widgetFor={widgetFor}
|
||||
widgetsFor={widgetsFor}
|
||||
theme={theme}
|
||||
hasLocalBackup={hasLocalBackup}
|
||||
/>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
@ -154,7 +205,22 @@ const EntryCard = ({
|
||||
<Card>
|
||||
<CardActionArea to={path}>
|
||||
{image && imageField ? <CardMedia height="140" image={imageUrl} /> : null}
|
||||
<CardContent>{summary}</CardContent>
|
||||
<CardContent>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div>{summary}</div>
|
||||
{hasLocalBackup ? (
|
||||
<InfoIcon
|
||||
className="
|
||||
w-5
|
||||
h-5
|
||||
text-blue-600
|
||||
dark:text-blue-300
|
||||
"
|
||||
title={t('ui.localBackup.hasLocalBackup')}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
);
|
||||
|
@ -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<TranslatedProps<EntryListingProps>> = ({
|
||||
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 = ({
|
||||
<>
|
||||
<Table
|
||||
columns={
|
||||
!isSingleCollectionInList ? ['Collection', ...summaryFieldHeaders] : summaryFieldHeaders
|
||||
!isSingleCollectionInList
|
||||
? ['Collection', ...summaryFieldHeaders, '']
|
||||
: [...summaryFieldHeaders, '']
|
||||
}
|
||||
>
|
||||
{renderedCards}
|
||||
@ -171,4 +184,4 @@ const EntryListing = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default EntryListing;
|
||||
export default translate()(EntryListing) as FC<EntryListingProps>;
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -7,7 +7,7 @@ interface CardContentProps {
|
||||
}
|
||||
|
||||
const CardContent = ({ children }: CardContentProps) => {
|
||||
return <p className="p-5 font-normal text-gray-700 dark:text-gray-300">{children}</p>;
|
||||
return <p className="w-full p-5 font-normal text-gray-700 dark:text-gray-300">{children}</p>;
|
||||
};
|
||||
|
||||
export default CardContent;
|
||||
|
@ -13,7 +13,7 @@ interface ConfirmProps {
|
||||
body: string | { key: string; options?: Record<string, unknown> };
|
||||
cancel?: string | { key: string; options?: Record<string, unknown> };
|
||||
confirm?: string | { key: string; options?: Record<string, unknown> };
|
||||
color?: 'success' | 'error' | 'primary';
|
||||
color?: 'success' | 'error' | 'warning' | 'primary';
|
||||
}
|
||||
|
||||
export interface ConfirmDialogProps extends ConfirmProps {
|
||||
@ -120,7 +120,7 @@ const ConfirmDialog = ({ t }: TranslateProps) => {
|
||||
gap-2
|
||||
"
|
||||
>
|
||||
<Button onClick={handleCancel} variant="text">
|
||||
<Button onClick={handleCancel} variant="text" color="secondary">
|
||||
{cancel}
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} variant="contained" color={color}>
|
||||
|
@ -13,7 +13,7 @@ export interface MenuItemButtonProps {
|
||||
disabled?: boolean;
|
||||
startIcon?: FC<{ className?: string }>;
|
||||
endIcon?: FC<{ className?: string }>;
|
||||
color?: 'default' | 'error';
|
||||
color?: 'default' | 'error' | 'warning';
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
@ -46,19 +46,32 @@ const MenuItemButton = ({
|
||||
items-center
|
||||
justify-between
|
||||
cursor-pointer
|
||||
hover:bg-gray-200
|
||||
dark:hover:bg-slate-600
|
||||
dark:disabled:text-gray-700
|
||||
`,
|
||||
color === 'default' &&
|
||||
`
|
||||
text-gray-700
|
||||
dark:text-gray-300
|
||||
hover:bg-gray-200
|
||||
dark:hover:bg-slate-600
|
||||
`,
|
||||
color === 'warning' &&
|
||||
`
|
||||
text-yellow-600
|
||||
dark:text-yellow-500
|
||||
hover:text-white
|
||||
hover:bg-yellow-500
|
||||
dark:hover:text-yellow-100
|
||||
dark:hover:bg-yellow-600
|
||||
`,
|
||||
color === 'error' &&
|
||||
`
|
||||
text-red-500
|
||||
dark:text-red-500
|
||||
hover:text-white
|
||||
hover:bg-red-500
|
||||
dark:hover:text-red-100
|
||||
dark:hover:bg-red-600
|
||||
`,
|
||||
),
|
||||
},
|
||||
|
@ -9,9 +9,10 @@ interface TableCellProps {
|
||||
children: ReactNode;
|
||||
emphasis?: boolean;
|
||||
to?: string;
|
||||
shrink?: boolean;
|
||||
}
|
||||
|
||||
const TableCell = ({ children, emphasis = false, to }: TableCellProps) => {
|
||||
const TableCell = ({ children, emphasis = false, to, shrink = false }: TableCellProps) => {
|
||||
const content = useMemo(() => {
|
||||
if (to) {
|
||||
return (
|
||||
@ -39,6 +40,7 @@ const TableCell = ({ children, emphasis = false, to }: TableCellProps) => {
|
||||
!to ? 'px-4 py-3' : 'p-0',
|
||||
'text-gray-500 dark:text-gray-300',
|
||||
emphasis && 'font-medium text-gray-900 whitespace-nowrap dark:text-white',
|
||||
shrink && 'w-0',
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { createHashHistory } from 'history';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { connect } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
import {
|
||||
createDraftDuplicateFromEntry,
|
||||
@ -21,8 +21,9 @@ import {
|
||||
import { loadScroll, toggleScroll } from '@staticcms/core/actions/scroll';
|
||||
import { selectFields } from '@staticcms/core/lib/util/collection.util';
|
||||
import { useWindowEvent } from '@staticcms/core/lib/util/window.util';
|
||||
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
|
||||
import { selectEntry } from '@staticcms/core/reducers/selectors/entries';
|
||||
import { useAppDispatch } from '@staticcms/core/store/hooks';
|
||||
import { useAppDispatch, useAppSelector } from '@staticcms/core/store/hooks';
|
||||
import confirm from '../common/confirm/Confirm';
|
||||
import Loader from '../common/progress/Loader';
|
||||
import MediaLibraryModal from '../media-library/MediaLibraryModal';
|
||||
@ -61,23 +62,32 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const config = useAppSelector(selectConfig);
|
||||
|
||||
const createBackup = useMemo(
|
||||
() =>
|
||||
debounce(function (entry: Entry, collection: Collection) {
|
||||
if (config?.disable_local_backup) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(persistLocalBackup(entry, collection));
|
||||
}, 2000),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
[config],
|
||||
);
|
||||
|
||||
const deleteBackup = useCallback(() => {
|
||||
if (config?.disable_local_backup) {
|
||||
return;
|
||||
}
|
||||
|
||||
createBackup.cancel();
|
||||
if (slug) {
|
||||
dispatch(deleteLocalBackup(collection, slug));
|
||||
}
|
||||
dispatch(deleteDraftLocalBackup());
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [collection, createBackup, slug]);
|
||||
}, [config?.disable_local_backup, createBackup, slug, dispatch, collection]);
|
||||
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const handlePersistEntry = useCallback(
|
||||
@ -166,6 +176,10 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
|
||||
>();
|
||||
|
||||
useEffect(() => {
|
||||
if (config?.disable_local_backup) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!prevLocalBackup &&
|
||||
localBackup &&
|
||||
@ -173,17 +187,8 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
|
||||
!isEqual(localBackup.entry.meta, entryDraft.entry?.meta))
|
||||
) {
|
||||
const updateLocalBackup = async () => {
|
||||
const confirmLoadBackupBody = await confirm({
|
||||
title: 'editor.editor.confirmLoadBackupTitle',
|
||||
body: 'editor.editor.confirmLoadBackupBody',
|
||||
});
|
||||
|
||||
if (confirmLoadBackupBody) {
|
||||
dispatch(loadLocalBackup());
|
||||
setVersion(version + 1);
|
||||
} else {
|
||||
deleteBackup();
|
||||
}
|
||||
};
|
||||
|
||||
updateLocalBackup();
|
||||
@ -191,6 +196,7 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
|
||||
|
||||
setPrevLocalBackup(localBackup);
|
||||
}, [
|
||||
config?.disable_local_backup,
|
||||
deleteBackup,
|
||||
dispatch,
|
||||
entryDraft.entry?.data,
|
||||
@ -219,14 +225,25 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
|
||||
});
|
||||
} else if (!newRecord && slug && (prevCollection !== collection || prevSlug !== slug)) {
|
||||
setTimeout(() => {
|
||||
if (!config?.disable_local_backup) {
|
||||
dispatch(retrieveLocalBackup(collection, slug));
|
||||
}
|
||||
dispatch(loadEntry(collection, slug));
|
||||
});
|
||||
}
|
||||
|
||||
setPrevCollection(collection);
|
||||
setPrevSlug(slug);
|
||||
}, [collection, entryDraft.entry, prevSlug, prevCollection, slug, dispatch, newRecord]);
|
||||
}, [
|
||||
collection,
|
||||
entryDraft.entry,
|
||||
prevSlug,
|
||||
prevCollection,
|
||||
slug,
|
||||
dispatch,
|
||||
newRecord,
|
||||
config?.disable_local_backup,
|
||||
]);
|
||||
|
||||
const leaveMessage = useMemo(() => t('editor.editor.onLeavePage'), [t]);
|
||||
|
||||
|
@ -280,6 +280,7 @@ const EditorInterface = ({
|
||||
togglePreview={handleTogglePreview}
|
||||
toggleScrollSync={handleToggleScrollSync}
|
||||
toggleI18n={handleToggleI18n}
|
||||
slug={slug}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
@ -7,13 +7,16 @@ import { Height as HeightIcon } from '@styled-icons/material-rounded/Height';
|
||||
import { Check as CheckIcon } from '@styled-icons/material/Check';
|
||||
import { MoreVert as MoreVertIcon } from '@styled-icons/material/MoreVert';
|
||||
import { Publish as PublishIcon } from '@styled-icons/material/Publish';
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { translate } from 'react-polyglot';
|
||||
|
||||
import { selectAllowDeletion } from '@staticcms/core/lib/util/collection.util';
|
||||
import Menu from '../common/menu/Menu';
|
||||
import MenuGroup from '../common/menu/MenuGroup';
|
||||
import MenuItemButton from '../common/menu/MenuItemButton';
|
||||
import confirm from '../common/confirm/Confirm';
|
||||
import { useAppDispatch } from '@staticcms/core/store/hooks';
|
||||
import { deleteLocalBackup, loadEntry } from '@staticcms/core/actions/entries';
|
||||
|
||||
import type { Collection, EditorPersistOptions, TranslatedProps } from '@staticcms/core/interface';
|
||||
import type { FC, MouseEventHandler } from 'react';
|
||||
@ -39,6 +42,7 @@ export interface EditorToolbarProps {
|
||||
togglePreview: MouseEventHandler;
|
||||
toggleScrollSync: MouseEventHandler;
|
||||
toggleI18n: MouseEventHandler;
|
||||
slug?: string | undefined;
|
||||
}
|
||||
|
||||
const EditorToolbar = ({
|
||||
@ -61,6 +65,7 @@ const EditorToolbar = ({
|
||||
togglePreview,
|
||||
toggleScrollSync,
|
||||
toggleI18n,
|
||||
slug,
|
||||
}: TranslatedProps<EditorToolbarProps>) => {
|
||||
const canCreate = useMemo(
|
||||
() => ('folder' in collection && collection.create) ?? false,
|
||||
@ -69,7 +74,28 @@ const EditorToolbar = ({
|
||||
const canDelete = useMemo(() => selectAllowDeletion(collection), [collection]);
|
||||
const isPublished = useMemo(() => !isNewEntry && !hasChanged, [hasChanged, isNewEntry]);
|
||||
|
||||
const menuItems = useMemo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleDiscardDraft = useCallback(async () => {
|
||||
if (!slug) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
await confirm({
|
||||
title: 'editor.editorToolbar.discardChangesTitle',
|
||||
body: {
|
||||
key: 'editor.editorToolbar.discardChangesBody',
|
||||
},
|
||||
color: 'warning',
|
||||
})
|
||||
) {
|
||||
dispatch(deleteLocalBackup(collection, slug));
|
||||
dispatch(loadEntry(collection, slug));
|
||||
}
|
||||
}, [collection, dispatch, slug]);
|
||||
|
||||
const menuItems: JSX.Element[][] = useMemo(() => {
|
||||
const items: JSX.Element[] = [];
|
||||
|
||||
if (!isPublished) {
|
||||
@ -105,8 +131,34 @@ const EditorToolbar = ({
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [canCreate, isPublished, onDuplicate, onPersist, onPersistAndDuplicate, onPersistAndNew, t]);
|
||||
if (hasChanged) {
|
||||
return [
|
||||
items,
|
||||
[
|
||||
<MenuItemButton
|
||||
key="discardChanges"
|
||||
onClick={handleDiscardDraft}
|
||||
startIcon={TrashIcon}
|
||||
color="warning"
|
||||
>
|
||||
{t('editor.editorToolbar.discardChanges')}
|
||||
</MenuItemButton>,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return [items];
|
||||
}, [
|
||||
canCreate,
|
||||
handleDiscardDraft,
|
||||
hasChanged,
|
||||
isPublished,
|
||||
onDuplicate,
|
||||
onPersist,
|
||||
onPersistAndDuplicate,
|
||||
onPersistAndNew,
|
||||
t,
|
||||
]);
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
@ -166,9 +218,11 @@ const EditorToolbar = ({
|
||||
isPublished ? t('editor.editorToolbar.published') : t('editor.editorToolbar.publish')
|
||||
}
|
||||
color={isPublished ? 'success' : 'primary'}
|
||||
disabled={menuItems.length == 0}
|
||||
disabled={menuItems.length == 1 && menuItems[0].length === 0}
|
||||
>
|
||||
<MenuGroup>{menuItems}</MenuGroup>
|
||||
{menuItems.map((group, index) => (
|
||||
<MenuGroup key={`menu-group-${index}`}>{group}</MenuGroup>
|
||||
))}
|
||||
</Menu>
|
||||
</div>
|
||||
),
|
||||
|
@ -363,7 +363,6 @@ function getConfigSchema() {
|
||||
folder_support: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
load_config_file: { type: 'boolean' },
|
||||
slug: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@ -388,6 +387,7 @@ function getConfigSchema() {
|
||||
},
|
||||
],
|
||||
},
|
||||
disable_local_backup: { type: 'boolean' },
|
||||
editor: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
@ -336,6 +336,7 @@ export interface TemplatePreviewCardProps<T = EntryData, EF extends BaseField =
|
||||
widgetFor: WidgetFor<T>;
|
||||
widgetsFor: WidgetsFor<T>;
|
||||
theme: 'dark' | 'light';
|
||||
hasLocalBackup: boolean;
|
||||
}
|
||||
|
||||
export type TemplatePreviewCardComponent<
|
||||
@ -791,10 +792,10 @@ export interface Config<EF extends BaseField = UnknownField> {
|
||||
public_folder?: string;
|
||||
media_folder_relative?: boolean;
|
||||
media_library?: MediaLibraryConfig;
|
||||
load_config_file?: boolean;
|
||||
slug?: Slug;
|
||||
i18n?: I18nInfo;
|
||||
local_backend?: boolean | LocalBackend;
|
||||
disable_local_backup?: boolean;
|
||||
editor?: EditorConfig;
|
||||
search?: boolean;
|
||||
}
|
||||
@ -989,3 +990,10 @@ export interface MediaLibrarInsertOptions {
|
||||
showAlt?: boolean;
|
||||
chooseUrl?: boolean;
|
||||
}
|
||||
|
||||
export interface BackupEntry {
|
||||
raw: string;
|
||||
path: string;
|
||||
mediaFiles: MediaFile[];
|
||||
i18n?: Record<string, { raw: string }>;
|
||||
}
|
||||
|
9
packages/core/src/lib/util/backup.util.ts
Normal file
9
packages/core/src/lib/util/backup.util.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export function getEntryBackupKey(collectionName?: string, slug?: string) {
|
||||
const baseKey = 'backup';
|
||||
if (!collectionName) {
|
||||
return baseKey;
|
||||
}
|
||||
const suffix = slug ? `.${slug}` : '';
|
||||
return `${baseKey}.${collectionName}${suffix}`;
|
||||
}
|
@ -108,8 +108,6 @@ const bg: LocalePhrasesRoot = {
|
||||
'Наистина ли искате да изтриете този публикуван запис, както и незаписаните промени от текущата сесия?',
|
||||
onDeletePublishedEntryBody: 'Наистина ли искате да изтриете този публикуван запис?',
|
||||
loadingEntry: 'Зареждане на запис...',
|
||||
confirmLoadBackupBody:
|
||||
'За този запис беше възстановен локален архив, бихте ли искали да го използвате?',
|
||||
},
|
||||
editorInterface: {
|
||||
toggleI18n: 'Превключване i18n',
|
||||
|
@ -107,8 +107,6 @@ const ca: LocalePhrasesRoot = {
|
||||
'Està segur que vol eliminar aquesta entrada publicada, així com els canvis no guardats de la sessió actual?',
|
||||
onDeletePublishedEntryBody: 'Està segur que vol eliminar aquesta entrada publicada?',
|
||||
loadingEntry: 'Carregant entrada...',
|
||||
confirmLoadBackupBody:
|
||||
"S'ha recuperat una copia de seguretat local per aquesta entrada. La vol utilitzar?",
|
||||
},
|
||||
editorInterface: {
|
||||
toggleI18n: 'Mostrar/Amagar traduccions',
|
||||
|
@ -108,7 +108,6 @@ const cs: LocalePhrasesRoot = {
|
||||
'Chcete opravdu vymazat tento publikovaný záznam a všechny neuložené změny z této relace?',
|
||||
onDeletePublishedEntryBody: 'Chcete opravdu smazat tento publikovaný záznam?',
|
||||
loadingEntry: 'Načítání záznamu…',
|
||||
confirmLoadBackupBody: 'Lokální kopie tohoto záznamu byla nalezena, chcete ji použít?',
|
||||
},
|
||||
editorInterface: {
|
||||
toggleI18n: 'Přepnout lokalizaci',
|
||||
|
@ -107,8 +107,6 @@ const da: LocalePhrasesRoot = {
|
||||
onDeletePublishedEntryBody:
|
||||
'Er du sikker på at du vil slette dette tidliere publiceret dokument?',
|
||||
loadingEntry: 'Indlæser dokument...',
|
||||
confirmLoadBackupBody:
|
||||
'En lokal sikkerhedskopi blev gendannet for dette dokument, vil du anvende denne?',
|
||||
},
|
||||
editorToolbar: {
|
||||
publishing: 'Publicerer...',
|
||||
|
@ -109,9 +109,6 @@ const de: LocalePhrasesRoot = {
|
||||
onDeletePublishedEntryTitle: 'Veröffentlichten Beitrag löschen?',
|
||||
onDeletePublishedEntryBody: 'Soll dieser veröffentlichte Beitrag wirklich gelöscht werden?',
|
||||
loadingEntry: 'Beitrag laden...',
|
||||
confirmLoadBackupTitle: 'Lokales Backup benutzen?',
|
||||
confirmLoadBackupBody:
|
||||
'Für diesen Beitrag ist ein lokales Backup vorhanden. Möchten Sie dieses benutzen?',
|
||||
},
|
||||
editorInterface: {
|
||||
toggleI18n: 'Übersetzungen',
|
||||
|
@ -120,9 +120,6 @@ const en: LocalePhrasesRoot = {
|
||||
onDeletePublishedEntryTitle: 'Delete this published entry?',
|
||||
onDeletePublishedEntryBody: 'Are you sure you want to delete this published entry?',
|
||||
loadingEntry: 'Loading entry...',
|
||||
confirmLoadBackupTitle: 'Use local backup?',
|
||||
confirmLoadBackupBody:
|
||||
'A local backup was recovered for this entry, would you like to use it?',
|
||||
},
|
||||
editorInterface: {
|
||||
sideBySideI18n: 'I18n Side by Side',
|
||||
@ -155,6 +152,9 @@ const en: LocalePhrasesRoot = {
|
||||
inReview: 'In review',
|
||||
ready: 'Ready',
|
||||
publishNow: 'Publish now',
|
||||
discardChanges: 'Discard changes',
|
||||
discardChangesTitle: 'Discard changes',
|
||||
discardChangesBody: 'Are you sure you want to discard the unsaved changed?',
|
||||
},
|
||||
editorWidgets: {
|
||||
markdown: {
|
||||
@ -279,6 +279,9 @@ const en: LocalePhrasesRoot = {
|
||||
default: {
|
||||
goBackToSite: 'Go back to site',
|
||||
},
|
||||
localBackup: {
|
||||
hasLocalBackup: 'Has local backup',
|
||||
},
|
||||
errorBoundary: {
|
||||
title: 'Error',
|
||||
details: "There's been an error - please ",
|
||||
|
@ -93,8 +93,6 @@ const es: LocalePhrasesRoot = {
|
||||
'¿Está seguro de que desea eliminar esta entrada publicada, así como los cambios no guardados de la sesión actual?',
|
||||
onDeletePublishedEntryBody: '¿Estás seguro de que quieres borrar esta entrada publicada?',
|
||||
loadingEntry: 'Cargando entrada...',
|
||||
confirmLoadBackupBody:
|
||||
'Se recuperó una copia de seguridad local para esta entrada, ¿le gustaría utilizarla?',
|
||||
},
|
||||
editorToolbar: {
|
||||
publishing: 'Publicando...',
|
||||
|
@ -109,8 +109,6 @@ const fr: LocalePhrasesRoot = {
|
||||
'Voulez-vous vraiment supprimer cette entrée publiée ainsi que vos modifications non enregistrées de cette session ?',
|
||||
onDeletePublishedEntryBody: 'Voulez-vous vraiment supprimer cette entrée publiée ?',
|
||||
loadingEntry: "Chargement de l'entrée...",
|
||||
confirmLoadBackupBody:
|
||||
"Une sauvegarde locale a été trouvée pour cette entrée. Voulez-vous l'utiliser ?",
|
||||
},
|
||||
editorInterface: {
|
||||
toggleI18n: 'Édition multilingue',
|
||||
|
@ -79,8 +79,6 @@ const gr: LocalePhrasesRoot = {
|
||||
onDeletePublishedEntryBody:
|
||||
'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν τη δημοσιευμένη καταχώρηση;',
|
||||
loadingEntry: 'Φόρτωση εισόδου...',
|
||||
confirmLoadBackupBody:
|
||||
'Ανακτήθηκε ένα τοπικό αντίγραφο ασφαλείας για αυτήν την καταχώρηση, θέλετε να το χρησιμοποιήσετε;',
|
||||
},
|
||||
editorToolbar: {
|
||||
publishing: 'Δημοσίευση...',
|
||||
|
@ -107,7 +107,6 @@ const he: LocalePhrasesRoot = {
|
||||
'האם ברצונך למחוק את האייטם הזה לפני פרסומו, וכן את השינויים שבוצעו כעת וטרם נשמרו?',
|
||||
onDeletePublishedEntryBody: 'האם ברצונך למחוק את האייטם הזה לאחר פרסומו?',
|
||||
loadingEntry: 'טעינת אייטם...',
|
||||
confirmLoadBackupBody: 'קיים עותק מקומי שמור של האייטם. האם ברצונך לטעון אותו?',
|
||||
},
|
||||
editorInterface: {
|
||||
toggleI18n: 'החלפת שפות',
|
||||
|
@ -108,7 +108,6 @@ const hr: LocalePhrasesRoot = {
|
||||
'Jeste li sigurni da želite obrisati objavljeni unos, te nespremljene promjene u trenutnoj sesiji?',
|
||||
onDeletePublishedEntryBody: 'Jeste li sigurni da želite obrisati ovaj objavljeni unos?',
|
||||
loadingEntry: 'Učitavanje unosa...',
|
||||
confirmLoadBackupBody: 'Lokalna kopija je dohvaćena za ovaj unos, želite li ju koristiti?',
|
||||
},
|
||||
editorToolbar: {
|
||||
publishing: 'Objavljivanje...',
|
||||
|
@ -62,8 +62,6 @@ const hu: LocalePhrasesRoot = {
|
||||
'Töröljük ezt a publikált bejegyzést, a többi mentetlen modositással együtt?',
|
||||
onDeletePublishedEntryBody: 'Töröljük ezt a publikált bejegyzést?',
|
||||
loadingEntry: 'Bejegyzés betöltése...',
|
||||
confirmLoadBackupBody:
|
||||
'Helyi biztonsági másolat került helyre ehhez a bejegyzéshez, szeretné használni?',
|
||||
},
|
||||
editorToolbar: {
|
||||
publishing: 'Publikálás...',
|
||||
|
@ -77,8 +77,6 @@ const it: LocalePhrasesRoot = {
|
||||
'Sei sicuro di voler cancellare questa voce pubblicata e tutte le modifiche non salvate della tua sessione corrente?',
|
||||
onDeletePublishedEntryBody: 'Sei sicuro di voler cancellare questa voce pubblicata?',
|
||||
loadingEntry: 'Caricando la voce...',
|
||||
confirmLoadBackupBody:
|
||||
'Un backup locale è stato recuperato per questa voce, vuoi utilizzarlo?',
|
||||
},
|
||||
editorToolbar: {
|
||||
publishing: 'Pubblicando...',
|
||||
|
@ -107,7 +107,6 @@ const ja: LocalePhrasesRoot = {
|
||||
'保存されていない変更も削除されますが、この公開エントリを削除しますか?',
|
||||
onDeletePublishedEntryBody: 'この公開エントリを削除しますか?',
|
||||
loadingEntry: 'エントリの読込中...',
|
||||
confirmLoadBackupBody: 'ローカルのバックアップが復旧できました。利用しますか?',
|
||||
},
|
||||
editorInterface: {
|
||||
toggleI18n: '言語を切り替える',
|
||||
|
@ -100,8 +100,6 @@ const ko: LocalePhrasesRoot = {
|
||||
'현재 세션에서의 저장되지 않은 변경사항과 이 게시된 항목을 삭제하시겠습니까?',
|
||||
onDeletePublishedEntryBody: '이 게시된 항목을 삭제하시겠습니까?',
|
||||
loadingEntry: '항목 불러오는 중...',
|
||||
confirmLoadBackupBody:
|
||||
'이 항목에 대한 로컬 백업이 복구되었습니다, 복구된 것으로 사용하시겠습니까?',
|
||||
},
|
||||
editorToolbar: {
|
||||
publishing: '게시 중...',
|
||||
|
@ -109,8 +109,6 @@ const lt: LocalePhrasesRoot = {
|
||||
'Tikrai norite panaikinti publikuotą įrašą ir Jūsų pakeiitmus iš dabartinės sesijos?',
|
||||
onDeletePublishedEntryBody: 'Tikrai norite ištrinti šį publikuotą įrašą?',
|
||||
loadingEntry: 'Kraunamas įrašas...',
|
||||
confirmLoadBackupBody:
|
||||
'Radome Jūsų įrenginyje išsaugota juodraštį šiam įrašui, ar norite jį atgaivinti ir naudoti?',
|
||||
},
|
||||
editorToolbar: {
|
||||
publishing: 'Publikuojama...',
|
||||
|
@ -91,7 +91,6 @@ const nb_no: LocalePhrasesRoot = {
|
||||
'Er du sikker på at du vil slette et publisert innlegg med tilhørende ulagrede endringer?',
|
||||
onDeletePublishedEntryBody: 'Er du sikker på at du vil slette dette publiserte innlegget?',
|
||||
loadingEntry: 'Laster innlegg...',
|
||||
confirmLoadBackupBody: 'Vil du gjenopprette tidligere endringer som ikke har blitt lagret?',
|
||||
},
|
||||
editorToolbar: {
|
||||
publishing: 'Publiserer...',
|
||||
|
@ -106,7 +106,6 @@ const nl: LocalePhrasesRoot = {
|
||||
'Weet u zeker dat u dit gepubliceerde item en uw niet-opgeslagen wijzigingen uit de huidige sessie wilt verwijderen?',
|
||||
onDeletePublishedEntryBody: 'Weet u zeker dat u dit gepubliceerde item wilt verwijderen?',
|
||||
loadingEntry: 'Item laden...',
|
||||
confirmLoadBackupBody: 'Voor dit item is een lokale back-up hersteld, wilt u deze gebruiken?',
|
||||
},
|
||||
editorInterface: {
|
||||
toggleI18n: 'Wissel i18n',
|
||||
|
@ -91,8 +91,6 @@ const nn_no: LocalePhrasesRoot = {
|
||||
'Er du sikkert på at du vil slette eit publisert innlegg med tilhøyrande ulagra endringar?',
|
||||
onDeletePublishedEntryBody: 'Er du sikker på at du vil slette dette publiserte innlegget?',
|
||||
loadingEntry: 'Lastar innlegg...',
|
||||
confirmLoadBackupBody:
|
||||
'Ynskjer du å gjennopprette tidlegare endringar som ikkje har verta lagra?',
|
||||
},
|
||||
editorToolbar: {
|
||||
publishing: 'Publiserer...',
|
||||
|
@ -108,7 +108,6 @@ const pl: LocalePhrasesRoot = {
|
||||
'Czy na pewno chcesz usunąć tę opublikowaną pozycję, a także niezapisane zmiany z bieżącej sesji?',
|
||||
onDeletePublishedEntryBody: 'Czy na pewno chcesz usunąć tę opublikowaną pozycję?',
|
||||
loadingEntry: 'Ładowanie pozycji...',
|
||||
confirmLoadBackupBody: 'Odzyskano lokalną kopię zapasową tej pozycji, czy chcesz jej użyć?',
|
||||
},
|
||||
editorInterface: {
|
||||
toggleI18n: 'Przełącz i18n',
|
||||
|
@ -108,7 +108,6 @@ const pt: LocalePhrasesRoot = {
|
||||
'Tem certeza de que deseja excluir esta entrada publicada, bem como as alterações não salvas da sessão atual?',
|
||||
onDeletePublishedEntryBody: 'Tem certeza de que deseja excluir esta entrada publicada?',
|
||||
loadingEntry: 'Carregando entrada...',
|
||||
confirmLoadBackupBody: 'Um backup local foi recuperado para esta entrada. Deseja usá-lo?',
|
||||
},
|
||||
editorInterface: {
|
||||
toggleI18n: 'Mudar i18n',
|
||||
|
@ -108,8 +108,6 @@ const ro: LocalePhrasesRoot = {
|
||||
'Ești sigur/ă că dorești să ștergi această publicare, dar și modificările nesalvate din sesiunea curentă?',
|
||||
onDeletePublishedEntryBody: 'Ești sigur/ă că dorești să ștergi această publicare?',
|
||||
loadingEntry: 'Se încarcă...',
|
||||
confirmLoadBackupBody:
|
||||
'Un backup local a fost recuperat pentru această intrare, dorești să îl folosești?',
|
||||
},
|
||||
editorInterface: {
|
||||
toggleI18n: 'Comută limba',
|
||||
|
@ -108,8 +108,6 @@ const ru: LocalePhrasesRoot = {
|
||||
'Вы уверены, что хотите удалить эту опубликованную запись, а также несохраненные изменения из текущего сеанса?',
|
||||
onDeletePublishedEntryBody: 'Вы уверены, что хотите удалить эту опубликованную запись?',
|
||||
loadingEntry: 'Загрузка записи…',
|
||||
confirmLoadBackupBody:
|
||||
'Для этой записи была восстановлена локальная резервная копия, хотите ли вы ее использовать?',
|
||||
},
|
||||
editorToolbar: {
|
||||
publishing: 'Публикация…',
|
||||
|
@ -108,7 +108,6 @@ const sv: LocalePhrasesRoot = {
|
||||
'Är du säker på att du vill radera det här publicerade inlägget, inklusive dina osparade ändringar från nuvarande session?',
|
||||
onDeletePublishedEntryBody: 'Är du säker på att du vill radera det här publicerade inlägget?',
|
||||
loadingEntry: 'Hämtar inlägg...',
|
||||
confirmLoadBackupBody: 'En lokal kopia hittades för det här inlägget, vill du använda den?',
|
||||
},
|
||||
editorInterface: {
|
||||
toggleI18n: 'Slå på/av i18n',
|
||||
|
@ -103,7 +103,6 @@ const th: LocalePhrasesRoot = {
|
||||
'คุณแน่ใจหรือว่าจะต้องการลบการเผยแพร่เนื้อหานี้ รวมถึงการเปลี่ยนแปลงที่ยังไม่ได้บันทึก?',
|
||||
onDeletePublishedEntryBody: 'คุณแน่ใจหรือว่าจะต้องการลบการเผยแพร่เนื้อหานี้?',
|
||||
loadingEntry: 'กำลังโหลดเนื้อหา...',
|
||||
confirmLoadBackupBody: 'ข้อมูลสำรองได้ถูกกู้คืนสำหรับเนื้อหานี้ คุณต้องการใช้มันไหม?',
|
||||
},
|
||||
editorToolbar: {
|
||||
publishing: 'กำลังเผยแพร่...',
|
||||
|
@ -109,8 +109,6 @@ const tr: LocalePhrasesRoot = {
|
||||
'Bu oturumda kaydedilmiş değişikliklerin yanı sıra geçerli oturumdaki kaydedilmemiş değişikliklerinizi silmek istediğinize emin misiniz?',
|
||||
onDeletePublishedEntryBody: 'Bu yayınlanmış girdiyi silmek istediğinize emin misiniz?',
|
||||
loadingEntry: 'Girdiler yükleniyor...',
|
||||
confirmLoadBackupBody:
|
||||
'Bu girdi için yerel bir yedekleme kurtarıldı, kullanmak ister misiniz?',
|
||||
},
|
||||
editorInterface: {
|
||||
toggleI18n: 'i18n değiştir',
|
||||
|
@ -61,7 +61,6 @@ const uk: LocalePhrasesRoot = {
|
||||
'Ви дійсно бажаєте видалити опублікований запис, як і всі незбережені зміни під час поточної сесії?',
|
||||
onDeletePublishedEntryBody: 'Ви дійсно бажаєте видалити опублікований запис?',
|
||||
loadingEntry: 'Завантаження...',
|
||||
confirmLoadBackupBody: 'Відновлено резервну копію, бажаєте її використати?',
|
||||
},
|
||||
editorToolbar: {
|
||||
publishing: 'Публікація...',
|
||||
|
@ -99,8 +99,6 @@ const vi: LocalePhrasesRoot = {
|
||||
'Bạn có chắc rằng bạn muốn xoá mục đã được công bố này, cũng như là những thay đổi chưa lưu của bạn trong phiên làm việc này?',
|
||||
onDeletePublishedEntryBody: 'Bạn có chắc rằng bạn muốn xoá mục đã được công bố này?',
|
||||
loadingEntry: 'Đang tải...',
|
||||
confirmLoadBackupBody:
|
||||
'Một bản sao lưu trên máy đã được phục hồi cho mục này, bạn có muốn tải lên không?',
|
||||
},
|
||||
editorToolbar: {
|
||||
publishing: 'Đang công bố...',
|
||||
|
@ -105,7 +105,6 @@ const zh_Hans: LocalePhrasesRoot = {
|
||||
onDeleteWithUnsavedChangesBody: '你确定要删除这个已经发布的内容,以及当前尚未保存的修改吗?',
|
||||
onDeletePublishedEntryBody: '你确定要删除这个已经发布的内容吗?',
|
||||
loadingEntry: '正在加载内容...',
|
||||
confirmLoadBackupBody: '发现了一个对应此内容的本地备份,你要加载它吗?',
|
||||
},
|
||||
editorInterface: {
|
||||
toggleI18n: '打开/关闭国际化',
|
||||
|
@ -100,7 +100,6 @@ const zh_Hant: LocalePhrasesRoot = {
|
||||
onDeleteWithUnsavedChangesBody: '你確定要刪除這篇已發布的內容以及你尚未儲存的變更?',
|
||||
onDeletePublishedEntryBody: '你確定要刪除這篇已發布的內容?',
|
||||
loadingEntry: '載入內容中...',
|
||||
confirmLoadBackupBody: '此內容的本地備份已經還原,你想要使用嗎?',
|
||||
},
|
||||
editorToolbar: {
|
||||
publishing: '發布中...',
|
||||
|
@ -265,6 +265,55 @@
|
||||
dark:disabled:hover:bg-transparent;
|
||||
}
|
||||
|
||||
.btn-contained-warning {
|
||||
@apply border
|
||||
border-transparent
|
||||
bg-yellow-500
|
||||
hover:bg-yellow-600
|
||||
text-white
|
||||
disabled:text-gray-50
|
||||
disabled:bg-gray-300/80
|
||||
dark:bg-yellow-600
|
||||
dark:hover:bg-yellow-800
|
||||
dark:disabled:text-slate-400/50
|
||||
dark:disabled:bg-slate-700/50;
|
||||
}
|
||||
|
||||
.btn-outlined-warning {
|
||||
@apply bg-transparent
|
||||
border
|
||||
text-yellow-500
|
||||
border-yellow-200
|
||||
hover:bg-yellow-50
|
||||
hover:text-yellow-600
|
||||
disabled:text-gray-300
|
||||
disabled:border-gray-200
|
||||
disabled:hover:bg-transparent
|
||||
dark:bg-transparent
|
||||
dark:text-yellow-400
|
||||
dark:border-yellow-600
|
||||
dark:hover:text-yellow-500
|
||||
dark:hover:bg-yellow-700/10
|
||||
dark:disabled:text-slate-600/75
|
||||
dark:disabled:border-slate-600/75
|
||||
dark:disabled:hover:bg-transparent;
|
||||
}
|
||||
|
||||
.btn-text-warning {
|
||||
@apply bg-transparent
|
||||
text-yellow-500
|
||||
hover:text-yellow-600
|
||||
hover:bg-yellow-50
|
||||
disabled:text-gray-300
|
||||
disabled:hover:bg-transparent
|
||||
dark:text-yellow-400
|
||||
dark:hover:text-yellow-500
|
||||
dark:hover:bg-yellow-700/10
|
||||
dark:disabled:text-slate-600/75
|
||||
dark:disabled:border-slate-600/75
|
||||
dark:disabled:hover:bg-transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox
|
||||
*/
|
||||
|
@ -300,3 +300,7 @@ _This setting is required._
|
||||
The `collections` setting is the heart of your Static CMS configuration, as it determines how content types and editor fields in the UI generate files and content in your repository. Each collection you configure displays in the left sidebar of the Content page of the editor UI, in the order they are entered into your Static CMS `config` file.
|
||||
|
||||
`collections` accepts a list of collection objects. See [Collections](/docs/collection-overview) for details.
|
||||
|
||||
## Disable Local Backup
|
||||
|
||||
When the `disable_local_backup` setting is set to `true` local backups will no be taken for your entries and you will not be prompted to load local backups.
|
||||
|
@ -42,10 +42,7 @@ The following parameters will be passed to your `react_component` during render:
|
||||
|
||||
```js
|
||||
const PostPreview = ({ widgetFor, entry, collection, fields }) => {
|
||||
const imageField = useMemo(() =>
|
||||
fields.find(field => field.name === 'image'),
|
||||
[fields],
|
||||
);
|
||||
const imageField = useMemo(() => fields.find(field => field.name === 'image'), [fields]);
|
||||
const imageUrl = useMediaAsset(entry.data.image, collection, imageField, entry);
|
||||
|
||||
return h(
|
||||
@ -64,10 +61,7 @@ CMS.registerPreviewTemplate('posts', PostPreview);
|
||||
import CMS, { useMediaAsset } from '@staticcms/core';
|
||||
|
||||
const PostPreview = ({ widgetFor, entry, collection, fields }) => {
|
||||
const imageField = useMemo(() =>
|
||||
fields.find(field => field.name === 'image'),
|
||||
[fields],
|
||||
);
|
||||
const imageField = useMemo(() => fields.find(field => field.name === 'image'), [fields]);
|
||||
const imageUrl = useMediaAsset(entry.data.image, collection, imageField, entry);
|
||||
|
||||
return (
|
||||
@ -97,10 +91,7 @@ interface Post {
|
||||
}
|
||||
|
||||
const PostPreview = ({ widgetFor, entry, collection, fields }: TemplatePreviewProps<Post>) => {
|
||||
const imageField = useMemo(() =>
|
||||
fields.find(field => field.name === 'image'),
|
||||
[fields],
|
||||
);
|
||||
const imageField = useMemo(() => fields.find(field => field.name === 'image'), [fields]);
|
||||
const imageUrl = useMediaAsset(entry.data.image, collection, imageField, entry);
|
||||
|
||||
return (
|
||||
@ -356,11 +347,12 @@ CMS.registerPreviewStyle('.main { color: blue; border: 1px solid gree; }', { raw
|
||||
The following parameters will be passed to your `react_component` during render:
|
||||
|
||||
| Param | Type | Description |
|
||||
| ---------- | ---------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| -------------- | ---------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| entry | object | Object with a `data` field that contains the current value of all widgets in the editor |
|
||||
| widgetFor | Function | Given a field name, returns the rendered preview of that field's widget and value |
|
||||
| widgetsFor | Function | Given a field name, returns the rendered previews of that field's nested child widgets and values |
|
||||
| theme | 'light'<br />\| 'dark' | The current theme being used by the app |
|
||||
| hasLocalBackup | boolean | Whether the current entry has a local backup |
|
||||
|
||||
#### Example
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user