♻️(frontend) add modal confirmation restore version

Add modal confirmation restore version explaining
that the current version will be replaced
by the selected version, and that some data
may be lost.
This commit is contained in:
Anthony LC
2024-09-02 16:02:21 +02:00
committed by Anthony LC
parent accbda44e2
commit 296b5dbf59
6 changed files with 193 additions and 94 deletions

View File

@@ -119,6 +119,14 @@ test.describe('Doc Version', () => {
await panel.getByLabel('Open the version options').click(); await panel.getByLabel('Open the version options').click();
await page.getByText('Restore the version').click(); await page.getByText('Restore the version').click();
await expect(page.getByText('Restore this version?')).toBeVisible();
await page
.getByRole('button', {
name: 'Restore',
})
.click();
await expect(panel.locator('li')).toHaveCount(3); await expect(panel.locator('li')).toHaveCount(3);
await panel.getByText('Current version').click(); await panel.getByText('Current version').click();

View File

@@ -5,12 +5,9 @@ import * as Y from 'yjs';
import { useUpdateDoc } from '@/features/docs/doc-management/'; import { useUpdateDoc } from '@/features/docs/doc-management/';
import { KEY_LIST_DOC_VERSIONS } from '@/features/docs/doc-versioning'; import { KEY_LIST_DOC_VERSIONS } from '@/features/docs/doc-versioning';
import { useDocStore } from '../stores';
import { toBase64 } from '../utils'; import { toBase64 } from '../utils';
const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => { const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
const { forceSave, setForceSave } = useDocStore();
const { mutate: updateDoc } = useUpdateDoc({ const { mutate: updateDoc } = useUpdateDoc({
listInvalideQueries: [KEY_LIST_DOC_VERSIONS], listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
}); });
@@ -68,18 +65,6 @@ const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
}); });
}, [doc, docId, updateDoc]); }, [doc, docId, updateDoc]);
useEffect(() => {
if (forceSave === 'false') {
return;
}
setForceSave('false');
if ((forceSave === 'current' && hasChanged()) || forceSave === 'version') {
saveDoc();
}
}, [forceSave, hasChanged, saveDoc, setForceSave]);
const timeout = useRef<NodeJS.Timeout>(); const timeout = useRef<NodeJS.Timeout>();
const router = useRouter(); const router = useRouter();

View File

@@ -1,2 +1,3 @@
export * from './components'; export * from './components';
export * from './stores'; export * from './stores';
export * from './utils';

View File

