[WEB-1019] chore: error state for unauthorized pages (#4219)

* chore: private page access

* chore: add error state for private pages

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
Aaryan Khandelwal 2024-04-17 14:53:01 +05:30 committed by GitHub
parent 1080094b01
commit 1880eb7704
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 226 additions and 184 deletions

View File

@ -169,9 +169,15 @@ class PageViewSet(BaseViewSet):
def retrieve(self, request, slug, project_id, pk=None): def retrieve(self, request, slug, project_id, pk=None):
page = self.get_queryset().filter(pk=pk).first() page = self.get_queryset().filter(pk=pk).first()
return Response( if page is None:
PageDetailSerializer(page).data, status=status.HTTP_200_OK return Response(
) {"error": "Page not found"},
status=status.HTTP_404_NOT_FOUND,
)
else:
return Response(
PageDetailSerializer(page).data, status=status.HTTP_200_OK
)
def lock(self, request, slug, project_id, pk): def lock(self, request, slug, project_id, pk):
page = Page.objects.filter( page = Page.objects.filter(

View File

@ -20,7 +20,17 @@ export const PageQuickActions: React.FC<Props> = observer((props) => {
// states // states
const [deletePageModal, setDeletePageModal] = useState(false); const [deletePageModal, setDeletePageModal] = useState(false);
// store hooks // store hooks
const { access, archive, archived_at, makePublic, makePrivate, restore } = usePage(pageId); const {
access,
archive,
archived_at,
makePublic,
makePrivate,
restore,
canCurrentUserArchivePage,
canCurrentUserChangeAccess,
canCurrentUserDeletePage,
} = usePage(pageId);
const pageLink = `${workspaceSlug}/projects/${projectId}/pages/${pageId}`; const pageLink = `${workspaceSlug}/projects/${projectId}/pages/${pageId}`;
const handleCopyText = () => const handleCopyText = () =>
@ -31,8 +41,53 @@ export const PageQuickActions: React.FC<Props> = observer((props) => {
message: "Page link copied to clipboard.", message: "Page link copied to clipboard.",
}); });
}); });
const handleOpenInNewTab = () => window.open(`/${pageLink}`, "_blank"); const handleOpenInNewTab = () => window.open(`/${pageLink}`, "_blank");
const MENU_ITEMS: {
key: string;
action: () => void;
label: string;
icon: React.FC<any>;
shouldRender: boolean;
}[] = [
{
key: "copy-link",
action: handleCopyText,
label: "Copy link",
icon: Link,
shouldRender: true,
},
{
key: "open-new-tab",
action: handleOpenInNewTab,
label: "Open in new tab",
icon: ExternalLink,
shouldRender: true,
},
{
key: "archive-restore",
action: archived_at ? restore : archive,
label: archived_at ? "Restore" : "Archive",
icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon,
shouldRender: canCurrentUserArchivePage,
},
{
key: "make-public-private",
action: access === 0 ? makePrivate : makePublic,
label: access === 0 ? "Make private" : "Make public",
icon: access === 0 ? Lock : UsersRound,
shouldRender: canCurrentUserChangeAccess && !archived_at,
},
{
key: "delete",
action: () => setDeletePageModal(true),
label: "Delete",
icon: Trash2,
shouldRender: canCurrentUserDeletePage && !!archived_at,
},
];
return ( return (
<> <>
<DeletePageModal <DeletePageModal
@ -42,89 +97,23 @@ export const PageQuickActions: React.FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
/> />
<CustomMenu placement="bottom-end" ellipsis closeOnSelect> <CustomMenu placement="bottom-end" ellipsis closeOnSelect>
<CustomMenu.MenuItem {MENU_ITEMS.map((item) => {
onClick={(e) => { if (!item.shouldRender) return null;
e.preventDefault(); return (
e.stopPropagation(); <CustomMenu.MenuItem
handleCopyText(); key={item.key}
}} onClick={(e) => {
> e.preventDefault();
<span className="flex items-center gap-2"> e.stopPropagation();
<Link className="h-3 w-3" /> item.action();
Copy link }}
</span> className="flex items-center gap-2"
</CustomMenu.MenuItem> >
<CustomMenu.MenuItem <item.icon className="h-3 w-3" />
onClick={(e) => { {item.label}
e.preventDefault(); </CustomMenu.MenuItem>
e.stopPropagation(); );
handleOpenInNewTab(); })}
}}
>
<span className="flex items-center gap-2">
<ExternalLink className="h-3 w-3" />
Open in new tab
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (archived_at) restore();
else archive();
}}
>
<span className="flex items-center gap-2">
{archived_at ? (
<>
<ArchiveRestoreIcon className="h-3 w-3" />
Restore
</>
) : (
<>
<ArchiveIcon className="h-3 w-3" />
Archive
</>
)}
</span>
</CustomMenu.MenuItem>
{!archived_at && (
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
access === 0 ? makePrivate() : makePublic();
}}
>
<span className="flex items-center gap-2">
{access === 0 ? (
<>
<Lock className="h-3 w-3" />
Make private
</>
) : (
<>
<UsersRound className="h-3 w-3" />
Make public
</>
)}
</span>
</CustomMenu.MenuItem>
)}
{archived_at && (
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDeletePageModal(true);
}}
>
<span className="flex items-center gap-2">
<Trash2 className="h-3 w-3" />
Delete
</span>
</CustomMenu.MenuItem>
)}
</CustomMenu> </CustomMenu>
</> </>
); );

