feat: local backup enhancements (#714)
This commit is contained in:
committed by
GitHub
parent
39bb9647b2
commit
804c09415b
@ -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();
|
||||
}
|
||||
dispatch(loadLocalBackup());
|
||||
setVersion(version + 1);
|
||||
};
|
||||
|
||||
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(() => {
|
||||
dispatch(retrieveLocalBackup(collection, slug));
|
||||
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
|
||||
*/
|
||||
|
Reference in New Issue
Block a user