@@ -11,24 +11,16 @@ interface DocStore {
editor?: BlockNoteEditor; editor?: BlockNoteEditor;
} }
type ForceSaveState = 'false' | 'version' | 'current';
export interface UseDocStore { export interface UseDocStore {
docsStore: { docsStore: {
[storeId: string]: DocStore; [storeId: string]: DocStore;
}; };
createProvider: (storeId: string, initialDoc: Base64) => WebrtcProvider; createProvider: (storeId: string, initialDoc: Base64) => WebrtcProvider;
setStore: (storeId: string, props: Partial<DocStore>) => void; setStore: (storeId: string, props: Partial<DocStore>) => void;
forceSave: ForceSaveState;
setForceSave: (forceSave: ForceSaveState) => void;
} }
export const useDocStore = create<UseDocStore>((set, get) => ({ export const useDocStore = create<UseDocStore>((set, get) => ({
docsStore: {}, docsStore: {},
forceSave: 'false',
setForceSave: (forceSave) => {
set(() => ({ forceSave }));
},
createProvider: (storeId: string, initialDoc: Base64) => { createProvider: (storeId: string, initialDoc: Base64) => {
const doc = new Y.Doc({ const doc = new Y.Doc({
guid: storeId, guid: storeId,

View File

@@ -0,0 +1,122 @@
import {
Alert,
Button,
Modal,
ModalSize,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
import { t } from 'i18next';
import { useRouter } from 'next/navigation';
import * as Y from 'yjs';
import { Box, Text } from '@/components';
import { toBase64, useDocStore } from '@/features/docs/doc-editor';
import { Doc, useUpdateDoc } from '@/features/docs/doc-management';
import { KEY_LIST_DOC_VERSIONS } from '../api/useDocVersions';
import { Versions } from '../types';
import { revertUpdate } from '../utils';
interface ModalVersionProps {
onClose: () => void;
docId: Doc['id'];
versionId: Versions['version_id'];
}
export const ModalVersion = ({
onClose,
docId,
versionId,
}: ModalVersionProps) => {
const { toast } = useToastProvider();
const router = useRouter();
const { docsStore, setStore } = useDocStore();
const { mutate: updateDoc } = useUpdateDoc({
listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
onSuccess: () => {
const onDisplaySuccess = () => {
toast(t('Version restored successfully'), VariantType.SUCCESS);
router.push(`/docs/${docId}`);
};
if (!docsStore?.[docId]?.provider || !docsStore?.[versionId]?.provider) {
onDisplaySuccess();
return;
}
setStore(docId, {
editor: undefined,
});
revertUpdate(
docsStore[docId].provider.doc,
docsStore[docId].provider.doc,
docsStore[versionId].provider.doc,
);
onDisplaySuccess();
},
});
return (
<Modal
isOpen
closeOnClickOutside
hideCloseButton
leftActions={
<Button
aria-label={t('Close the modal')}
color="secondary"
fullWidth
onClick={() => onClose()}
>
{t('Cancel')}
</Button>
}
onClose={() => onClose()}
rightActions={
<Button
aria-label={t('Restore')}
color="primary"
fullWidth
onClick={() => {
const newDoc = toBase64(
Y.encodeStateAsUpdate(docsStore?.[versionId]?.provider.doc),
);
updateDoc({
id: docId,
content: newDoc,
});
}}
>
{t('Restore')}
</Button>
}
size={ModalSize.MEDIUM}
title={
<Box $gap="1rem">
<Text $isMaterialIcon $size="36px" $theme="primary">
restore
</Text>
<Text as="h2" $size="h3" $margin="none">
{t('Restore this version?')}
</Text>
</Box>
}
>
<Box aria-label={t('Modal confirmation to restore the version')}>
<Alert canClose={false} type={VariantType.WARNING}>
<Box>
<Text>
{t('Your current document will revert to this version.')}
</Text>
<Text>{t('If a member is editing, his works can be lost.')}</Text>
</Box>
</Alert>
</Box>
</Modal>
);
};

View File

@@ -4,11 +4,11 @@ import React, { PropsWithChildren, useState } from 'react';
import { Box, DropButton, IconOptions, StyledLink, Text } from '@/components'; import { Box, DropButton, IconOptions, StyledLink, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import { useDocStore } from '@/features/docs/doc-editor';
import { Doc } from '@/features/docs/doc-management'; import { Doc } from '@/features/docs/doc-management';
import { Versions } from '../types'; import { Versions } from '../types';
import { revertUpdate } from '../utils';
import { ModalVersion } from './ModalVersion';
interface VersionItemProps { interface VersionItemProps {
docId: Doc['id']; docId: Doc['id'];
@@ -25,15 +25,16 @@ export const VersionItem = ({
link, link,
isActive, isActive,
}: VersionItemProps) => { }: VersionItemProps) => {
const { setForceSave, docsStore, setStore } = useDocStore();
const { colorsTokens } = useCunninghamTheme(); const { colorsTokens } = useCunninghamTheme();
const [isDropOpen, setIsDropOpen] = useState(false); const [isDropOpen, setIsDropOpen] = useState(false);
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
return ( return (
<Box <>
as="li" <Box
$background={isActive ? colorsTokens()['primary-300'] : 'transparent'} as="li"
$css={` $background={isActive ? colorsTokens()['primary-300'] : 'transparent'}
$css={`
border-left: 4px solid transparent; border-left: 4px solid transparent;
border-bottom: 1px solid ${colorsTokens()['primary-100']}; border-bottom: 1px solid ${colorsTokens()['primary-100']};
&:hover{ &:hover{
@@ -41,71 +42,61 @@ export const VersionItem = ({
background: ${colorsTokens()['primary-300']}; background: ${colorsTokens()['primary-300']};
} }
`} `}
$hasTransition $hasTransition
$minWidth="13rem" $minWidth="13rem"
> >
<Link href={link} isActive={isActive}> <Link href={link} isActive={isActive}>
<Box <Box
$padding={{ vertical: '0.7rem', horizontal: 'small' }} $padding={{ vertical: '0.7rem', horizontal: 'small' }}
$align="center" $align="center"
$direction="row" $direction="row"
$justify="space-between" $justify="space-between"
$width="100%" $width="100%"
> >
<Box $direction="row" $gap="0.5rem" $align="center"> <Box $direction="row" $gap="0.5rem" $align="center">
<Text $isMaterialIcon $size="24px" $theme="primary"> <Text $isMaterialIcon $size="24px" $theme="primary">
description description
</Text> </Text>
<Text $weight="bold" $theme="primary" $size="m"> <Text $weight="bold" $theme="primary" $size="m">
{text} {text}
</Text> </Text>
</Box>
{isActive && versionId && (
<DropButton
button={
<IconOptions
isOpen={isDropOpen}
aria-label={t('Open the version options')}
/>
}
onOpenChange={(isOpen) => setIsDropOpen(isOpen)}
isOpen={isDropOpen}
>
<Box>
<Button
onClick={() => {
setIsModalVersionOpen(true);
}}
color="primary-text"
icon={<span className="material-icons">save</span>}
size="small"
>
<Text $theme="primary">{t('Restore the version')}</Text>
</Button>
</Box>
</DropButton>
)}
</Box> </Box>
{isActive && versionId && ( </Link>
<DropButton </Box>
button={ {isModalVersionOpen && versionId && (
<IconOptions <ModalVersion
isOpen={isDropOpen} onClose={() => setIsModalVersionOpen(false)}
aria-label={t('Open the version options')} docId={docId}
/> versionId={versionId}
} />
onOpenChange={(isOpen) => setIsDropOpen(isOpen)} )}
isOpen={isDropOpen} </>
>
<Box>
<Button
onClick={() => {
setIsDropOpen(false);
setForceSave(versionId ? 'version' : 'current');
if (
!docsStore?.[docId]?.provider ||
!docsStore?.[versionId]?.provider
) {
return;
}
setStore(docId, {
editor: undefined,
});
revertUpdate(
docsStore[docId].provider.doc,
docsStore[docId].provider.doc,
docsStore[versionId].provider.doc,
);
}}
color="primary-text"
icon={<span className="material-icons">save</span>}
size="small"
>
<Text $theme="primary">{t('Restore the version')}</Text>
</Button>
</Box>
</DropButton>
)}
</Box>
</Link>
</Box>
); );
}; };