View File

@ -1,5 +1,5 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Clipboard, Copy, Link, Lock } from "lucide-react"; import { ArchiveRestoreIcon, Clipboard, Copy, Link, Lock, LockOpen } from "lucide-react";
// document editor // document editor
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/document-editor"; import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/document-editor";
// ui // ui
@ -21,6 +21,9 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
const { editorRef, handleDuplicatePage, pageStore } = props; const { editorRef, handleDuplicatePage, pageStore } = props;
// store values // store values
const { const {
archived_at,
is_locked,
id,
archive, archive,
lock, lock,
unlock, unlock,
@ -99,7 +102,7 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
{ {
key: "copy-page-link", key: "copy-page-link",
action: () => { action: () => {
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/pages/${pageStore.id}`).then(() => copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/pages/${id}`).then(() =>
setToast({ setToast({
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS,
title: "Successful!", title: "Successful!",
@ -119,32 +122,18 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
shouldRender: canCurrentUserDuplicatePage, shouldRender: canCurrentUserDuplicatePage,
}, },
{ {
key: "lock-page", key: "lock-unlock-page",
action: handleLockPage, action: is_locked ? handleUnlockPage : handleLockPage,
label: "Lock page", label: is_locked ? "Unlock page" : "Lock page",
icon: Lock, icon: is_locked ? LockOpen : Lock,
shouldRender: !pageStore.is_locked && canCurrentUserLockPage, shouldRender: canCurrentUserLockPage,
}, },
{ {
key: "unlock-page", key: "archive-restore-page",
action: handleUnlockPage, action: archived_at ? handleRestorePage : handleArchivePage,
label: "Unlock page", label: archived_at ? "Restore page" : "Archive page",
icon: Lock, icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon,
shouldRender: pageStore.is_locked && canCurrentUserLockPage, shouldRender: canCurrentUserArchivePage,
},
{
key: "archive-page",
action: handleArchivePage,
label: "Archive page",
icon: ArchiveIcon,
shouldRender: !pageStore.archived_at && canCurrentUserArchivePage,
},
{
key: "restore-page",
action: handleRestorePage,
label: "Restore page",
icon: ArchiveIcon,
shouldRender: !!pageStore.archived_at && canCurrentUserArchivePage,
}, },
]; ];
@ -166,7 +155,7 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
return ( return (
<CustomMenu.MenuItem key={item.key} onClick={item.action} className="flex items-center gap-2"> <CustomMenu.MenuItem key={item.key} onClick={item.action} className="flex items-center gap-2">
<item.icon className="h-3 w-3" /> <item.icon className="h-3 w-3" />
<div className="text-custom-text-300">{item.label}</div> {item.label}
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
); );
})} })}

View File

