(frontend) doc page when deleted

Whe the doc is deleted, the doc page is a bit
different, we have to adapt the doc header
to add some information and actions that
are relevant for a deleted doc.
This commit is contained in:
Anthony LC
2025-10-08 17:14:35 +02:00
parent de4d11732f
commit 6523165ea0
19 changed files with 477 additions and 110 deletions

View File

@@ -96,7 +96,7 @@ test.describe('Doc Header', () => {
page.getByRole('heading', { name: 'Delete a doc' }), page.getByRole('heading', { name: 'Delete a doc' }),
).toBeVisible(); ).toBeVisible();
await expect(page.getByText(`This document and any sub-`)).toBeVisible(); await expect(page.getByText(`This document will be`)).toBeVisible();
await page await page
.getByRole('button', { .getByRole('button', {

View File

@@ -1,12 +1,18 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { import {
clickInEditorMenu,
clickInGridMenu, clickInGridMenu,
createDoc, createDoc,
getGridRow, getGridRow,
verifyDocName, verifyDocName,
} from './utils-common'; } from './utils-common';
import { addNewMember } from './utils-share'; import { addNewMember } from './utils-share';
import {
addChild,
createRootSubPage,
navigateToPageFromTree,
} from './utils-sub-pages';
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await page.goto('/'); await page.goto('/');
@@ -74,4 +80,71 @@ test.describe('Doc Trashbin', () => {
await page.getByRole('link', { name: 'Trashbin' }).click(); await page.getByRole('link', { name: 'Trashbin' }).click();
await expect(row2.getByText(title2)).toBeHidden(); await expect(row2.getByText(title2)).toBeHidden();
}); });
test('it controls UI and interaction from the doc page', async ({
page,
browserName,
}) => {
const [topParent] = await createDoc(
page,
'my-trash-editor-doc',
browserName,
1,
);
await verifyDocName(page, topParent);
const { name: subDocName } = await createRootSubPage(
page,
browserName,
'my-trash-editor-subdoc',
);
const subsubDocName = await addChild({
page,
browserName,
docParent: subDocName,
});
await verifyDocName(page, subsubDocName);
await navigateToPageFromTree({ page, title: subDocName });
await verifyDocName(page, subDocName);
await clickInEditorMenu(page, 'Delete sub-document');
await page.getByRole('button', { name: 'Delete document' }).click();
await verifyDocName(page, topParent);
await page.getByRole('button', { name: 'Back to homepage' }).click();
await page.getByRole('link', { name: 'Trashbin' }).click();
const row = await getGridRow(page, subDocName);
await row.getByText(subDocName).click();
await verifyDocName(page, subDocName);
await expect(page.getByLabel('Alert deleted document')).toBeVisible();
await expect(page.getByRole('button', { name: 'Share' })).toBeDisabled();
await expect(page.locator('.bn-editor')).toHaveAttribute(
'contenteditable',
'false',
);
const docTree = page.getByTestId('doc-tree');
await expect(docTree.getByText(topParent)).toBeHidden();
await expect(
docTree.getByText(subDocName, {
exact: true,
}),
).toBeVisible();
await expect(docTree.getByText(subsubDocName)).toBeVisible();
await expect(
docTree
.locator(".--docs-sub-page-item[aria-disabled='true']")
.getByText(subsubDocName),
).toBeVisible();
await page.getByRole('button', { name: 'Restore' }).click();
await expect(page.getByLabel('Alert deleted document')).toBeHidden();
await expect(page.locator('.bn-editor')).toHaveAttribute(
'contenteditable',
'true',
);
await expect(page.getByRole('button', { name: 'Share' })).toBeEnabled();
await expect(docTree.getByText(topParent)).toBeVisible();
});
}); });

View File

