♻️(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: [],
|
accesses: [],
|
||||||
abilities: {
|
abilities: {
|
||||||
destroy: false, // Means not owner
|
destroy: false, // Means not owner
|
||||||
|
link_configuration: false,
|
||||||
versions_destroy: false,
|
versions_destroy: false,
|
||||||
versions_list: true,
|
versions_list: true,
|
||||||
versions_retrieve: true,
|
versions_retrieve: true,
|
||||||
@@ -172,7 +173,7 @@ export const mockedDocument = async (page: Page, json: object) => {
|
|||||||
partial_update: false, // Means not editor
|
partial_update: false, // Means not editor
|
||||||
retrieve: true,
|
retrieve: true,
|
||||||
},
|
},
|
||||||
is_public: false,
|
link_reach: 'restricted',
|
||||||
created_at: '2021-09-01T09:00:00Z',
|
created_at: '2021-09-01T09:00:00Z',
|
||||||
...json,
|
...json,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ test.describe('Doc Editor', () => {
|
|||||||
await mockedDocument(page, {
|
await mockedDocument(page, {
|
||||||
abilities: {
|
abilities: {
|
||||||
destroy: false, // Means not owner
|
destroy: false, // Means not owner
|
||||||
|
link_configuration: false,
|
||||||
versions_destroy: false,
|
versions_destroy: false,
|
||||||
versions_list: true,
|
versions_list: true,
|
||||||
versions_retrieve: true,
|
versions_retrieve: true,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ test.describe('Doc Header', () => {
|
|||||||
],
|
],
|
||||||
abilities: {
|
abilities: {
|
||||||
destroy: true, // Means owner
|
destroy: true, // Means owner
|
||||||
|
link_configuration: true,
|
||||||
versions_destroy: true,
|
versions_destroy: true,
|
||||||
versions_list: true,
|
versions_list: true,
|
||||||
versions_retrieve: true,
|
versions_retrieve: true,
|
||||||
@@ -42,7 +43,7 @@ test.describe('Doc Header', () => {
|
|||||||
partial_update: true,
|
partial_update: true,
|
||||||
retrieve: true,
|
retrieve: true,
|
||||||
},
|
},
|
||||||
is_public: true,
|
link_reach: 'public',
|
||||||
created_at: '2021-09-01T09:00:00Z',
|
created_at: '2021-09-01T09:00:00Z',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -153,6 +154,7 @@ test.describe('Doc Header', () => {
|
|||||||
await mockedDocument(page, {
|
await mockedDocument(page, {
|
||||||
abilities: {
|
abilities: {
|
||||||
destroy: false, // Means not owner
|
destroy: false, // Means not owner
|
||||||
|
link_configuration: true,
|
||||||
versions_destroy: true,
|
versions_destroy: true,
|
||||||
versions_list: true,
|
versions_list: true,
|
||||||
versions_retrieve: true,
|
versions_retrieve: true,
|
||||||
@@ -184,6 +186,7 @@ test.describe('Doc Header', () => {
|
|||||||
await mockedDocument(page, {
|
await mockedDocument(page, {
|
||||||
abilities: {
|
abilities: {
|
||||||
destroy: false, // Means not owner
|
destroy: false, // Means not owner
|
||||||
|
link_configuration: false,
|
||||||
versions_destroy: true,
|
versions_destroy: true,
|
||||||
versions_list: true,
|
versions_list: true,
|
||||||
versions_retrieve: true,
|
versions_retrieve: true,
|
||||||
@@ -215,6 +218,7 @@ test.describe('Doc Header', () => {
|
|||||||
await mockedDocument(page, {
|
await mockedDocument(page, {
|
||||||
abilities: {
|
abilities: {
|
||||||
destroy: false, // Means not owner
|
destroy: false, // Means not owner
|
||||||
|
link_configuration: false,
|
||||||
versions_destroy: false,
|
versions_destroy: false,
|
||||||
versions_list: true,
|
versions_list: true,
|
||||||
versions_retrieve: true,
|
versions_retrieve: true,
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export const DocHeader = ({ doc, versionId }: DocHeaderProps) => {
|
|||||||
$wrap="wrap"
|
$wrap="wrap"
|
||||||
>
|
>
|
||||||
<Box $direction="row" $align="center" $gap="0.5rem 2rem" $wrap="wrap">
|
<Box $direction="row" $align="center" $gap="0.5rem 2rem" $wrap="wrap">
|
||||||
<DocTagPublic />
|
<DocTagPublic doc={doc} />
|
||||||
<Text $size="s" $display="inline">
|
<Text $size="s" $display="inline">
|
||||||
{t('Created at')} <strong>{formatDate(doc.created_at)}</strong>
|
{t('Created at')} <strong>{formatDate(doc.created_at)}</strong>
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,26 +1,18 @@
|
|||||||
import { useRouter } from 'next/router';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Text } from '@/components';
|
import { Text } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
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 { colorsTokens } = useCunninghamTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
|
||||||
query: { id },
|
|
||||||
} = useRouter();
|
|
||||||
|
|
||||||
const { data: doc } = useDoc(
|
if (doc?.link_reach !== LinkReach.PUBLIC) {
|
||||||
{ id: id as string },
|
|
||||||
{
|
|
||||||
enabled: !!id,
|
|
||||||
queryKey: [KEY_DOC_VISIBILITY, { id }],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!doc?.is_public) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './useDoc';
|
export * from './useDoc';
|
||||||
export * from './useDocs';
|
export * from './useDocs';
|
||||||
export * from './useUpdateDoc';
|
export * from './useUpdateDoc';
|
||||||
|
export * from './useUpdateDocLink';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { APIError, errorCauses, fetchAPI } from '@/api';
|
|||||||
import { Doc } from '@/features/docs';
|
import { Doc } from '@/features/docs';
|
||||||
|
|
||||||
export type UpdateDocParams = Pick<Doc, 'id'> &
|
export type UpdateDocParams = Pick<Doc, 'id'> &
|
||||||
Partial<Pick<Doc, 'content' | 'title' | 'is_public'>>;
|
Partial<Pick<Doc, 'content' | 'title'>>;
|
||||||
|
|
||||||
export const updateDoc = async ({
|
export const updateDoc = async ({
|
||||||
id,
|
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 { Box, Card, IconBG } from '@/components';
|
||||||
|
|
||||||
import { KEY_DOC_VISIBILITY, KEY_LIST_DOC, useUpdateDoc } from '../api';
|
import { KEY_DOC, KEY_LIST_DOC, useUpdateDocLink } from '../api';
|
||||||
import { Doc } from '../types';
|
import { Doc, LinkReach } from '../types';
|
||||||
|
|
||||||
interface DocVisibilityProps {
|
interface DocVisibilityProps {
|
||||||
doc: Doc;
|
doc: Doc;
|
||||||
@@ -18,9 +18,11 @@ interface DocVisibilityProps {
|
|||||||
|
|
||||||
export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [docPublic, setDocPublic] = useState(doc.is_public);
|
const [docPublic, setDocPublic] = useState(
|
||||||
|
doc.link_reach === LinkReach.PUBLIC,
|
||||||
|
);
|
||||||
const { toast } = useToastProvider();
|
const { toast } = useToastProvider();
|
||||||
const api = useUpdateDoc({
|
const api = useUpdateDocLink({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast(
|
toast(
|
||||||
t('The document visiblitity has been updated.'),
|
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 (
|
return (
|
||||||
<Card
|
<Card
|
||||||
$margin="tiny"
|
$margin="tiny"
|
||||||
$padding="small"
|
$padding={{ horizontal: 'small', vertical: 'tiny' }}
|
||||||
aria-label={t('Doc visibility card')}
|
aria-label={t('Doc visibility card')}
|
||||||
$direction="row"
|
$direction="row"
|
||||||
$align="center"
|
$align="center"
|
||||||
@@ -50,10 +52,12 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
|||||||
onChange={() => {
|
onChange={() => {
|
||||||
api.mutate({
|
api.mutate({
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
is_public: !docPublic,
|
link_reach: docPublic ? LinkReach.RESTRICTED : LinkReach.PUBLIC,
|
||||||
|
link_role: 'reader',
|
||||||
});
|
});
|
||||||
setDocPublic(!docPublic);
|
setDocPublic(!docPublic);
|
||||||
}}
|
}}
|
||||||
|
disabled={!doc.abilities.link_configuration}
|
||||||
text={t(
|
text={t(
|
||||||
docPublic
|
docPublic
|
||||||
? 'Anyone on the internet with the link can view'
|
? 'Anyone on the internet with the link can view'
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ export const ModalUpdateDoc = ({ onClose, doc }: ModalUpdateDocProps) => {
|
|||||||
buttonText: t('Validate the modification'),
|
buttonText: t('Validate the modification'),
|
||||||
onClose,
|
onClose,
|
||||||
initialTitle: doc.title,
|
initialTitle: doc.title,
|
||||||
isPublic: doc.is_public,
|
|
||||||
infoText: t('Enter the new name of the selected document.'),
|
infoText: t('Enter the new name of the selected document.'),
|
||||||
titleModal: t('Update document "{{documentTitle}}"', {
|
titleModal: t('Update document "{{documentTitle}}"', {
|
||||||
documentTitle: doc.title,
|
documentTitle: doc.title,
|
||||||
|
|||||||
@@ -46,28 +46,26 @@ export const ModalShare = ({ onClose, doc }: ModalShareProps) => {
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
width="70vw"
|
width="70vw"
|
||||||
$css="min-width: 320px;max-width: 777px;"
|
$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} />
|
<DocVisibility doc={doc} />
|
||||||
<AddMembers doc={doc} currentRole={currentDocRole(doc.abilities)} />
|
<AddMembers doc={doc} currentRole={currentDocRole(doc.abilities)} />
|
||||||
<InvitationList doc={doc} />
|
<InvitationList doc={doc} />
|
||||||
|
|||||||
@@ -20,18 +20,26 @@ export enum Role {
|
|||||||
OWNER = 'owner',
|
OWNER = 'owner',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum LinkReach {
|
||||||
|
RESTRICTED = 'restricted',
|
||||||
|
PUBLIC = 'public',
|
||||||
|
AUTHENTICATED = 'authenticated',
|
||||||
|
}
|
||||||
|
|
||||||
export type Base64 = string;
|
export type Base64 = string;
|
||||||
|
|
||||||
export interface Doc {
|
export interface Doc {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
content: Base64;
|
content: Base64;
|
||||||
is_public: boolean;
|
link_reach: LinkReach;
|
||||||
|
link_role: 'reader' | 'editor';
|
||||||
accesses: Access[];
|
accesses: Access[];
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
abilities: {
|
abilities: {
|
||||||
destroy: boolean;
|
destroy: boolean;
|
||||||
|
link_configuration: boolean;
|
||||||
manage_accesses: boolean;
|
manage_accesses: boolean;
|
||||||
partial_update: boolean;
|
partial_update: boolean;
|
||||||
retrieve: boolean;
|
retrieve: boolean;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useCunninghamTheme } from '@/cunningham';
|
|||||||
import {
|
import {
|
||||||
Doc,
|
Doc,
|
||||||
DocsOrdering,
|
DocsOrdering,
|
||||||
|
LinkReach,
|
||||||
currentDocRole,
|
currentDocRole,
|
||||||
isDocsOrdering,
|
isDocsOrdering,
|
||||||
useDocs,
|
useDocs,
|
||||||
@@ -109,7 +110,7 @@ export const DocsGrid = () => {
|
|||||||
renderCell: ({ row }) => {
|
renderCell: ({ row }) => {
|
||||||
return (
|
return (
|
||||||
<StyledLink href={`/docs/${row.id}`}>
|
<StyledLink href={`/docs/${row.id}`}>
|
||||||
{row.is_public && (
|
{row.link_reach === LinkReach.PUBLIC && (
|
||||||
<Text
|
<Text
|
||||||
$weight="bold"
|
$weight="bold"
|
||||||
$background={colorsTokens()['primary-600']}
|
$background={colorsTokens()['primary-600']}
|
||||||
@@ -117,7 +118,7 @@ export const DocsGrid = () => {
|
|||||||
$padding="xtiny"
|
$padding="xtiny"
|
||||||
$radius="3px"
|
$radius="3px"
|
||||||
>
|
>
|
||||||
{row.is_public ? t('Public') : ''}
|
{t('Public')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export const InvitationList = ({ doc }: InvitationListProps) => {
|
|||||||
<Card
|
<Card
|
||||||
$margin="tiny"
|
$margin="tiny"
|
||||||
$padding="tiny"
|
$padding="tiny"
|
||||||
$maxHeight="60%"
|
$maxHeight="40%"
|
||||||
$overflow="auto"
|
$overflow="auto"
|
||||||
aria-label={t('List invitation card')}
|
aria-label={t('List invitation card')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
$gap="1rem"
|
$gap="1rem"
|
||||||
$padding="1rem"
|
$padding={{ horizontal: 'small', vertical: 'tiny' }}
|
||||||
$margin="tiny"
|
$margin="tiny"
|
||||||
$direction="row"
|
$direction="row"
|
||||||
$align="center"
|
$align="center"
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export const MemberList = ({ doc }: MemberListProps) => {
|
|||||||
<Card
|
<Card
|
||||||
$margin="tiny"
|
$margin="tiny"
|
||||||
$padding="tiny"
|
$padding="tiny"
|
||||||
$maxHeight="85%"
|
$maxHeight="69%"
|
||||||
$overflow="auto"
|
$overflow="auto"
|
||||||
aria-label={t('List members card')}
|
aria-label={t('List members card')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ export class ApiPlugin implements WorkboxPlugin {
|
|||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
abilities: {
|
abilities: {
|
||||||
destroy: true,
|
destroy: true,
|
||||||
|
link_configuration: true,
|
||||||
versions_destroy: true,
|
versions_destroy: true,
|
||||||
versions_list: true,
|
versions_list: true,
|
||||||
versions_retrieve: true,
|
versions_retrieve: true,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Loader } from '@openfun/cunningham-react';
|
import { Loader } from '@openfun/cunningham-react';
|
||||||
import { useRouter as useNavigate } from 'next/navigation';
|
import { useRouter as useNavigate } from 'next/navigation';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Box, Text } from '@/components';
|
import { Box, Text } from '@/components';
|
||||||
import { TextErrors } from '@/components/TextErrors';
|
import { TextErrors } from '@/components/TextErrors';
|
||||||
@@ -31,7 +31,9 @@ interface DocProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DocPage = ({ id }: 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();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -42,6 +44,14 @@ const DocPage = ({ id }: DocProps) => {
|
|||||||
}
|
}
|
||||||
}, [doc?.title]);
|
}, [doc?.title]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!docQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDoc(docQuery);
|
||||||
|
}, [docQuery]);
|
||||||
|
|
||||||
if (isError && error) {
|
if (isError && error) {
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
navigate.replace(`/404`);
|
navigate.replace(`/404`);
|
||||||
@@ -64,7 +74,7 @@ const DocPage = ({ id }: DocProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading || !doc) {
|
if (!doc) {
|
||||||
return (
|
return (
|
||||||
<Box $align="center" $justify="center" $height="100%">
|
<Box $align="center" $justify="center" $height="100%">
|
||||||
<Loader />
|
<Loader />
|
||||||
|
|||||||
Reference in New Issue
Block a user