@ -1,5 +1,6 @@
import { ReactElement, useEffect, useRef, useState } from "react"; import { ReactElement, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import useSWR from "swr"; import useSWR from "swr";
@ -8,12 +9,14 @@ import { EditorRefApi, useEditorMarkings } from "@plane/document-editor";
// types // types
import { TPage } from "@plane/types"; import { TPage } from "@plane/types";
// ui // ui
import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; import { Spinner, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
// components // components
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { PageDetailsHeader } from "@/components/headers"; import { PageDetailsHeader } from "@/components/headers";
import { IssuePeekOverview } from "@/components/issues"; import { IssuePeekOverview } from "@/components/issues";
import { PageEditorBody, PageEditorHeaderRoot } from "@/components/pages"; import { PageEditorBody, PageEditorHeaderRoot } from "@/components/pages";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks // hooks
import { usePage, useProjectPages } from "@/hooks/store"; import { usePage, useProjectPages } from "@/hooks/store";
// layouts // layouts
@ -46,15 +49,16 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
}); });
// fetching page details // fetching page details
const { data: swrPageDetails, isValidating } = useSWR( const {
pageId ? `PAGE_DETAILS_${pageId}` : null, data: swrPageDetails,
pageId ? () => getPageById(pageId.toString()) : null, isValidating,
{ error: pageDetailsError,
revalidateIfStale: false, } = useSWR(pageId ? `PAGE_DETAILS_${pageId}` : null, pageId ? () => getPageById(pageId.toString()) : null, {
revalidateOnFocus: true, revalidateIfStale: false,
revalidateOnReconnect: true, revalidateOnFocus: true,
} revalidateOnReconnect: true,
); });
useEffect( useEffect(
() => () => { () => () => {
if (pageStore.cleanup) pageStore.cleanup(); if (pageStore.cleanup) pageStore.cleanup();
@ -62,17 +66,29 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
[pageStore] [pageStore]
); );
const handleEditorReady = (value: boolean) => setEditorReady(value); if ((!pageStore || !pageStore.id) && !pageDetailsError)
const handleReadOnlyEditorReady = () => setReadOnlyEditorReady(true);
if (!pageStore || !pageStore.id)
return ( return (
<div className="h-full w-full grid place-items-center"> <div className="h-full w-full grid place-items-center">
<Spinner /> <Spinner />
</div> </div>
); );
if (pageDetailsError)
return (
<div className="h-full w-full flex flex-col items-center justify-center">
<h3 className="text-lg font-semibold text-center">Page not found</h3>
<p className="text-sm text-custom-text-200 text-center mt-3">
The page you are trying to access doesn{"'"}t exist or you don{"'"}t have permission to view it.
</p>
<Link
href={`/${workspaceSlug}/projects/${projectId}/pages`}
className={cn(getButtonStyling("neutral-primary", "md"), "mt-5")}
>
View other Pages
</Link>
</div>
);
// we need to get the values of title and description from the page store but we don't have to subscribe to those values // we need to get the values of title and description from the page store but we don't have to subscribe to those values
const pageTitle = pageStore?.name; const pageTitle = pageStore?.name;
@ -132,9 +148,9 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
swrPageDetails={swrPageDetails} swrPageDetails={swrPageDetails}
control={control} control={control}
editorRef={editorRef} editorRef={editorRef}
handleEditorReady={handleEditorReady} handleEditorReady={(val) => setEditorReady(val)}
readOnlyEditorRef={readOnlyEditorRef} readOnlyEditorRef={readOnlyEditorRef}
handleReadOnlyEditorReady={handleReadOnlyEditorReady} handleReadOnlyEditorReady={() => setReadOnlyEditorReady(true)}
handleSubmit={() => handleSubmit(handleUpdatePage)()} handleSubmit={() => handleSubmit(handleUpdatePage)()}
markings={markings} markings={markings}
pageStore={pageStore} pageStore={pageStore}

View File

@ -21,6 +21,7 @@ export interface IPageStore extends TPage {
canCurrentUserEditPage: boolean; // it will give the user permission to read the page or write the page canCurrentUserEditPage: boolean; // it will give the user permission to read the page or write the page
canCurrentUserDuplicatePage: boolean; canCurrentUserDuplicatePage: boolean;
canCurrentUserLockPage: boolean; canCurrentUserLockPage: boolean;
canCurrentUserChangeAccess: boolean;
canCurrentUserArchivePage: boolean; canCurrentUserArchivePage: boolean;
canCurrentUserDeletePage: boolean; canCurrentUserDeletePage: boolean;
isContentEditable: boolean; isContentEditable: boolean;
@ -72,7 +73,10 @@ export class PageStore implements IPageStore {
// service // service
pageService: PageService; pageService: PageService;
constructor(private store: RootStore, page: TPage) { constructor(
private store: RootStore,
page: TPage
) {
this.id = page?.id || undefined; this.id = page?.id || undefined;
this.name = page?.name || undefined; this.name = page?.name || undefined;
this.description_html = page?.description_html || undefined; this.description_html = page?.description_html || undefined;
@ -122,6 +126,7 @@ export class PageStore implements IPageStore {
canCurrentUserEditPage: computed, canCurrentUserEditPage: computed,
canCurrentUserDuplicatePage: computed, canCurrentUserDuplicatePage: computed,
canCurrentUserLockPage: computed, canCurrentUserLockPage: computed,
canCurrentUserChangeAccess: computed,
canCurrentUserArchivePage: computed, canCurrentUserArchivePage: computed,
canCurrentUserDeletePage: computed, canCurrentUserDeletePage: computed,
isContentEditable: computed, isContentEditable: computed,
@ -245,12 +250,19 @@ export class PageStore implements IPageStore {
return this.isCurrentUserOwner || (!!currentUserProjectRole && currentUserProjectRole >= EUserProjectRoles.MEMBER); return this.isCurrentUserOwner || (!!currentUserProjectRole && currentUserProjectRole >= EUserProjectRoles.MEMBER);
} }
/**
* @description returns true if the current logged in user can change the access of the page
*/
get canCurrentUserChangeAccess() {
return this.isCurrentUserOwner;
}
/** /**
* @description returns true if the current logged in user can archive the page * @description returns true if the current logged in user can archive the page
*/ */
get canCurrentUserArchivePage() { get canCurrentUserArchivePage() {
const currentUserProjectRole = this.store.user.membership.currentProjectRole; const currentUserProjectRole = this.store.user.membership.currentProjectRole;
return this.isCurrentUserOwner || (!!currentUserProjectRole && currentUserProjectRole >= EUserProjectRoles.MEMBER); return this.isCurrentUserOwner || currentUserProjectRole === EUserProjectRoles.ADMIN;
} }
/** /**
@ -315,13 +327,14 @@ export class PageStore implements IPageStore {
set(this, key, currentPageResponse?.[currentPageKey] || undefined); set(this, key, currentPageResponse?.[currentPageKey] || undefined);
}); });
}); });
} catch { } catch (error) {
runInAction(() => { runInAction(() => {
Object.keys(pageData).forEach((key) => { Object.keys(pageData).forEach((key) => {
const currentPageKey = key as keyof TPage; const currentPageKey = key as keyof TPage;
set(this, key, currentPage?.[currentPageKey] || undefined); set(this, key, currentPage?.[currentPageKey] || undefined);
}); });
}); });
throw error;
} }
}; };
@ -349,10 +362,11 @@ export class PageStore implements IPageStore {
...updatedProps, ...updatedProps,
}, },
}); });
} catch { } catch (error) {
runInAction(() => { runInAction(() => {
this.view_props = currentViewProps; this.view_props = currentViewProps;
}); });
throw error;
} }
}; };
@ -366,13 +380,16 @@ export class PageStore implements IPageStore {
const pageAccess = this.access; const pageAccess = this.access;
runInAction(() => (this.access = EPageAccess.PUBLIC)); runInAction(() => (this.access = EPageAccess.PUBLIC));
await this.pageService try {
.update(workspaceSlug, projectId, this.id, { await this.pageService.update(workspaceSlug, projectId, this.id, {
access: EPageAccess.PUBLIC, access: EPageAccess.PUBLIC,
})
.catch(() => {
runInAction(() => (this.access = pageAccess));
}); });
} catch (error) {
runInAction(() => {
this.access = pageAccess;
});
throw error;
}
}; };
/** /**
@ -385,13 +402,16 @@ export class PageStore implements IPageStore {
const pageAccess = this.access; const pageAccess = this.access;
runInAction(() => (this.access = EPageAccess.PRIVATE)); runInAction(() => (this.access = EPageAccess.PRIVATE));
await this.pageService try {
.update(workspaceSlug, projectId, this.id, { await this.pageService.update(workspaceSlug, projectId, this.id, {
access: EPageAccess.PRIVATE, access: EPageAccess.PRIVATE,
})
.catch(() => {
runInAction(() => (this.access = pageAccess));
}); });
} catch (error) {
runInAction(() => {
this.access = pageAccess;
});
throw error;
}
}; };
/** /**
@ -404,36 +424,11 @@ export class PageStore implements IPageStore {
const pageIsLocked = this.is_locked; const pageIsLocked = this.is_locked;
runInAction(() => (this.is_locked = true)); runInAction(() => (this.is_locked = true));
await this.pageService.lock(workspaceSlug, projectId, this.id).catch(() => { await this.pageService.lock(workspaceSlug, projectId, this.id).catch((error) => {
runInAction(() => (this.is_locked = pageIsLocked));
});
};
/**
* @description archive the page
*/
archive = async () => {
const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug || !projectId || !this.id) return undefined;
await this.pageService.archive(workspaceSlug, projectId, this.id).then((res) => {
runInAction(() => { runInAction(() => {
this.archived_at = res.archived_at; this.is_locked = pageIsLocked;
});
});
};
/**
* @description restore the page
*/
restore = async () => {
const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug || !projectId || !this.id) return undefined;
await this.pageService.restore(workspaceSlug, projectId, this.id).then(() => {
runInAction(() => {
this.archived_at = null;
}); });
throw error;
}); });
}; };
@ -447,11 +442,48 @@ export class PageStore implements IPageStore {
const pageIsLocked = this.is_locked; const pageIsLocked = this.is_locked;
runInAction(() => (this.is_locked = false)); runInAction(() => (this.is_locked = false));
await this.pageService.unlock(workspaceSlug, projectId, this.id).catch(() => { await this.pageService.unlock(workspaceSlug, projectId, this.id).catch((error) => {
runInAction(() => (this.is_locked = pageIsLocked)); runInAction(() => {
this.is_locked = pageIsLocked;
});
throw error;
}); });
}; };
/**
* @description archive the page
*/
archive = async () => {
const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug || !projectId || !this.id) return undefined;
try {
const response = await this.pageService.archive(workspaceSlug, projectId, this.id);
runInAction(() => {
this.archived_at = response.archived_at;
});
} catch (error) {
throw error;
}
};
/**
* @description restore the page
*/
restore = async () => {
const { workspaceSlug, projectId } = this.store.app.router;
if (!workspaceSlug || !projectId || !this.id) return undefined;
try {
await this.pageService.restore(workspaceSlug, projectId, this.id);
runInAction(() => {
this.archived_at = null;
});
} catch (error) {
throw error;
}
};
/** /**
* @description add the page to favorites * @description add the page to favorites
*/ */
@ -464,8 +496,11 @@ export class PageStore implements IPageStore {
this.is_favorite = true; this.is_favorite = true;
}); });
await this.pageService.addToFavorites(workspaceSlug, projectId, this.id).catch(() => { await this.pageService.addToFavorites(workspaceSlug, projectId, this.id).catch((error) => {
runInAction(() => (this.is_favorite = pageIsFavorite)); runInAction(() => {
this.is_favorite = pageIsFavorite;
});
throw error;
}); });
}; };
@ -481,8 +516,11 @@ export class PageStore implements IPageStore {
this.is_favorite = false; this.is_favorite = false;
}); });
await this.pageService.removeFromFavorites(workspaceSlug, projectId, this.id).catch(() => { await this.pageService.removeFromFavorites(workspaceSlug, projectId, this.id).catch((error) => {
runInAction(() => (this.is_favorite = pageIsFavorite)); runInAction(() => {
this.is_favorite = pageIsFavorite;
});
throw error;
}); });
}; };
} }

