(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' }),
).toBeVisible();
await expect(page.getByText(`This document and any sub-`)).toBeVisible();
await expect(page.getByText(`This document will be`)).toBeVisible();
await page
.getByRole('button', {

View File

@@ -1,12 +1,18 @@
import { expect, test } from '@playwright/test';
import {
clickInEditorMenu,
clickInGridMenu,
createDoc,
getGridRow,
verifyDocName,
} from './utils-common';
import { addNewMember } from './utils-share';
import {
addChild,
createRootSubPage,
navigateToPageFromTree,
} from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -74,4 +80,71 @@ test.describe('Doc Trashbin', () => {
await page.getByRole('link', { name: 'Trashbin' }).click();
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();
}
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 (
page: Page,
row: Locator,

View File

@@ -1,6 +1,7 @@
import { Page, expect } from '@playwright/test';
import {
BrowserName,
randomName,
updateDocTitle,
verifyDocName,
@@ -9,7 +10,7 @@ import {
export const createRootSubPage = async (
page: Page,
browserName: string,
browserName: BrowserName,
docName: string,
isMobile: boolean = false,
) => {
@@ -67,6 +68,47 @@ export const clickOnAddRootSubPage = async (page: Page) => {
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 ({
page,
title,

View File

@@ -7,6 +7,7 @@ import { Box, BoxType } from '.';
export const Card = ({
children,
className,
$css,
...props
}: PropsWithChildren<BoxType>) => {
@@ -14,7 +15,7 @@ export const Card = ({
return (
<Box
className={`--docs--card ${props.className || ''}`}
className={`--docs--card ${className || ''}`}
$background="white"
$radius="4px"
$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 './Loading';
export * from './modal';
export * from './Overlayer';
export * from './separators';
export * from './Text';
export * from './TextErrors';

View File

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

View File

@@ -1,6 +1,6 @@
import { css } from 'styled-components';
export const cssEditor = (readonly: boolean) => css`
export const cssEditor = (readonly: boolean, isDeletedDoc: boolean) => css`
&,
& > .bn-container,
& .ProseMirror {
@@ -127,6 +127,13 @@ export const cssEditor = (readonly: boolean) => css`
.bn-block-outer:not([data-prev-depth-changed]):before {
border-left: none;
}
${isDeletedDoc &&
`
.node-interlinkingLinkInline button {
pointer-events: none;
}
`}
}
& .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 { Box, HorizontalSeparator, Text } from '@/components';
import { useConfig } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import {
Doc,
@@ -11,10 +11,13 @@ import {
useIsCollaborativeEditable,
useTrans,
} from '@/docs/doc-management';
import { useDate } from '@/hook';
import { useResponsiveStore } from '@/stores';
import { AlertNetwork } from './AlertNetwork';
import { AlertPublic } from './AlertPublic';
import { AlertRestore } from './AlertRestore';
import { BoutonShare } from './BoutonShare';
import { DocTitle } from './DocTitle';
import { DocToolBox } from './DocToolBox';
@@ -30,6 +33,22 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
const { isEditable } = useIsCollaborativeEditable(doc);
const docIsPublic = getDocLinkReach(doc) === LinkReach.PUBLIC;
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 (
<>
@@ -40,6 +59,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
aria-label={t('It is the card information about the document.')}
className="--docs--doc-header"
>
{isDeletedDoc && <AlertRestore doc={doc} />}
{!isEditable && <AlertNetwork />}
{(docIsPublic || docIsAuth) && (
<AlertPublic isPublicDoc={docIsPublic} />
@@ -78,20 +98,26 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
&nbsp;·&nbsp;
</Text>
<Text $variation="600" $size="s">
{t('Last update: {{update}}', {
update: DateTime.fromISO(doc.updated_at).toRelative(),
})}
{dateToDisplay}
</Text>
</>
)}
{!isDesktop && (
<Text $variation="400" $size="s">
{DateTime.fromISO(doc.updated_at).toRelative()}
{dateToDisplay}
</Text>
)}
</Box>
</Box>
<DocToolBox doc={doc} />
{!isDeletedDoc && <DocToolBox doc={doc} />}
{isDeletedDoc && (
<BoutonShare
doc={doc}
open={() => {}}
displayNbAccess={true}
isDisabled
/>
)}
</Box>
</Box>
<HorizontalSeparator $withPadding={false} />

View File

@@ -2,7 +2,7 @@ import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
import { Button, useModal } from '@openfun/cunningham-react';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -23,6 +23,7 @@ import {
useCopyDocLink,
useCreateFavoriteDoc,
useDeleteFavoriteDoc,
useDocUtils,
useDuplicateDoc,
} from '@/docs/doc-management';
import { DocShareModal } from '@/docs/doc-share';
@@ -35,6 +36,8 @@ import { useResponsiveStore } from '@/stores';
import { useCopyCurrentEditorToClipboard } from '../hooks/useCopyCurrentEditorToClipboard';
import { BoutonShare } from './BoutonShare';
const ModalExport = Export?.ModalExport;
interface DocToolBoxProps {
@@ -44,21 +47,9 @@ interface DocToolBoxProps {
export const DocToolBox = ({ doc }: DocToolBoxProps) => {
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 && doc.abilities.accesses_view;
}
return doc.nb_accesses_direct >= 1 && doc.abilities.accesses_view;
}, [doc, treeContext?.root]);
const queryClient = useQueryClient();
const router = useRouter();
const { isChild } = useDocUtils(doc);
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',
disabled: !doc.abilities.destroy,
callback: () => {
@@ -190,46 +181,12 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
$margin={{ left: 'auto' }}
$gap={spacingsTokens['2xs']}
>
{!isSmallMobile && (
<>
{!hasAccesses && (
<Button
color="tertiary-text"
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>
)}
</>
)}
<BoutonShare
doc={doc}
open={modalShare.open}
isHidden={isSmallMobile}
displayNbAccess={doc.abilities.accesses_view}
/>
{!isSmallMobile && ModalExport && (
<Button
@@ -283,7 +240,26 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
<ModalExport onClose={() => setIsModalExportOpen(false)} doc={doc} />
)}
{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 && (
<ModalSelectVersion

View File

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

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 { untitledDocument } = useTrans();
const { node } = props;
const { spacingsTokens } = useCunninghamTheme();
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const { isDesktop } = useResponsiveStore();
const { t } = useTranslation();
@@ -101,6 +101,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
const isExpanded = node.isOpen;
const isSelected = isSelectedNow;
const ariaLabel = docTitle;
const isDisabled = !!doc.deleted_at;
return (
<Box
@@ -111,6 +112,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
aria-label={ariaLabel}
aria-selected={isSelected}
aria-expanded={hasChildren ? isExpanded : undefined}
aria-disabled={isDisabled}
$css={css`
background-color: ${menuOpen
? 'var(--c--theme--colors--greyscale-100)'
@@ -164,7 +166,11 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
`}
>
<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

View File

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

View File

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

View File

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