✨(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:
@@ -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', {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`
|
||||
|
||||
36
src/frontend/apps/impress/src/components/Overlayer.tsx
Normal file
36
src/frontend/apps/impress/src/components/Overlayer.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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) => {
|
||||
·
|
||||
</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} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 |
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user