♻️(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:
committed by
Samuel Paccoud
parent
9b44e021fd
commit
a092c2915b
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './useDoc';
|
||||
export * from './useDocs';
|
||||
export * from './useUpdateDoc';
|
||||
export * from './useUpdateDocLink';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')}
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user