Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9194bf5a90 | |||
| dc63a5839e | |||
| d406846986 | |||
| e85b07021e | |||
| 282200ac3d |
+10
-1
@@ -9,6 +9,14 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.0.1] - 2025-01-17
|
||||
|
||||
## Fixed
|
||||
|
||||
-🐛(frontend) share modal is shown when you don't have the abilities #557
|
||||
-🐛(frontend) title copy break app #564
|
||||
|
||||
|
||||
## [2.0.0] - 2025-01-13
|
||||
|
||||
## Added
|
||||
@@ -353,7 +361,8 @@ and this project adheres to
|
||||
- 🚀 Impress, project to manage your documents easily and collaboratively.
|
||||
|
||||
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v2.0.0...main
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v2.0.1...main
|
||||
[v2.0.1]: https://github.com/numerique-gouv/impress/releases/v2.0.1
|
||||
[v2.0.0]: https://github.com/numerique-gouv/impress/releases/v2.0.0
|
||||
[v1.10.0]: https://github.com/numerique-gouv/impress/releases/v1.10.0
|
||||
[v1.9.0]: https://github.com/numerique-gouv/impress/releases/v1.9.0
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "2.0.0"
|
||||
version = "2.0.1"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -47,6 +47,7 @@ test.describe('Doc Header', () => {
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
accesses_manage: true,
|
||||
accesses_view: true,
|
||||
update: true,
|
||||
partial_update: true,
|
||||
retrieve: true,
|
||||
@@ -394,7 +395,31 @@ test.describe('Doc Header', () => {
|
||||
navigator.clipboard.readText(),
|
||||
);
|
||||
const clipboardContent = await handle.jsonValue();
|
||||
expect(clipboardContent.trim()).toBe(`<h1>Hello World</h1><p></p>`);
|
||||
expect(clipboardContent.trim()).toBe(
|
||||
`<h1 data-level=\"1\">Hello World</h1><p></p>`,
|
||||
);
|
||||
});
|
||||
|
||||
test('it checks the copy link button', async ({ page }) => {
|
||||
await mockedDocument(page, {
|
||||
abilities: {
|
||||
destroy: false, // Means owner
|
||||
link_configuration: true,
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
accesses_manage: false,
|
||||
accesses_view: false,
|
||||
update: true,
|
||||
partial_update: true,
|
||||
retrieve: true,
|
||||
},
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Copy link' }).click();
|
||||
await expect(page.getByText('Link Copied !')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -405,6 +430,45 @@ test.describe('Documents Header mobile', () => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('it checks the copy link button', async ({ page, browserName }) => {
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(
|
||||
browserName === 'webkit',
|
||||
'navigator.clipboard is not working with webkit and playwright',
|
||||
);
|
||||
await mockedDocument(page, {
|
||||
abilities: {
|
||||
destroy: false,
|
||||
link_configuration: true,
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
accesses_manage: false,
|
||||
accesses_view: false,
|
||||
update: true,
|
||||
partial_update: true,
|
||||
retrieve: true,
|
||||
},
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Copy link' })).toBeHidden();
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page.getByRole('button', { name: 'Copy link' }).click();
|
||||
await expect(page.getByText('Link Copied !')).toBeVisible();
|
||||
// Test that clipboard is in HTML format
|
||||
const handle = await page.evaluateHandle(() =>
|
||||
navigator.clipboard.readText(),
|
||||
);
|
||||
const clipboardContent = await handle.jsonValue();
|
||||
|
||||
const origin = await page.evaluate(() => window.location.origin);
|
||||
expect(clipboardContent.trim()).toMatch(
|
||||
`${origin}/docs/mocked-document-id/`,
|
||||
);
|
||||
});
|
||||
|
||||
test('it checks the close button on Share modal', async ({ page }) => {
|
||||
await mockedDocument(page, {
|
||||
abilities: {
|
||||
@@ -414,6 +478,7 @@ test.describe('Documents Header mobile', () => {
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
accesses_manage: true,
|
||||
accesses_view: true,
|
||||
update: true,
|
||||
partial_update: true,
|
||||
retrieve: true,
|
||||
|
||||
@@ -79,7 +79,7 @@ test.describe('Document create member', () => {
|
||||
await expect(quickSearchContent.getByText(email).first()).toBeVisible();
|
||||
|
||||
// Check user added
|
||||
await expect(page.getByText('Share with 3 users')).toBeVisible();
|
||||
await expect(page.getByText('Share with 2 users')).toBeVisible();
|
||||
await expect(
|
||||
quickSearchContent.getByText(users[0].full_name).first(),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -413,14 +413,8 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
await page.goto(urlDoc);
|
||||
|
||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
await expect(selectVisibility).toBeHidden();
|
||||
|
||||
const inputSearch = page.getByRole('combobox', {
|
||||
name: 'Quick search input',
|
||||
});
|
||||
await expect(inputSearch).toBeHidden();
|
||||
await page.getByRole('button', { name: 'Copy link' }).click();
|
||||
await expect(page.getByText('Link Copied !')).toBeVisible();
|
||||
});
|
||||
|
||||
test('It checks a authenticated doc in editable mode', async ({
|
||||
@@ -474,13 +468,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
await page.goto(urlDoc);
|
||||
|
||||
await verifyDocName(page, docTitle);
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
await expect(selectVisibility).toBeHidden();
|
||||
|
||||
const inputSearch = page.getByRole('combobox', {
|
||||
name: 'Quick search input',
|
||||
});
|
||||
await expect(inputSearch).toBeHidden();
|
||||
await page.getByRole('button', { name: 'Copy link' }).click();
|
||||
await expect(page.getByText('Link Copied !')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -15,9 +15,9 @@
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocknote/core": "0.22.0",
|
||||
"@blocknote/mantine": "0.22.0",
|
||||
"@blocknote/react": "0.22.0",
|
||||
"@blocknote/core": "0.21.0",
|
||||
"@blocknote/mantine": "0.21.0",
|
||||
"@blocknote/react": "0.21.0",
|
||||
"@gouvfr-lasuite/integration": "1.0.2",
|
||||
"@hocuspocus/provider": "2.15.0",
|
||||
"@openfun/cunningham-react": "2.9.4",
|
||||
|
||||
+30
-23
@@ -6,6 +6,7 @@ import { useCreateBlockNote } from '@blocknote/react';
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { Box, TextErrors } from '@/components';
|
||||
@@ -20,17 +21,19 @@ import { randomColor } from '../utils';
|
||||
|
||||
import { BlockNoteToolbar } from './BlockNoteToolbar';
|
||||
|
||||
const cssEditor = (readonly: boolean) => `
|
||||
&, & > .bn-container, & .ProseMirror {
|
||||
height:100%;
|
||||
|
||||
.bn-side-menu[data-block-type=heading][data-level="1"] {
|
||||
height: 50px;
|
||||
}
|
||||
.bn-side-menu[data-block-type=heading][data-level="2"] {
|
||||
height: 43px;
|
||||
}
|
||||
.bn-side-menu[data-block-type=heading][data-level="3"] {
|
||||
const cssEditor = (readonly: boolean) => css`
|
||||
&,
|
||||
& > .bn-container,
|
||||
& .ProseMirror {
|
||||
height: 100%;
|
||||
|
||||
.bn-side-menu[data-block-type='heading'][data-level='1'] {
|
||||
height: 50px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='heading'][data-level='2'] {
|
||||
height: 43px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='heading'][data-level='3'] {
|
||||
height: 35px;
|
||||
}
|
||||
h1 {
|
||||
@@ -52,11 +55,11 @@ const cssEditor = (readonly: boolean) => `
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bn-editor {
|
||||
|
||||
color: var(--c--theme--colors--greyscale-700);
|
||||
}
|
||||
|
||||
.bn-block-outer:not(:first-child) {
|
||||
&:has(h1) {
|
||||
padding-top: 32px;
|
||||
@@ -67,25 +70,25 @@ const cssEditor = (readonly: boolean) => `
|
||||
&:has(h3) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
& .bn-inline-content code {
|
||||
background-color: gainsboro;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@media screen and (width <= 560px) {
|
||||
& .bn-editor {
|
||||
|
||||
${readonly && `padding-left: 10px;`}
|
||||
};
|
||||
.bn-side-menu[data-block-type=heading][data-level="1"] {
|
||||
height: 46px;
|
||||
}
|
||||
.bn-side-menu[data-block-type=heading][data-level="2"] {
|
||||
.bn-side-menu[data-block-type='heading'][data-level='1'] {
|
||||
height: 46px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='heading'][data-level='2'] {
|
||||
height: 40px;
|
||||
}
|
||||
.bn-side-menu[data-block-type=heading][data-level="3"] {
|
||||
.bn-side-menu[data-block-type='heading'][data-level='3'] {
|
||||
height: 40px;
|
||||
}
|
||||
& .bn-editor h1 {
|
||||
@@ -97,7 +100,7 @@ const cssEditor = (readonly: boolean) => `
|
||||
& .bn-editor h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.bn-block-content[data-is-empty-and-focused][data-content-type="paragraph"]
|
||||
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
|
||||
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -176,7 +179,11 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
}, [setEditor, editor]);
|
||||
|
||||
return (
|
||||
<Box $css={cssEditor(readOnly)}>
|
||||
<Box
|
||||
$padding={{ top: 'md' }}
|
||||
$background="white"
|
||||
$css={cssEditor(readOnly)}
|
||||
>
|
||||
{errorAttachment && (
|
||||
<Box $margin={{ bottom: 'big' }}>
|
||||
<TextErrors
|
||||
|
||||
@@ -64,7 +64,12 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box $direction="row" $align="center" $width="100%">
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$width="100%"
|
||||
$padding={{ bottom: 'xs' }}
|
||||
>
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
@@ -98,7 +103,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
|
||||
<DocToolBox doc={doc} />
|
||||
</Box>
|
||||
</Box>
|
||||
<HorizontalSeparator $withPadding={true} />
|
||||
<HorizontalSeparator $withPadding={false} />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -16,10 +16,13 @@ import {
|
||||
Icon,
|
||||
IconOptions,
|
||||
} from '@/components';
|
||||
import { useAuthStore } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useEditorStore } from '@/features/docs/doc-editor/';
|
||||
import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management';
|
||||
import {
|
||||
Doc,
|
||||
ModalRemoveDoc,
|
||||
useCopyDocLink,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { DocShareModal } from '@/features/docs/doc-share';
|
||||
import {
|
||||
KEY_LIST_DOC_VERSIONS,
|
||||
@@ -37,6 +40,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
const { t } = useTranslation();
|
||||
const hasAccesses = doc.nb_accesses > 1;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const copyDocLink = useCopyDocLink(doc.id);
|
||||
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const spacings = spacingsTokens();
|
||||
@@ -48,18 +54,24 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
const modalShare = useModal();
|
||||
|
||||
const { isSmallMobile, isDesktop } = useResponsiveStore();
|
||||
const { authenticated } = useAuthStore();
|
||||
const { editor } = useEditorStore();
|
||||
|
||||
const { toast } = useToastProvider();
|
||||
const canViewAccesses = doc.abilities.accesses_view;
|
||||
|
||||
const options: DropdownMenuOption[] = [
|
||||
...(isSmallMobile
|
||||
? [
|
||||
{
|
||||
label: t('Share'),
|
||||
icon: 'upload',
|
||||
label: canViewAccesses ? t('Share') : t('Copy link'),
|
||||
icon: canViewAccesses ? 'group' : 'link',
|
||||
|
||||
callback: () => {
|
||||
modalShare.open();
|
||||
if (canViewAccesses) {
|
||||
modalShare.open();
|
||||
return;
|
||||
}
|
||||
copyDocLink();
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -153,7 +165,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
$margin={{ left: 'auto' }}
|
||||
$gap={spacings['2xs']}
|
||||
>
|
||||
{authenticated && !isSmallMobile && (
|
||||
{canViewAccesses && !isSmallMobile && (
|
||||
<>
|
||||
{!hasAccesses && (
|
||||
<Button
|
||||
@@ -187,12 +199,23 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
}}
|
||||
size={isSmallMobile ? 'small' : 'medium'}
|
||||
>
|
||||
{doc.nb_accesses}
|
||||
{doc.nb_accesses - 1}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!canViewAccesses && !isSmallMobile && (
|
||||
<Button
|
||||
color="tertiary-text"
|
||||
onClick={() => {
|
||||
copyDocLink();
|
||||
}}
|
||||
size={isSmallMobile ? 'small' : 'medium'}
|
||||
>
|
||||
{t('Copy link')}
|
||||
</Button>
|
||||
)}
|
||||
{!isSmallMobile && (
|
||||
<Button
|
||||
color="tertiary-text"
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './useCollaboration';
|
||||
export * from './useTrans';
|
||||
export * from './useCopyDocLink';
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useClipboard } from '@/hook';
|
||||
|
||||
import { Doc } from '../types';
|
||||
|
||||
export const useCopyDocLink = (docId: Doc['id']) => {
|
||||
const { t } = useTranslation();
|
||||
const copyToClipboard = useClipboard();
|
||||
|
||||
return useCallback(() => {
|
||||
copyToClipboard(
|
||||
`${window.location.origin}/docs/${docId}/`,
|
||||
t('Link Copied !'),
|
||||
t('Failed to copy link'),
|
||||
);
|
||||
}, [copyToClipboard, docId, t]);
|
||||
};
|
||||
+41
-32
@@ -56,8 +56,9 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
||||
const [userQuery, setUserQuery] = useState('');
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const [listHeight, setListHeight] = useState<string>('400px');
|
||||
const [listHeight, setListHeight] = useState<string>('0px');
|
||||
const canShare = doc.abilities.accesses_manage;
|
||||
const canViewAccesses = doc.abilities.accesses_view;
|
||||
const showMemberSection = inputValue === '' && selectedUsers.length === 0;
|
||||
const showFooter = selectedUsers.length === 0 && !inputValue;
|
||||
|
||||
@@ -94,7 +95,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
||||
count === 1
|
||||
? t('Document owner')
|
||||
: t('Share with {{count}} users', {
|
||||
count,
|
||||
count: count - 1,
|
||||
}),
|
||||
elements: members,
|
||||
endActions: membersQuery.hasNextPage
|
||||
@@ -168,6 +169,10 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
||||
};
|
||||
|
||||
const handleRef = (node: HTMLDivElement) => {
|
||||
if (!canViewAccesses) {
|
||||
setListHeight('0px');
|
||||
return;
|
||||
}
|
||||
const inputHeight = canShare ? 70 : 0;
|
||||
const marginTop = 11;
|
||||
const footerHeight = node?.clientHeight ?? 0;
|
||||
@@ -191,7 +196,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
||||
<ShareModalStyle />
|
||||
<Box
|
||||
aria-label={t('Share modal')}
|
||||
$height={modalContentHeight}
|
||||
$height={canViewAccesses ? modalContentHeight : 'auto'}
|
||||
$overflow="hidden"
|
||||
$justify="space-between"
|
||||
>
|
||||
@@ -235,39 +240,43 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
||||
loading={searchUsersQuery.isLoading}
|
||||
placeholder={t('Type a name or email')}
|
||||
>
|
||||
{!showMemberSection && inputValue !== '' && (
|
||||
<QuickSearchGroup
|
||||
group={searchUserData}
|
||||
onSelect={onSelect}
|
||||
renderElement={(user) => (
|
||||
<DocShareModalInviteUserRow user={user} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{showMemberSection && (
|
||||
{canViewAccesses && (
|
||||
<>
|
||||
{invitationsData.elements.length > 0 && (
|
||||
<Box aria-label={t('List invitation card')}>
|
||||
<QuickSearchGroup
|
||||
group={invitationsData}
|
||||
renderElement={(invitation) => (
|
||||
<DocShareInvitationItem
|
||||
doc={doc}
|
||||
invitation={invitation}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box aria-label={t('List members card')}>
|
||||
{!showMemberSection && inputValue !== '' && (
|
||||
<QuickSearchGroup
|
||||
group={membersData}
|
||||
renderElement={(access) => (
|
||||
<DocShareMemberItem doc={doc} access={access} />
|
||||
group={searchUserData}
|
||||
onSelect={onSelect}
|
||||
renderElement={(user) => (
|
||||
<DocShareModalInviteUserRow user={user} />
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{showMemberSection && (
|
||||
<>
|
||||
{invitationsData.elements.length > 0 && (
|
||||
<Box aria-label={t('List invitation card')}>
|
||||
<QuickSearchGroup
|
||||
group={invitationsData}
|
||||
renderElement={(invitation) => (
|
||||
<DocShareInvitationItem
|
||||
doc={doc}
|
||||
invitation={invitation}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box aria-label={t('List members card')}>
|
||||
<QuickSearchGroup
|
||||
group={membersData}
|
||||
renderElement={(access) => (
|
||||
<DocShareMemberItem doc={doc} access={access} />
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</QuickSearch>
|
||||
|
||||
+5
-19
@@ -1,13 +1,9 @@
|
||||
import {
|
||||
Button,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, HorizontalSeparator } from '@/components';
|
||||
import { Doc } from '@/features/docs';
|
||||
import { Doc, useCopyDocLink } from '@/features/docs';
|
||||
|
||||
import { DocVisibility } from './DocVisibility';
|
||||
|
||||
@@ -18,7 +14,8 @@ type Props = {
|
||||
|
||||
export const DocShareModalFooter = ({ doc, onClose }: Props) => {
|
||||
const canShare = doc.abilities.accesses_manage;
|
||||
const { toast } = useToastProvider();
|
||||
|
||||
const copyDocLink = useCopyDocLink(doc.id);
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box
|
||||
@@ -41,18 +38,7 @@ export const DocShareModalFooter = ({ doc, onClose }: Props) => {
|
||||
<Button
|
||||
fullWidth={false}
|
||||
onClick={() => {
|
||||
navigator.clipboard
|
||||
.writeText(window.location.href)
|
||||
.then(() => {
|
||||
toast(t('Link Copied !'), VariantType.SUCCESS, {
|
||||
duration: 3000,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast(t('Failed to copy link'), VariantType.ERROR, {
|
||||
duration: 3000,
|
||||
});
|
||||
});
|
||||
copyDocLink();
|
||||
}}
|
||||
color="tertiary"
|
||||
icon={<span className="material-icons">add_link</span>}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import { useResponsiveStore } from '@/stores';
|
||||
const leftPaddingMap: { [key: number]: string } = {
|
||||
3: '1.5rem',
|
||||
2: '0.9rem',
|
||||
1: '0.3',
|
||||
1: '0.3rem',
|
||||
};
|
||||
|
||||
export type HeadingsHighlight = {
|
||||
|
||||
+16
-3
@@ -6,6 +6,7 @@ import {
|
||||
Doc,
|
||||
KEY_LIST_DOC,
|
||||
ModalRemoveDoc,
|
||||
useCopyDocLink,
|
||||
useCreateFavoriteDoc,
|
||||
useDeleteFavoriteDoc,
|
||||
} from '@/features/docs/doc-management';
|
||||
@@ -20,6 +21,11 @@ export const DocsGridActions = ({
|
||||
openShareModal,
|
||||
}: DocsGridActionsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const copyDocLink = useCopyDocLink(doc.id);
|
||||
|
||||
const canViewAccesses = doc.abilities.accesses_view;
|
||||
|
||||
const deleteModal = useModal();
|
||||
|
||||
const removeFavoriteDoc = useDeleteFavoriteDoc({
|
||||
@@ -43,9 +49,16 @@ export const DocsGridActions = ({
|
||||
testId: `docs-grid-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`,
|
||||
},
|
||||
{
|
||||
label: t('Share'),
|
||||
icon: 'group',
|
||||
callback: () => openShareModal?.(),
|
||||
label: canViewAccesses ? t('Share') : t('Copy link'),
|
||||
icon: canViewAccesses ? 'group' : 'link',
|
||||
callback: () => {
|
||||
if (canViewAccesses) {
|
||||
openShareModal?.();
|
||||
return;
|
||||
}
|
||||
copyDocLink();
|
||||
},
|
||||
|
||||
testId: `docs-grid-actions-share-${doc.id}`,
|
||||
},
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Button, useModal } from '@openfun/cunningham-react';
|
||||
import { useModal } from '@openfun/cunningham-react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, StyledLink, Text } from '@/components';
|
||||
import { Doc, LinkReach } from '@/features/docs/doc-management';
|
||||
import { Box, StyledLink, Text } from '@/components';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import { DocShareModal } from '@/features/docs/doc-share';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocsGridActions } from './DocsGridActions';
|
||||
import { DocsGridItemSharedButton } from './DocsGridItemSharedButton';
|
||||
import { SimpleDocItem } from './SimpleDocItem';
|
||||
|
||||
type DocsGridItemProps = {
|
||||
@@ -17,11 +18,6 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
|
||||
const shareModal = useModal();
|
||||
const isPublic = doc.link_reach === LinkReach.PUBLIC;
|
||||
const isAuthenticated = doc.link_reach === LinkReach.AUTHENTICATED;
|
||||
const isRestricted = doc.link_reach === LinkReach.RESTRICTED;
|
||||
const sharedCount = doc.nb_accesses - 1;
|
||||
const isShared = sharedCount > 0;
|
||||
|
||||
const handleShareClick = () => {
|
||||
shareModal.open();
|
||||
@@ -70,49 +66,13 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
|
||||
$justify="flex-end"
|
||||
$gap="32px"
|
||||
>
|
||||
{isDesktop && isPublic && (
|
||||
<Button
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleShareClick();
|
||||
}}
|
||||
size="nano"
|
||||
fullWidth
|
||||
icon={<Icon $variation="000" iconName="public" />}
|
||||
>
|
||||
{isShared ? sharedCount : undefined}
|
||||
</Button>
|
||||
)}
|
||||
{isDesktop && !isPublic && isRestricted && isShared && (
|
||||
<Button
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleShareClick();
|
||||
}}
|
||||
fullWidth
|
||||
color="tertiary"
|
||||
size="nano"
|
||||
icon={<Icon $variation="800" $theme="primary" iconName="group" />}
|
||||
>
|
||||
{sharedCount}
|
||||
</Button>
|
||||
)}
|
||||
{isDesktop && !isPublic && isAuthenticated && (
|
||||
<Button
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleShareClick();
|
||||
}}
|
||||
fullWidth
|
||||
size="nano"
|
||||
icon={<Icon $variation="000" iconName="corporate_fare" />}
|
||||
>
|
||||
{sharedCount}
|
||||
</Button>
|
||||
{isDesktop && (
|
||||
<DocsGridItemSharedButton
|
||||
doc={doc}
|
||||
handleClick={handleShareClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DocsGridActions doc={doc} openShareModal={handleShareClick} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Box, Icon } from '@/components';
|
||||
|
||||
import { Doc, LinkReach } from '../../doc-management';
|
||||
|
||||
type Props = {
|
||||
doc: Doc;
|
||||
handleClick: () => void;
|
||||
};
|
||||
export const DocsGridItemSharedButton = ({ doc, handleClick }: Props) => {
|
||||
const isPublic = doc.link_reach === LinkReach.PUBLIC;
|
||||
const isAuthenticated = doc.link_reach === LinkReach.AUTHENTICATED;
|
||||
const isRestricted = doc.link_reach === LinkReach.RESTRICTED;
|
||||
const sharedCount = doc.nb_accesses - 1;
|
||||
const isShared = sharedCount > 0;
|
||||
|
||||
const icon = useMemo(() => {
|
||||
if (isPublic) {
|
||||
return 'public';
|
||||
}
|
||||
if (isAuthenticated) {
|
||||
return 'corporate_fare';
|
||||
}
|
||||
if (isRestricted) {
|
||||
return 'group';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [isPublic, isAuthenticated, isRestricted]);
|
||||
|
||||
if (!icon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!doc.abilities.accesses_view) {
|
||||
return (
|
||||
<Box $align="center" $width="100%">
|
||||
<Icon $variation="800" $theme="primary" iconName={icon} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleClick();
|
||||
}}
|
||||
fullWidth
|
||||
color={isRestricted ? 'tertiary' : 'primary'}
|
||||
size="nano"
|
||||
icon={
|
||||
<Icon
|
||||
$variation={isRestricted ? '800' : '000'}
|
||||
$theme={isRestricted ? 'primary' : 'greyscale'}
|
||||
iconName={icon}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{isShared ? sharedCount : undefined}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -1 +1,2 @@
|
||||
export * from './useDate';
|
||||
export * from './useClipboard';
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const useClipboard = () => {
|
||||
const { toast } = useToastProvider();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useCallback(
|
||||
(text: string, successMessage?: string, errorMessage?: string) => {
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
toast(
|
||||
successMessage ?? t('Copied to clipboard'),
|
||||
VariantType.SUCCESS,
|
||||
{
|
||||
duration: 3000,
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
toast(
|
||||
errorMessage ?? t('Failed to copy to clipboard'),
|
||||
VariantType.ERROR,
|
||||
{
|
||||
duration: 3000,
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
[t, toast],
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impress",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eslint-config-impress",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js ."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "packages-i18n",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"extract-translation": "yarn extract-translation:impress",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server-y-provider",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"description": "Y.js provider for docs",
|
||||
"repository": "https://github.com/numerique-gouv/impress",
|
||||
"license": "MIT",
|
||||
|
||||
+70
-9
@@ -988,7 +988,56 @@
|
||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@blocknote/core@0.22.0", "@blocknote/core@^0.22.0":
|
||||
"@blocknote/core@0.21.0", "@blocknote/core@^0.21.0":
|
||||
version "0.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@blocknote/core/-/core-0.21.0.tgz#b54baaa3eca3b700c80c59113a837c3d4153dca2"
|
||||
integrity sha512-TQAN0qRCkXpz5AwfdxjuFvIKWsU4bBI5d/e5iX7PEkhwf4PFgPHsMUl2uuGw5o/hhjzoU54YKaAjlJlWyw4goA==
|
||||
dependencies:
|
||||
"@emoji-mart/data" "^1.2.1"
|
||||
"@tiptap/core" "^2.7.1"
|
||||
"@tiptap/extension-bold" "^2.7.1"
|
||||
"@tiptap/extension-code" "^2.7.1"
|
||||
"@tiptap/extension-collaboration" "^2.7.1"
|
||||
"@tiptap/extension-collaboration-cursor" "^2.7.1"
|
||||
"@tiptap/extension-gapcursor" "^2.7.1"
|
||||
"@tiptap/extension-hard-break" "^2.7.1"
|
||||
"@tiptap/extension-history" "^2.7.1"
|
||||
"@tiptap/extension-horizontal-rule" "^2.7.1"
|
||||
"@tiptap/extension-italic" "^2.7.1"
|
||||
"@tiptap/extension-link" "^2.7.1"
|
||||
"@tiptap/extension-paragraph" "^2.7.1"
|
||||
"@tiptap/extension-strike" "^2.7.1"
|
||||
"@tiptap/extension-table-cell" "^2.7.1"
|
||||
"@tiptap/extension-table-header" "^2.7.1"
|
||||
"@tiptap/extension-table-row" "^2.7.1"
|
||||
"@tiptap/extension-text" "^2.7.1"
|
||||
"@tiptap/extension-underline" "^2.7.1"
|
||||
"@tiptap/pm" "^2.7.1"
|
||||
emoji-mart "^5.6.0"
|
||||
hast-util-from-dom "^4.2.0"
|
||||
prosemirror-dropcursor "^1.8.1"
|
||||
prosemirror-highlight "^0.9.0"
|
||||
prosemirror-model "^1.23.0"
|
||||
prosemirror-state "^1.4.3"
|
||||
prosemirror-tables "^1.6.1"
|
||||
prosemirror-transform "^1.9.0"
|
||||
prosemirror-view "^1.33.7"
|
||||
rehype-format "^5.0.0"
|
||||
rehype-parse "^8.0.4"
|
||||
rehype-remark "^9.1.2"
|
||||
rehype-stringify "^9.0.3"
|
||||
remark-gfm "^3.0.1"
|
||||
remark-parse "^10.0.1"
|
||||
remark-rehype "^10.1.0"
|
||||
remark-stringify "^10.0.2"
|
||||
shiki "^1.22.0"
|
||||
unified "^10.1.2"
|
||||
uuid "^8.3.2"
|
||||
y-prosemirror "1.2.13"
|
||||
y-protocols "^1.0.6"
|
||||
yjs "^13.6.15"
|
||||
|
||||
"@blocknote/core@^0.22.0":
|
||||
version "0.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@blocknote/core/-/core-0.22.0.tgz#2f363f9677d4fa5f20299b22850f5f34a6340a55"
|
||||
integrity sha512-AAEx01zK6u+b1SsZniMm/aogEMjasF4vA9ZHgFGj04G7AwK5Hjwa0Sxre58qcW+KzuvR09CQHTkwjmgVmJX/HA==
|
||||
@@ -1037,19 +1086,31 @@
|
||||
y-protocols "^1.0.6"
|
||||
yjs "^13.6.15"
|
||||
|
||||
"@blocknote/mantine@0.22.0":
|
||||
version "0.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@blocknote/mantine/-/mantine-0.22.0.tgz#15509aaefe88c3efd73a884b9fb1e0584a6223ec"
|
||||
integrity sha512-6irIKCGUpE47X8qWLx9oa5ndztSrvLEHgVRp+fdVUHMJCx0/OzijJyYTTFKw8yEI9qc01pjmwdYMZrMMZybyGw==
|
||||
"@blocknote/mantine@0.21.0":
|
||||
version "0.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@blocknote/mantine/-/mantine-0.21.0.tgz#b8a640f498a4884129fe33f854be8d2bb842ea41"
|
||||
integrity sha512-GAxgvn/87wDyE8qdkystTkEbqE8AFO81gaMJ6df0P6ZAdfIH3sFYUf9MffVOjtq7T6NSCM9vHNnhHsC9K8m/fg==
|
||||
dependencies:
|
||||
"@blocknote/core" "^0.22.0"
|
||||
"@blocknote/react" "^0.22.0"
|
||||
"@blocknote/core" "^0.21.0"
|
||||
"@blocknote/react" "^0.21.0"
|
||||
"@mantine/core" "^7.10.1"
|
||||
"@mantine/hooks" "^7.10.1"
|
||||
"@mantine/utils" "^6.0.21"
|
||||
react-icons "^5.2.1"
|
||||
|
||||
"@blocknote/react@0.22.0", "@blocknote/react@^0.22.0":
|
||||
"@blocknote/react@0.21.0", "@blocknote/react@^0.21.0":
|
||||
version "0.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@blocknote/react/-/react-0.21.0.tgz#ad8907f89575e8c139d07d75bdb66ef4e33f84f9"
|
||||
integrity sha512-eBKe3hihGNeO4G/qKKJ/B5uuEmWm8XMbT8SxJ2zpNTjHx5lLP45vhtjAM+HCzQqz4xYacc2NphUIdjPPH5eXrQ==
|
||||
dependencies:
|
||||
"@blocknote/core" "^0.21.0"
|
||||
"@floating-ui/react" "^0.26.4"
|
||||
"@tiptap/core" "^2.7.1"
|
||||
"@tiptap/react" "^2.7.1"
|
||||
lodash.merge "^4.6.2"
|
||||
react-icons "^5.2.1"
|
||||
|
||||
"@blocknote/react@^0.22.0":
|
||||
version "0.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@blocknote/react/-/react-0.22.0.tgz#a17167a26b70ef421218ae3e49d15cca751291f0"
|
||||
integrity sha512-Y6Oj99iOKnlh2FE/lgy8kO5PziPnA8MyEJyjCH9Jbvlc9t493L9EFmLK8iKBZek7sh0TOzhXGBOA6lIpk02X6A==
|
||||
@@ -11080,7 +11141,7 @@ prosemirror-trailing-node@^3.0.0:
|
||||
"@remirror/core-constants" "3.0.0"
|
||||
escape-string-regexp "^4.0.0"
|
||||
|
||||
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.7.3:
|
||||
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.7.3, prosemirror-transform@^1.9.0:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz#8ebac4e305b586cd96595aa028118c9191bbf052"
|
||||
integrity sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==
|
||||
|
||||
@@ -93,4 +93,4 @@ releases:
|
||||
environments:
|
||||
dev:
|
||||
values:
|
||||
- version: 2.0.0
|
||||
- version: 2.0.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
Reference in New Issue
Block a user