feat: local backup enhancements (#714)

This commit is contained in:
Daniel Lautzenheiser
2023-04-19 13:30:21 -04:00
committed by GitHub
parent 39bb9647b2
commit 804c09415b
49 changed files with 348 additions and 140 deletions

View File

@ -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 =

View File

@ -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>
);

View File

@ -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>;

View File

@ -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;

View File

@ -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',
},
};

View File

@ -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;

View File

@ -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}>

View File

@ -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
`,
),
},

View File

@ -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}

View File

@ -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]);

View File

@ -280,6 +280,7 @@ const EditorInterface = ({
togglePreview={handleTogglePreview}
toggleScrollSync={handleToggleScrollSync}
toggleI18n={handleToggleI18n}
slug={slug}
/>
}
>

View File

@ -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>
),

View File

@ -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: {

View File

@ -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 }>;
}

View 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}`;
}

View File

@ -108,8 +108,6 @@ const bg: LocalePhrasesRoot = {
'Наистина ли искате да изтриете този публикуван запис, както и незаписаните промени от текущата сесия?',
onDeletePublishedEntryBody: 'Наистина ли искате да изтриете този публикуван запис?',
loadingEntry: 'Зареждане на запис...',
confirmLoadBackupBody:
'За този запис беше възстановен локален архив, бихте ли искали да го използвате?',
},
editorInterface: {
toggleI18n: 'Превключване i18n',

View File

@ -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',

View File

@ -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',

View File

@ -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...',

View File

@ -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',

View File

@ -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 ",

View File

@ -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...',

View File

@ -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',

View File

@ -79,8 +79,6 @@ const gr: LocalePhrasesRoot = {
onDeletePublishedEntryBody:
'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν τη δημοσιευμένη καταχώρηση;',
loadingEntry: 'Φόρτωση εισόδου...',
confirmLoadBackupBody:
'Ανακτήθηκε ένα τοπικό αντίγραφο ασφαλείας για αυτήν την καταχώρηση, θέλετε να το χρησιμοποιήσετε;',
},
editorToolbar: {
publishing: 'Δημοσίευση...',

View File

@ -107,7 +107,6 @@ const he: LocalePhrasesRoot = {
'האם ברצונך למחוק את האייטם הזה לפני פרסומו, וכן את השינויים שבוצעו כעת וטרם נשמרו?',
onDeletePublishedEntryBody: 'האם ברצונך למחוק את האייטם הזה לאחר פרסומו?',
loadingEntry: 'טעינת אייטם...',
confirmLoadBackupBody: 'קיים עותק מקומי שמור של האייטם. האם ברצונך לטעון אותו?',
},
editorInterface: {
toggleI18n: 'החלפת שפות',

View File

@ -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...',

View File

@ -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...',

View File

@ -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...',

View File

@ -107,7 +107,6 @@ const ja: LocalePhrasesRoot = {
'保存されていない変更も削除されますが、この公開エントリを削除しますか?',
onDeletePublishedEntryBody: 'この公開エントリを削除しますか?',
loadingEntry: 'エントリの読込中...',
confirmLoadBackupBody: 'ローカルのバックアップが復旧できました。利用しますか?',
},
editorInterface: {
toggleI18n: '言語を切り替える',

View File

@ -100,8 +100,6 @@ const ko: LocalePhrasesRoot = {
'현재 세션에서의 저장되지 않은 변경사항과 이 게시된 항목을 삭제하시겠습니까?',
onDeletePublishedEntryBody: '이 게시된 항목을 삭제하시겠습니까?',
loadingEntry: '항목 불러오는 중...',
confirmLoadBackupBody:
'이 항목에 대한 로컬 백업이 복구되었습니다, 복구된 것으로 사용하시겠습니까?',
},
editorToolbar: {
publishing: '게시 중...',

View File

@ -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...',

View File

@ -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...',

View File

@ -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',

View File

@ -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...',

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -108,8 +108,6 @@ const ru: LocalePhrasesRoot = {
'Вы уверены, что хотите удалить эту опубликованную запись, а также несохраненные изменения из текущего сеанса?',
onDeletePublishedEntryBody: 'Вы уверены, что хотите удалить эту опубликованную запись?',
loadingEntry: 'Загрузка записи…',
confirmLoadBackupBody:
'Для этой записи была восстановлена локальная резервная копия, хотите ли вы ее использовать?',
},
editorToolbar: {
publishing: 'Публикация…',

View File

@ -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',

View File

@ -103,7 +103,6 @@ const th: LocalePhrasesRoot = {
'คุณแน่ใจหรือว่าจะต้องการลบการเผยแพร่เนื้อหานี้ รวมถึงการเปลี่ยนแปลงที่ยังไม่ได้บันทึก?',
onDeletePublishedEntryBody: 'คุณแน่ใจหรือว่าจะต้องการลบการเผยแพร่เนื้อหานี้?',
loadingEntry: 'กำลังโหลดเนื้อหา...',
confirmLoadBackupBody: 'ข้อมูลสำรองได้ถูกกู้คืนสำหรับเนื้อหานี้ คุณต้องการใช้มันไหม?',
},
editorToolbar: {
publishing: 'กำลังเผยแพร่...',

View File

@ -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',

View File

@ -61,7 +61,6 @@ const uk: LocalePhrasesRoot = {
'Ви дійсно бажаєте видалити опублікований запис, як і всі незбережені зміни під час поточної сесії?',
onDeletePublishedEntryBody: 'Ви дійсно бажаєте видалити опублікований запис?',
loadingEntry: 'Завантаження...',
confirmLoadBackupBody: 'Відновлено резервну копію, бажаєте її використати?',
},
editorToolbar: {
publishing: 'Публікація...',

View File

@ -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ố...',

View File

@ -105,7 +105,6 @@ const zh_Hans: LocalePhrasesRoot = {
onDeleteWithUnsavedChangesBody: '你确定要删除这个已经发布的内容,以及当前尚未保存的修改吗?',
onDeletePublishedEntryBody: '你确定要删除这个已经发布的内容吗?',
loadingEntry: '正在加载内容...',
confirmLoadBackupBody: '发现了一个对应此内容的本地备份,你要加载它吗?',
},
editorInterface: {
toggleI18n: '打开/关闭国际化',

View File

@ -100,7 +100,6 @@ const zh_Hant: LocalePhrasesRoot = {
onDeleteWithUnsavedChangesBody: '你確定要刪除這篇已發布的內容以及你尚未儲存的變更?',
onDeletePublishedEntryBody: '你確定要刪除這篇已發布的內容?',
loadingEntry: '載入內容中...',
confirmLoadBackupBody: '此內容的本地備份已經還原,你想要使用嗎?',
},
editorToolbar: {
publishing: '發布中...',

View File

@ -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
*/