@@ -327,6 +327,11 @@ export async function waitForLanguageSwitch(
await page.getByRole('menuitem', { name: lang.label }).click(); await page.getByRole('menuitem', { name: lang.label }).click();
} }
export const clickInEditorMenu = async (page: Page, textButton: string) => {
await page.getByRole('button', { name: 'Open the document options' }).click();
await page.getByRole('menuitem', { name: textButton }).click();
};
export const clickInGridMenu = async ( export const clickInGridMenu = async (
page: Page, page: Page,
row: Locator, row: Locator,

View File

@@ -1,6 +1,7 @@
import { Page, expect } from '@playwright/test'; import { Page, expect } from '@playwright/test';
import { import {
BrowserName,
randomName, randomName,
updateDocTitle, updateDocTitle,
verifyDocName, verifyDocName,
@@ -9,7 +10,7 @@ import {
export const createRootSubPage = async ( export const createRootSubPage = async (
page: Page, page: Page,
browserName: string, browserName: BrowserName,
docName: string, docName: string,
isMobile: boolean = false, isMobile: boolean = false,
) => { ) => {
@@ -67,6 +68,47 @@ export const clickOnAddRootSubPage = async (page: Page) => {
await rootItem.getByTestId('doc-tree-item-actions-add-child').click(); await rootItem.getByTestId('doc-tree-item-actions-add-child').click();
}; };
export const addChild = async ({
page,
browserName,
docParent,
}: {
page: Page;
browserName: BrowserName;
docParent: string;
}) => {
let item = page.getByTestId('doc-tree-root-item');
const isParent = await item
.filter({
hasText: docParent,
})
.first()
.count();
if (!isParent) {
const items = page.getByRole('treeitem');
item = items
.filter({
hasText: docParent,
})
.first();
}
await item.hover();
await item.getByTestId('doc-tree-item-actions-add-child').click();
const [name] = randomName(docParent, browserName, 1);
await updateDocTitle(page, name);
return name;
};
export const navigateToTopParentFromTree = async ({ page }: { page: Page }) => {
await page.getByRole('link', { name: /Open root document/ }).click();
};
export const navigateToPageFromTree = async ({ export const navigateToPageFromTree = async ({
page, page,
title, title,

View File

@@ -7,6 +7,7 @@ import { Box, BoxType } from '.';
export const Card = ({ export const Card = ({
children, children,
className,
$css, $css,
...props ...props
}: PropsWithChildren<BoxType>) => { }: PropsWithChildren<BoxType>) => {
@@ -14,7 +15,7 @@ export const Card = ({
return ( return (
<Box <Box
className={`--docs--card ${props.className || ''}`} className={`--docs--card ${className || ''}`}
$background="white" $background="white"
$radius="4px" $radius="4px"
$css={css` $css={css`

View File

@@ -0,0 +1,36 @@
import { PropsWithChildren } from 'react';
import { css } from 'styled-components';
import { Box, BoxType } from '.';
type OverlayerProps = PropsWithChildren<{
isOverlay: boolean;
}> &
Partial<BoxType>;
export const Overlayer = ({
children,
className,
$css,
isOverlay,
...props
}: OverlayerProps) => {
if (!isOverlay) {
return children;
}
return (
<Box
className={`--docs--overlayer ${className || ''}`}
$opacity="0.4"
$zIndex="10"
$css={css`
${$css}
pointer-events: none;
`}
{...props}
>
{children}
</Box>
);
};

View File

@@ -9,6 +9,7 @@ export * from './InfiniteScroll';
export * from './Link'; export * from './Link';
export * from './Loading'; export * from './Loading';
export * from './modal'; export * from './modal';
export * from './Overlayer';
export * from './separators'; export * from './separators';
export * from './Text'; export * from './Text';
export * from './TextErrors'; export * from './TextErrors';

View File

@@ -83,6 +83,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
const { isEditable, isLoading } = useIsCollaborativeEditable(doc); const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
const isConnectedToCollabServer = provider.isSynced; const isConnectedToCollabServer = provider.isSynced;
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading; const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
const isDeletedDoc = !!doc.deleted_at;
useSaveDoc(doc.id, provider.document, !readOnly, isConnectedToCollabServer); useSaveDoc(doc.id, provider.document, !readOnly, isConnectedToCollabServer);
const { i18n } = useTranslation(); const { i18n } = useTranslation();
@@ -180,7 +181,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
<Box <Box
$padding={{ top: 'md' }} $padding={{ top: 'md' }}
$background="white" $background="white"
$css={cssEditor(readOnly)} $css={cssEditor(readOnly, isDeletedDoc)}
className="--docs--editor-container" className="--docs--editor-container"
> >
{errorAttachment && ( {errorAttachment && (
@@ -231,7 +232,7 @@ export const BlockNoteEditorVersion = ({
); );
return ( return (
<Box $css={cssEditor(readOnly)} className="--docs--editor-container"> <Box $css={cssEditor(readOnly, true)} className="--docs--editor-container">
<BlockNoteView editor={editor} editable={!readOnly} theme="light" /> <BlockNoteView editor={editor} editable={!readOnly} theme="light" />
</Box> </Box>
); );

View File

@@ -1,6 +1,6 @@
import { css } from 'styled-components'; import { css } from 'styled-components';
export const cssEditor = (readonly: boolean) => css` export const cssEditor = (readonly: boolean, isDeletedDoc: boolean) => css`
&, &,
& > .bn-container, & > .bn-container,
& .ProseMirror { & .ProseMirror {
@@ -127,6 +127,13 @@ export const cssEditor = (readonly: boolean) => css`
.bn-block-outer:not([data-prev-depth-changed]):before { .bn-block-outer:not([data-prev-depth-changed]):before {
border-left: none; border-left: none;
} }
${isDeletedDoc &&
`
.node-interlinkingLinkInline button {
pointer-events: none;
}
`}
} }
& .bn-editor { & .bn-editor {

View File

@@ -0,0 +1,98 @@
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, BoxButton, Icon, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import {
Doc,
KEY_DOC,
KEY_LIST_DOC,
useRestoreDoc,
} from '@/docs/doc-management';
import { KEY_LIST_DOC_TRASHBIN } from '@/docs/docs-grid';
export const AlertRestore = ({ doc }: { doc: Doc }) => {
const { t } = useTranslation();
const { toast } = useToastProvider();
const treeContext = useTreeContext<Doc>();
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
const { mutate: restoreDoc, error } = useRestoreDoc({
listInvalidQueries: [KEY_LIST_DOC, KEY_LIST_DOC_TRASHBIN, KEY_DOC],
options: {
onSuccess: (_data) => {
// It will force the tree to be reloaded
treeContext?.setRoot(undefined as unknown as Doc);
toast(t('The document has been restored.'), VariantType.SUCCESS, {
duration: 4000,
});
},
onError: () => {
toast(
t('An error occurred while restoring the document: {{error}}', {
error: error?.message,
}),
VariantType.ERROR,
{
duration: 4000,
},
);
},
},
});
return (
<Box
className="--docs--alert-restore"
aria-label={t('Alert deleted document')}
$color={colorsTokens['danger-800']}
$background={colorsTokens['danger-100']}
$radius={spacingsTokens['3xs']}
$direction="row"
$padding="xs"
$flex={1}
$align="center"
$gap={spacingsTokens['3xs']}
$css={css`
border: 1px solid var(--c--theme--colors--danger-300, #e3e3fd);
`}
$justify="space-between"
>
<Box $direction="row" $align="center" $gap={spacingsTokens['2xs']}>
<Icon
$theme="danger"
$variation="700"
data-testid="public-icon"
iconName="delete"
variant="symbols-outlined"
/>
<Text $theme="danger" $variation="700" $weight="500">
{t('Document deleted')}
</Text>
</Box>
<BoxButton
onClick={() =>
restoreDoc({
docId: doc.id,
})
}
$direction="row"
$gap="0.2rem"
$align="center"
>
<Icon
iconName="undo"
$theme="danger"
$variation="600"
$size="18px"
variant="symbols-outlined"
/>
<Text $theme="danger" $variation="600" $size="s">
{t('Restore')}
</Text>
</BoxButton>
</Box>
);
};

View File

@@ -0,0 +1,87 @@
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
import { Button } from '@openfun/cunningham-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, Icon } from '@/components';
import { Doc } from '@/docs/doc-management';
interface BoutonShareProps {
displayNbAccess: boolean;
doc: Doc;
isDisabled?: boolean;
isHidden?: boolean;
open: () => void;
}
export const BoutonShare = ({
displayNbAccess,
doc,
isDisabled,
isHidden,
open,
}: BoutonShareProps) => {
const { t } = useTranslation();
const treeContext = useTreeContext<Doc>();
/**
* Following the change where there is no default owner when adding a sub-page,
* we need to handle both the case where the doc is the root and the case of sub-pages.
*/
const hasAccesses = useMemo(() => {
if (treeContext?.root?.id === doc.id) {
return doc.nb_accesses_direct > 1 && displayNbAccess;
}
return doc.nb_accesses_direct >= 1 && displayNbAccess;
}, [doc.id, treeContext?.root, doc.nb_accesses_direct, displayNbAccess]);
if (isHidden) {
return null;
}
if (hasAccesses) {
return (
<Box
$css={css`
.c__button--medium {
height: 32px;
padding: 10px var(--c--theme--spacings--xs);
gap: 7px;
}
`}
>
<Button
color="tertiary"
aria-label={t('Share button')}
icon={
<Icon
iconName="group"
$theme="primary"
$variation="800"
variant="filled"
disabled={isDisabled}
/>
}
onClick={open}
size="medium"
disabled={isDisabled}
>
{doc.nb_accesses_direct}
</Button>
</Box>
);
}
return (
<Button
color="tertiary-text"
onClick={open}
size="medium"
disabled={isDisabled}
>
{t('Share')}
</Button>
);
};

View File

@@ -1,7 +1,7 @@
import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Box, HorizontalSeparator, Text } from '@/components'; import { Box, HorizontalSeparator, Text } from '@/components';
import { useConfig } from '@/core';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import { import {
Doc, Doc,
@@ -11,10 +11,13 @@ import {
useIsCollaborativeEditable, useIsCollaborativeEditable,
useTrans, useTrans,
} from '@/docs/doc-management'; } from '@/docs/doc-management';
import { useDate } from '@/hook';
import { useResponsiveStore } from '@/stores'; import { useResponsiveStore } from '@/stores';
import { AlertNetwork } from './AlertNetwork'; import { AlertNetwork } from './AlertNetwork';
import { AlertPublic } from './AlertPublic'; import { AlertPublic } from './AlertPublic';
import { AlertRestore } from './AlertRestore';
import { BoutonShare } from './BoutonShare';
import { DocTitle } from './DocTitle'; import { DocTitle } from './DocTitle';
import { DocToolBox } from './DocToolBox'; import { DocToolBox } from './DocToolBox';
@@ -30,6 +33,22 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
const { isEditable } = useIsCollaborativeEditable(doc); const { isEditable } = useIsCollaborativeEditable(doc);
const docIsPublic = getDocLinkReach(doc) === LinkReach.PUBLIC; const docIsPublic = getDocLinkReach(doc) === LinkReach.PUBLIC;
const docIsAuth = getDocLinkReach(doc) === LinkReach.AUTHENTICATED; const docIsAuth = getDocLinkReach(doc) === LinkReach.AUTHENTICATED;
const { relativeDate, calculateDaysLeft } = useDate();
const { data: config } = useConfig();
const isDeletedDoc = !!doc.deleted_at;
let dateToDisplay = t('Last update: {{update}}', {
update: relativeDate(doc.updated_at),
});
if (config?.TRASHBIN_CUTOFF_DAYS && doc.deleted_at) {
const daysLeft = calculateDaysLeft(
doc.deleted_at,
config.TRASHBIN_CUTOFF_DAYS,
);
dateToDisplay = `${t('Days remaining:')} ${daysLeft} ${t('days', { count: daysLeft })}`;
}
return ( return (
<> <>
@@ -40,6 +59,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
aria-label={t('It is the card information about the document.')} aria-label={t('It is the card information about the document.')}
className="--docs--doc-header" className="--docs--doc-header"
> >
{isDeletedDoc && <AlertRestore doc={doc} />}
{!isEditable && <AlertNetwork />} {!isEditable && <AlertNetwork />}
{(docIsPublic || docIsAuth) && ( {(docIsPublic || docIsAuth) && (
<AlertPublic isPublicDoc={docIsPublic} /> <AlertPublic isPublicDoc={docIsPublic} />
@@ -78,20 +98,26 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
&nbsp;·&nbsp; &nbsp;·&nbsp;
</Text> </Text>
<Text $variation="600" $size="s"> <Text $variation="600" $size="s">
{t('Last update: {{update}}', { {dateToDisplay}
update: DateTime.fromISO(doc.updated_at).toRelative(),
})}
</Text> </Text>
</> </>
)} )}
{!isDesktop && ( {!isDesktop && (
<Text $variation="400" $size="s"> <Text $variation="400" $size="s">
{DateTime.fromISO(doc.updated_at).toRelative()} {dateToDisplay}
</Text> </Text>
)} )}
</Box> </Box>
</Box> </Box>
<DocToolBox doc={doc} /> {!isDeletedDoc && <DocToolBox doc={doc} />}
{isDeletedDoc && (
<BoutonShare
doc={doc}
open={() => {}}
displayNbAccess={true}
isDisabled
/>
)}
</Box> </Box>
</Box> </Box>
<HorizontalSeparator $withPadding={false} /> <HorizontalSeparator $withPadding={false} />

View File

@@ -2,7 +2,7 @@ import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
import { Button, useModal } from '@openfun/cunningham-react'; import { Button, useModal } from '@openfun/cunningham-react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { css } from 'styled-components'; import { css } from 'styled-components';
@@ -23,6 +23,7 @@ import {
useCopyDocLink, useCopyDocLink,
useCreateFavoriteDoc, useCreateFavoriteDoc,
useDeleteFavoriteDoc, useDeleteFavoriteDoc,
useDocUtils,
useDuplicateDoc, useDuplicateDoc,
} from '@/docs/doc-management'; } from '@/docs/doc-management';
import { DocShareModal } from '@/docs/doc-share'; import { DocShareModal } from '@/docs/doc-share';
@@ -35,6 +36,8 @@ import { useResponsiveStore } from '@/stores';
import { useCopyCurrentEditorToClipboard } from '../hooks/useCopyCurrentEditorToClipboard'; import { useCopyCurrentEditorToClipboard } from '../hooks/useCopyCurrentEditorToClipboard';
import { BoutonShare } from './BoutonShare';
const ModalExport = Export?.ModalExport; const ModalExport = Export?.ModalExport;
interface DocToolBoxProps { interface DocToolBoxProps {
@@ -44,21 +47,9 @@ interface DocToolBoxProps {
export const DocToolBox = ({ doc }: DocToolBoxProps) => { export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const treeContext = useTreeContext<Doc>(); const treeContext = useTreeContext<Doc>();
/**
* Following the change where there is no default owner when adding a sub-page,
* we need to handle both the case where the doc is the root and the case of sub-pages.
*/
const hasAccesses = useMemo(() => {
if (treeContext?.root?.id === doc.id) {
return doc.nb_accesses_direct > 1 && doc.abilities.accesses_view;
}
return doc.nb_accesses_direct >= 1 && doc.abilities.accesses_view;
}, [doc, treeContext?.root]);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const { isChild } = useDocUtils(doc);
const { spacingsTokens, colorsTokens } = useCunninghamTheme(); const { spacingsTokens, colorsTokens } = useCunninghamTheme();
@@ -164,7 +155,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
}, },
}, },
{ {
label: t('Delete document'), label: isChild ? t('Delete sub-document') : t('Delete document'),
icon: 'delete', icon: 'delete',
disabled: !doc.abilities.destroy, disabled: !doc.abilities.destroy,
callback: () => { callback: () => {
@@ -190,46 +181,12 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
$margin={{ left: 'auto' }} $margin={{ left: 'auto' }}
$gap={spacingsTokens['2xs']} $gap={spacingsTokens['2xs']}
> >
{!isSmallMobile && ( <BoutonShare
<> doc={doc}
{!hasAccesses && ( open={modalShare.open}
<Button isHidden={isSmallMobile}
color="tertiary-text" displayNbAccess={doc.abilities.accesses_view}
onClick={() => { />
modalShare.open();
}}
size={isSmallMobile ? 'small' : 'medium'}
>
{t('Share')}
</Button>
)}
{hasAccesses && (
<Box
$css={css`
.c__button--medium {
height: 32px;
padding: 10px var(--c--theme--spacings--xs);
gap: 7px;
}
`}
>
<Button
color="tertiary"
aria-label={t('Share button')}
icon={
<Icon iconName="group" $theme="primary" $variation="800" />
}
onClick={() => {
modalShare.open();
}}
size={isSmallMobile ? 'small' : 'medium'}
>
{doc.nb_accesses_direct}
</Button>
</Box>
)}
</>
)}
{!isSmallMobile && ModalExport && ( {!isSmallMobile && ModalExport && (
<Button <Button
@@ -283,7 +240,26 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
<ModalExport onClose={() => setIsModalExportOpen(false)} doc={doc} /> <ModalExport onClose={() => setIsModalExportOpen(false)} doc={doc} />
)} )}
{isModalRemoveOpen && ( {isModalRemoveOpen && (
<ModalRemoveDoc onClose={() => setIsModalRemoveOpen(false)} doc={doc} /> <ModalRemoveDoc
onClose={() => setIsModalRemoveOpen(false)}
doc={doc}
onSuccess={() => {
const isTopParent = doc.id === treeContext?.root?.id;
const parentId =
treeContext?.treeData.getParentId(doc.id) ||
treeContext?.root?.id;
if (isTopParent) {
void router.push(`/`);
} else if (parentId) {
void router.push(`/docs/${parentId}`).then(() => {
setTimeout(() => {
treeContext?.treeData.deleteNode(doc.id);
}, 100);
});
}
}}
/>
)} )}
{selectHistoryModal.isOpen && ( {selectHistoryModal.isOpen && (
<ModalSelectVersion <ModalSelectVersion

View File

@@ -5,7 +5,6 @@ import {
VariantType, VariantType,
useToastProvider, useToastProvider,
} from '@openfun/cunningham-react'; } from '@openfun/cunningham-react';
import { usePathname } from 'next/navigation';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
@@ -35,7 +34,6 @@ export const ModalRemoveDoc = ({
const trashBinCutoffDays = config?.TRASHBIN_CUTOFF_DAYS || 30; const trashBinCutoffDays = config?.TRASHBIN_CUTOFF_DAYS || 30;
const { push } = useRouter(); const { push } = useRouter();
const { hasChildren } = useDocUtils(doc); const { hasChildren } = useDocUtils(doc);
const pathname = usePathname();
const { const {
mutate: removeDoc, mutate: removeDoc,
isError, isError,
@@ -46,12 +44,12 @@ export const ModalRemoveDoc = ({
onSuccess: () => { onSuccess: () => {
if (onSuccess) { if (onSuccess) {
onSuccess(doc); onSuccess(doc);
} else if (pathname === '/') {
onClose();
} else { } else {
void push('/'); void push('/');
} }
onClose();
toast(t('The document has been deleted.'), VariantType.SUCCESS, { toast(t('The document has been deleted.'), VariantType.SUCCESS, {
duration: 4000, duration: 4000,
}); });

View File

@@ -1,3 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg
<path d="M5.40918 4.69434C5.28613 4.69434 5.18359 4.65332 5.10156 4.57129C5.02409 4.48926 4.98535 4.389 4.98535 4.27051C4.98535 4.15202 5.02409 4.05404 5.10156 3.97656C5.18359 3.89453 5.28613 3.85352 5.40918 3.85352H10.5977C10.7161 3.85352 10.8141 3.89453 10.8916 3.97656C10.9736 4.05404 11.0146 4.15202 11.0146 4.27051C11.0146 4.389 10.9736 4.48926 10.8916 4.57129C10.8141 4.65332 10.7161 4.69434 10.5977 4.69434H5.40918ZM5.40918 7.08008C5.28613 7.08008 5.18359 7.03906 5.10156 6.95703C5.02409 6.875 4.98535 6.77474 4.98535 6.65625C4.98535 6.53776 5.02409 6.43978 5.10156 6.3623C5.18359 6.28027 5.28613 6.23926 5.40918 6.23926H10.5977C10.7161 6.23926 10.8141 6.28027 10.8916 6.3623C10.9736 6.43978 11.0146 6.53776 11.0146 6.65625C11.0146 6.77474 10.9736 6.875 10.8916 6.95703C10.8141 7.03906 10.7161 7.08008 10.5977 7.08008H5.40918ZM5.40918 9.46582C5.28613 9.46582 5.18359 9.42708 5.10156 9.34961C5.02409 9.26758 4.98535 9.1696 4.98535 9.05566C4.98535 8.93262 5.02409 8.83008 5.10156 8.74805C5.18359 8.66602 5.28613 8.625 5.40918 8.625H7.86328C7.98633 8.625 8.08659 8.66602 8.16406 8.74805C8.24609 8.83008 8.28711 8.93262 8.28711 9.05566C8.28711 9.1696 8.24609 9.26758 8.16406 9.34961C8.08659 9.42708 7.98633 9.46582 7.86328 9.46582H5.40918ZM2.25098 13.2529V2.88281C2.25098 2.17188 2.42643 1.63639 2.77734 1.27637C3.13281 0.916341 3.66374 0.736328 4.37012 0.736328H11.6299C12.3363 0.736328 12.8649 0.916341 13.2158 1.27637C13.5713 1.63639 13.749 2.17188 13.749 2.88281V13.2529C13.749 13.9684 13.5713 14.5039 13.2158 14.8594C12.8649 15.2148 12.3363 15.3926 11.6299 15.3926H4.37012C3.66374 15.3926 3.13281 15.2148 2.77734 14.8594C2.42643 14.5039 2.25098 13.9684 2.25098 13.2529ZM3.35156 13.2324C3.35156 13.5742 3.44043 13.8363 3.61816 14.0186C3.80046 14.2008 4.06934 14.292 4.4248 14.292H11.5752C11.9307 14.292 12.1973 14.2008 12.375 14.0186C12.5573 13.8363 12.6484 13.5742 12.6484 13.2324V2.90332C12.6484 2.56152 12.5573 2.29948 12.375 2.11719C12.1973 1.93034 11.9307 1.83691 11.5752 1.83691H4.4248C4.06934 1.83691 3.80046 1.93034 3.61816 2.11719C3.44043 2.29948 3.35156 2.56152 3.35156 2.90332V13.2324Z" fill="#8585F6"/> width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.40918 4.69434C5.28613 4.69434 5.18359 4.65332 5.10156 4.57129C5.02409 4.48926 4.98535 4.389 4.98535 4.27051C4.98535 4.15202 5.02409 4.05404 5.10156 3.97656C5.18359 3.89453 5.28613 3.85352 5.40918 3.85352H10.5977C10.7161 3.85352 10.8141 3.89453 10.8916 3.97656C10.9736 4.05404 11.0146 4.15202 11.0146 4.27051C11.0146 4.389 10.9736 4.48926 10.8916 4.57129C10.8141 4.65332 10.7161 4.69434 10.5977 4.69434H5.40918ZM5.40918 7.08008C5.28613 7.08008 5.18359 7.03906 5.10156 6.95703C5.02409 6.875 4.98535 6.77474 4.98535 6.65625C4.98535 6.53776 5.02409 6.43978 5.10156 6.3623C5.18359 6.28027 5.28613 6.23926 5.40918 6.23926H10.5977C10.7161 6.23926 10.8141 6.28027 10.8916 6.3623C10.9736 6.43978 11.0146 6.53776 11.0146 6.65625C11.0146 6.77474 10.9736 6.875 10.8916 6.95703C10.8141 7.03906 10.7161 7.08008 10.5977 7.08008H5.40918ZM5.40918 9.46582C5.28613 9.46582 5.18359 9.42708 5.10156 9.34961C5.02409 9.26758 4.98535 9.1696 4.98535 9.05566C4.98535 8.93262 5.02409 8.83008 5.10156 8.74805C5.18359 8.66602 5.28613 8.625 5.40918 8.625H7.86328C7.98633 8.625 8.08659 8.66602 8.16406 8.74805C8.24609 8.83008 8.28711 8.93262 8.28711 9.05566C8.28711 9.1696 8.24609 9.26758 8.16406 9.34961C8.08659 9.42708 7.98633 9.46582 7.86328 9.46582H5.40918ZM2.25098 13.2529V2.88281C2.25098 2.17188 2.42643 1.63639 2.77734 1.27637C3.13281 0.916341 3.66374 0.736328 4.37012 0.736328H11.6299C12.3363 0.736328 12.8649 0.916341 13.2158 1.27637C13.5713 1.63639 13.749 2.17188 13.749 2.88281V13.2529C13.749 13.9684 13.5713 14.5039 13.2158 14.8594C12.8649 15.2148 12.3363 15.3926 11.6299 15.3926H4.37012C3.66374 15.3926 3.13281 15.2148 2.77734 14.8594C2.42643 14.5039 2.25098 13.9684 2.25098 13.2529ZM3.35156 13.2324C3.35156 13.5742 3.44043 13.8363 3.61816 14.0186C3.80046 14.2008 4.06934 14.292 4.4248 14.292H11.5752C11.9307 14.292 12.1973 14.2008 12.375 14.0186C12.5573 13.8363 12.6484 13.5742 12.6484 13.2324V2.90332C12.6484 2.56152 12.5573 2.29948 12.375 2.11719C12.1973 1.93034 11.9307 1.83691 11.5752 1.83691H4.4248C4.06934 1.83691 3.80046 1.93034 3.61816 2.11719C3.44043 2.29948 3.35156 2.56152 3.35156 2.90332V13.2324Z"
fill="currentColor"
/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -39,7 +39,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
const treeContext = useTreeContext<Doc>(); const treeContext = useTreeContext<Doc>();
const { untitledDocument } = useTrans(); const { untitledDocument } = useTrans();
const { node } = props; const { node } = props;
const { spacingsTokens } = useCunninghamTheme(); const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const { isDesktop } = useResponsiveStore(); const { isDesktop } = useResponsiveStore();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -101,6 +101,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
const isExpanded = node.isOpen; const isExpanded = node.isOpen;
const isSelected = isSelectedNow; const isSelected = isSelectedNow;
const ariaLabel = docTitle; const ariaLabel = docTitle;
const isDisabled = !!doc.deleted_at;
return ( return (
<Box <Box
@@ -111,6 +112,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
aria-label={ariaLabel} aria-label={ariaLabel}
aria-selected={isSelected} aria-selected={isSelected}
aria-expanded={hasChildren ? isExpanded : undefined} aria-expanded={hasChildren ? isExpanded : undefined}
aria-disabled={isDisabled}
$css={css` $css={css`
background-color: ${menuOpen background-color: ${menuOpen
? 'var(--c--theme--colors--greyscale-100)' ? 'var(--c--theme--colors--greyscale-100)'
@@ -164,7 +166,11 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
`} `}
> >
<Box $width="16px" $height="16px"> <Box $width="16px" $height="16px">
<DocIcon emoji={emoji} defaultIcon={<SubPageIcon />} $size="sm" /> <DocIcon
emoji={emoji}
defaultIcon={<SubPageIcon color={colorsTokens['primary-400']} />}
$size="sm"
/>
</Box> </Box>
<Box <Box

View File

@@ -10,7 +10,7 @@ import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { css } from 'styled-components'; import { css } from 'styled-components';
import { Box, StyledLink } from '@/components'; import { Box, Overlayer, StyledLink } from '@/components';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import { Doc, SimpleDocItem } from '@/docs/doc-management'; import { Doc, SimpleDocItem } from '@/docs/doc-management';
@@ -289,29 +289,31 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
{initialOpenState && {initialOpenState &&
treeContext.treeData.nodes.length > 0 && treeContext.treeData.nodes.length > 0 &&
treeRoot && ( treeRoot && (
<TreeView <Overlayer isOverlay={currentDoc.deleted_at != null} inert>
dndRootElement={treeRoot} <TreeView
initialOpenState={initialOpenState} dndRootElement={treeRoot}
afterMove={handleMove} initialOpenState={initialOpenState}
selectedNodeId={ afterMove={handleMove}
treeContext.treeData.selectedNode?.id ?? selectedNodeId={
treeContext.initialTargetId ?? treeContext.treeData.selectedNode?.id ??
undefined treeContext.initialTargetId ??
} undefined
canDrop={({ parentNode }) => {
const parentDoc = parentNode?.data.value as Doc;
if (!parentDoc) {
return currentDoc.abilities.move && isDesktop;
} }
return parentDoc.abilities.move && isDesktop; canDrop={({ parentNode }) => {
}} const parentDoc = parentNode?.data.value as Doc;
canDrag={(node) => { if (!parentDoc) {
const doc = node.value as Doc; return currentDoc.abilities.move && isDesktop;
return doc.abilities.move && isDesktop; }
}} return parentDoc.abilities.move && isDesktop;
rootNodeId={treeContext.root.id} }}
renderNode={DocSubPageItem} canDrag={(node) => {
/> const doc = node.value as Doc;
return doc.abilities.move && isDesktop;
}}
rootNodeId={treeContext.root.id}
renderNode={DocSubPageItem}
/>
</Overlayer>
)} )}
</Box> </Box>
); );

View File

@@ -126,16 +126,13 @@ export const DocTreeItemActions = ({
}); });
const onSuccessDelete = () => { const onSuccessDelete = () => {
if (parentId) { const isTopParent = doc.id === treeContext?.root?.id && !parentId;
void router.push(`/docs/${parentId}`).then(() => { const parentIdComputed = parentId || treeContext?.root?.id;
setTimeout(() => {
treeContext?.treeData.deleteNode(doc.id); if (isTopParent) {
}, 100);
});
} else if (doc.id === treeContext?.root?.id && !parentId) {
void router.push(`/`); void router.push(`/`);
} else if (treeContext && treeContext.root) { } else if (parentIdComputed) {
void router.push(`/docs/${treeContext.root.id}`).then(() => { void router.push(`/docs/${parentIdComputed}`).then(() => {
setTimeout(() => { setTimeout(() => {
treeContext?.treeData.deleteNode(doc.id); treeContext?.treeData.deleteNode(doc.id);
}, 100); }, 100);

View File

@@ -16,6 +16,7 @@ import {
useDoc, useDoc,
useDocStore, useDocStore,
useProviderStore, useProviderStore,
useTrans,
} from '@/docs/doc-management/'; } from '@/docs/doc-management/';
import { KEY_AUTH, setAuthUrl, useAuth } from '@/features/auth'; import { KEY_AUTH, setAuthUrl, useAuth } from '@/features/auth';
import { getDocChildren, subPageToTree } from '@/features/docs/doc-tree/'; import { getDocChildren, subPageToTree } from '@/features/docs/doc-tree/';
@@ -89,6 +90,7 @@ const DocPage = ({ id }: DocProps) => {
useCollaboration(doc?.id, doc?.content); useCollaboration(doc?.id, doc?.content);
const { t } = useTranslation(); const { t } = useTranslation();
const { authenticated } = useAuth(); const { authenticated } = useAuth();
const { untitledDocument } = useTrans();
/** /**
* Scroll to top when navigating to a new document * Scroll to top when navigating to a new document
@@ -194,11 +196,11 @@ const DocPage = ({ id }: DocProps) => {
<> <>
<Head> <Head>
<title> <title>
{doc.title} - {t('Docs')} {doc.title || untitledDocument} - {t('Docs')}
</title> </title>
<meta <meta
property="og:title" property="og:title"
content={`${doc.title} - ${t('Docs')}`} content={`${doc.title || untitledDocument} - ${t('Docs')}`}
key="title" key="title"
/> />
</Head> </Head>