View File

@ -148,7 +148,7 @@ export class ProjectPageStore implements IProjectPageStore {
}); });
return pages; return pages;
} catch { } catch (error) {
runInAction(() => { runInAction(() => {
this.loader = undefined; this.loader = undefined;
this.error = { this.error = {
@ -156,6 +156,7 @@ export class ProjectPageStore implements IProjectPageStore {
description: "Failed to fetch the pages, Please try again later.", description: "Failed to fetch the pages, Please try again later.",
}; };
}); });
throw error;
} }
}; };
@ -181,7 +182,7 @@ export class ProjectPageStore implements IProjectPageStore {
}); });
return page; return page;
} catch { } catch (error) {
runInAction(() => { runInAction(() => {
this.loader = undefined; this.loader = undefined;
this.error = { this.error = {
@ -189,6 +190,7 @@ export class ProjectPageStore implements IProjectPageStore {
description: "Failed to fetch the page, Please try again later.", description: "Failed to fetch the page, Please try again later.",
}; };
}); });
throw error;
} }
}; };
@ -213,7 +215,7 @@ export class ProjectPageStore implements IProjectPageStore {
}); });
return page; return page;
} catch { } catch (error) {
runInAction(() => { runInAction(() => {
this.loader = undefined; this.loader = undefined;
this.error = { this.error = {
@ -221,6 +223,7 @@ export class ProjectPageStore implements IProjectPageStore {
description: "Failed to create a page, Please try again later.", description: "Failed to create a page, Please try again later.",
}; };
}); });
throw error;
} }
}; };
@ -235,7 +238,7 @@ export class ProjectPageStore implements IProjectPageStore {
await this.service.remove(workspaceSlug, projectId, pageId); await this.service.remove(workspaceSlug, projectId, pageId);
runInAction(() => unset(this.data, [pageId])); runInAction(() => unset(this.data, [pageId]));
} catch { } catch (error) {
runInAction(() => { runInAction(() => {
this.loader = undefined; this.loader = undefined;
this.error = { this.error = {
@ -243,6 +246,7 @@ export class ProjectPageStore implements IProjectPageStore {
description: "Failed to delete a page, Please try again later.", description: "Failed to delete a page, Please try again later.",
}; };
}); });
throw error;
} }
}; };
} }

View File

@ -2747,7 +2747,7 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react@*", "@types/react@^18.2.42": "@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42":
version "18.2.42" version "18.2.42"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA== integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==