♻️(frontend) adapt doc visibility to new api

We updated the way we handle the visibility of a doc
in the backend. Now we use a new api to update
the visibility (documents/{id}/link-configuration/)
of a doc. We adapted the frontend to use this new api.
We changed the types to reflect the new api and
to keep the same logic.
This commit is contained in:
Anthony LC
2024-09-11 10:23:06 +02:00
committed by Samuel Paccoud
parent 9b44e021fd
commit a092c2915b
18 changed files with 128 additions and 57 deletions

View File

@@ -164,6 +164,7 @@ export const mockedDocument = async (page: Page, json: object) => {
accesses: [],
abilities: {
destroy: false, // Means not owner
link_configuration: false,
versions_destroy: false,
versions_list: true,
versions_retrieve: true,
@@ -172,7 +173,7 @@ export const mockedDocument = async (page: Page, json: object) => {
partial_update: false, // Means not editor
retrieve: true,
},
is_public: false,
link_reach: 'restricted',
created_at: '2021-09-01T09:00:00Z',
...json,
},

View File

@@ -136,6 +136,7 @@ test.describe('Doc Editor', () => {
await mockedDocument(page, {
abilities: {
destroy: false, // Means not owner
link_configuration: false,
versions_destroy: false,
versions_list: true,
versions_retrieve: true,

View File

@@ -34,6 +34,7 @@ test.describe('Doc Header', () => {
],
abilities: {
destroy: true, // Means owner
link_configuration: true,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
@@ -42,7 +43,7 @@ test.describe('Doc Header', () => {
partial_update: true,
retrieve: true,
},
is_public: true,
link_reach: 'public',
created_at: '2021-09-01T09:00:00Z',
});
@@ -153,6 +154,7 @@ test.describe('Doc Header', () => {
await mockedDocument(page, {
abilities: {
destroy: false, // Means not owner
link_configuration: true,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
@@ -184,6 +186,7 @@ test.describe('Doc Header', () => {
await mockedDocument(page, {
abilities: {
destroy: false, // Means not owner
link_configuration: false,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
@@ -215,6 +218,7 @@ test.describe('Doc Header', () => {
await mockedDocument(page, {
abilities: {
destroy: false, // Means not owner
link_configuration: false,
versions_destroy: false,
versions_list: true,
versions_retrieve: true,

View File

@@ -85,7 +85,7 @@ export const DocHeader = ({ doc, versionId }: DocHeaderProps) => {
$wrap="wrap"
>
<Box $direction="row" $align="center" $gap="0.5rem 2rem" $wrap="wrap">
<DocTagPublic />
<DocTagPublic doc={doc} />
<Text $size="s" $display="inline">
{t('Created at')} <strong>{formatDate(doc.created_at)}</strong>
</Text>

View File

@@ -1,26 +1,18 @@
import { useRouter } from 'next/router';
import { useTranslation } from 'react-i18next';
import { Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { KEY_DOC_VISIBILITY, useDoc } from '@/features/docs/doc-management';
import { Doc, LinkReach } from '@/features/docs/doc-management';
export const DocTagPublic = () => {
interface DocTagPublicProps {
doc: Doc;
}
export const DocTagPublic = ({ doc }: DocTagPublicProps) => {
const { colorsTokens } = useCunninghamTheme();
const { t } = useTranslation();
const {
query: { id },
} = useRouter();
const { data: doc } = useDoc(
{ id: id as string },
{
enabled: !!id,
queryKey: [KEY_DOC_VISIBILITY, { id }],
},
);
if (!doc?.is_public) {
if (doc?.link_reach !== LinkReach.PUBLIC) {
return null;
}

View File

@@ -1,3 +1,4 @@
export * from './useDoc';
export * from './useDocs';
export * from './useUpdateDoc';
export * from './useUpdateDocLink';

View File

@@ -4,7 +4,7 @@ import { APIError, errorCauses, fetchAPI } from '@/api';
import { Doc } from '@/features/docs';
export type UpdateDocParams = Pick<Doc, 'id'> &
Partial<Pick<Doc, 'content' | 'title' | 'is_public'>>;
Partial<Pick<Doc, 'content' | 'title'>>;
export const updateDoc = async ({
id,

View File

@@ -0,0 +1,51 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { Doc } from '@/features/docs';
export type UpdateDocLinkParams = Pick<Doc, 'id'> &
Partial<Pick<Doc, 'link_role' | 'link_reach'>>;
export const updateDocLink = async ({
id,
...params
}: UpdateDocLinkParams): Promise<Doc> => {
const response = await fetchAPI(`documents/${id}/link-configuration/`, {
method: 'PUT',
body: JSON.stringify({
...params,
}),
});
if (!response.ok) {
throw new APIError(
'Failed to update the doc link',
await errorCauses(response),
);
}
return response.json() as Promise<Doc>;
};
interface UpdateDocLinkProps {
onSuccess?: (data: Doc) => void;
listInvalideQueries?: string[];
}
export function useUpdateDocLink({
onSuccess,
listInvalideQueries,
}: UpdateDocLinkProps = {}) {
const queryClient = useQueryClient();
return useMutation<Doc, APIError, UpdateDocLinkParams>({
mutationFn: updateDocLink,
onSuccess: (data) => {
listInvalideQueries?.forEach((queryKey) => {
void queryClient.resetQueries({
queryKey: [queryKey],
});
});
onSuccess?.(data);
},
});
}

View File

@@ -9,8 +9,8 @@ import { useTranslation } from 'react-i18next';
import { Box, Card, IconBG } from '@/components';
import { KEY_DOC_VISIBILITY, KEY_LIST_DOC, useUpdateDoc } from '../api';
import { Doc } from '../types';
import { KEY_DOC, KEY_LIST_DOC, useUpdateDocLink } from '../api';
import { Doc, LinkReach } from '../types';
interface DocVisibilityProps {
doc: Doc;
@@ -18,9 +18,11 @@ interface DocVisibilityProps {
export const DocVisibility = ({ doc }: DocVisibilityProps) => {
const { t } = useTranslation();
const [docPublic, setDocPublic] = useState(doc.is_public);
const [docPublic, setDocPublic] = useState(
doc.link_reach === LinkReach.PUBLIC,
);
const { toast } = useToastProvider();
const api = useUpdateDoc({
const api = useUpdateDocLink({
onSuccess: () => {
toast(
t('The document visiblitity has been updated.'),
@@ -30,13 +32,13 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
},
);
},
listInvalideQueries: [KEY_LIST_DOC, KEY_DOC_VISIBILITY],
listInvalideQueries: [KEY_LIST_DOC, KEY_DOC],
});
return (
<Card
$margin="tiny"
$padding="small"
$padding={{ horizontal: 'small', vertical: 'tiny' }}
aria-label={t('Doc visibility card')}
$direction="row"
$align="center"
@@ -50,10 +52,12 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
onChange={() => {
api.mutate({
id: doc.id,
is_public: !docPublic,
link_reach: docPublic ? LinkReach.RESTRICTED : LinkReach.PUBLIC,
link_role: 'reader',
});
setDocPublic(!docPublic);
}}
disabled={!doc.abilities.link_configuration}
text={t(
docPublic
? 'Anyone on the internet with the link can view'

View File

@@ -78,7 +78,6 @@ export const ModalUpdateDoc = ({ onClose, doc }: ModalUpdateDocProps) => {
buttonText: t('Validate the modification'),
onClose,
initialTitle: doc.title,
isPublic: doc.is_public,
infoText: t('Enter the new name of the selected document.'),
titleModal: t('Update document "{{documentTitle}}"', {
documentTitle: doc.title,

View File

@@ -46,28 +46,26 @@ export const ModalShare = ({ onClose, doc }: ModalShareProps) => {
onClose={onClose}
width="70vw"
$css="min-width: 320px;max-width: 777px;"
title={
<Card
$direction="row"
$align="center"
$margin={{ horizontal: 'tiny', top: 'none', bottom: 'big' }}
$padding="tiny"
$gap="1rem"
>
<Text $isMaterialIcon $size="48px" $theme="primary">
share
</Text>
<Box $align="flex-start">
<Text as="h3" $size="26px" $margin="none">
{t('Share')}
</Text>
<Text $size="small" $weight="normal" $textAlign="left">
{doc.title}
</Text>
</Box>
</Card>
}
>
<Card
$direction="row"
$align="center"
$margin={{ horizontal: 'tiny', top: 'none', bottom: 'big' }}
$padding="tiny"
$gap="1rem"
>
<Text $isMaterialIcon $size="48px" $theme="primary">
share
</Text>
<Box $align="flex-start">
<Text as="h3" $size="26px" $margin="none">
{t('Share')}
</Text>
<Text $size="small" $weight="normal" $textAlign="left">
{doc.title}
</Text>
</Box>
</Card>
<DocVisibility doc={doc} />
<AddMembers doc={doc} currentRole={currentDocRole(doc.abilities)} />
<InvitationList doc={doc} />

View File

@@ -20,18 +20,26 @@ export enum Role {
OWNER = 'owner',
}
export enum LinkReach {
RESTRICTED = 'restricted',
PUBLIC = 'public',
AUTHENTICATED = 'authenticated',
}
export type Base64 = string;
export interface Doc {
id: string;
title: string;
content: Base64;
is_public: boolean;
link_reach: LinkReach;
link_role: 'reader' | 'editor';
accesses: Access[];
created_at: string;
updated_at: string;
abilities: {
destroy: boolean;
link_configuration: boolean;
manage_accesses: boolean;
partial_update: boolean;
retrieve: boolean;

View File

@@ -8,6 +8,7 @@ import { useCunninghamTheme } from '@/cunningham';
import {
Doc,
DocsOrdering,
LinkReach,
currentDocRole,
isDocsOrdering,
useDocs,
@@ -109,7 +110,7 @@ export const DocsGrid = () => {
renderCell: ({ row }) => {
return (
<StyledLink href={`/docs/${row.id}`}>
{row.is_public && (
{row.link_reach === LinkReach.PUBLIC && (
<Text
$weight="bold"
$background={colorsTokens()['primary-600']}
@@ -117,7 +118,7 @@ export const DocsGrid = () => {
$padding="xtiny"
$radius="3px"
>
{row.is_public ? t('Public') : ''}
{t('Public')}
</Text>
)}
</StyledLink>

View File

@@ -98,7 +98,7 @@ export const InvitationList = ({ doc }: InvitationListProps) => {
<Card
$margin="tiny"
$padding="tiny"
$maxHeight="60%"
$maxHeight="40%"
$overflow="auto"
aria-label={t('List invitation card')}
>

View File

@@ -138,7 +138,7 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
return (
<Card
$gap="1rem"
$padding="1rem"
$padding={{ horizontal: 'small', vertical: 'tiny' }}
$margin="tiny"
$direction="row"
$align="center"

View File

@@ -93,7 +93,7 @@ export const MemberList = ({ doc }: MemberListProps) => {
<Card
$margin="tiny"
$padding="tiny"
$maxHeight="85%"
$maxHeight="69%"
$overflow="auto"
aria-label={t('List members card')}
>

View File

@@ -189,6 +189,7 @@ export class ApiPlugin implements WorkboxPlugin {
updated_at: new Date().toISOString(),
abilities: {
destroy: true,
link_configuration: true,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,

View File

@@ -1,7 +1,7 @@
import { Loader } from '@openfun/cunningham-react';
import { useRouter as useNavigate } from 'next/navigation';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { Box, Text } from '@/components';
import { TextErrors } from '@/components/TextErrors';
@@ -31,7 +31,9 @@ interface DocProps {
}
const DocPage = ({ id }: DocProps) => {
const { data: doc, isLoading, isError, error } = useDoc({ id });
const { data: docQuery, isError, error } = useDoc({ id });
const [doc, setDoc] = useState(docQuery);
const navigate = useNavigate();
useEffect(() => {
@@ -42,6 +44,14 @@ const DocPage = ({ id }: DocProps) => {
}
}, [doc?.title]);
useEffect(() => {
if (!docQuery) {
return;
}
setDoc(docQuery);
}, [docQuery]);
if (isError && error) {
if (error.status === 404) {
navigate.replace(`/404`);
@@ -64,7 +74,7 @@ const DocPage = ({ id }: DocProps) => {
);
}
if (isLoading || !doc) {
if (!doc) {
return (
<Box $align="center" $justify="center" $height="100%">
<Loader />