chore: added authorization to pages (#3006)

* chore: updated pages authorization

* chore: updated pages empty state image
This commit is contained in:
Aaryan Khandelwal 2023-12-06 19:13:42 +05:30 committed by Aaryan Khandelwal
parent ddb6f8ed47
commit f8585e87f8
9 changed files with 109 additions and 92 deletions

View File

@ -1,4 +1,3 @@
import { FC } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { FileText, Plus } from "lucide-react"; import { FileText, Plus } from "lucide-react";
@ -8,18 +7,22 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { Breadcrumbs, Button } from "@plane/ui"; import { Breadcrumbs, Button } from "@plane/ui";
// helper // helper
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export interface IPagesHeaderProps { export const PagesHeader = observer(() => {
showButton?: boolean; // router
}
export const PagesHeader: FC<IPagesHeaderProps> = observer((props) => {
const { showButton = false } = props;
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// mobx store
const {
user: { currentProjectRole },
project: { currentProjectDetails },
commandPalette: { toggleCreatePageModal },
} = useMobxStore();
const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const canUserCreatePage =
const { currentProjectDetails } = projectStore; currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
return ( return (
<div className="relative flex w-full flex-shrink-0 flex-row z-10 h-[3.75rem] items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4"> <div className="relative flex w-full flex-shrink-0 flex-row z-10 h-[3.75rem] items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
@ -50,14 +53,9 @@ export const PagesHeader: FC<IPagesHeaderProps> = observer((props) => {
</Breadcrumbs> </Breadcrumbs>
</div> </div>
</div> </div>
{showButton && ( {canUserCreatePage && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button variant="primary" prependIcon={<Plus />} size="sm" onClick={() => toggleCreatePageModal(true)}>
variant="primary"
prependIcon={<Plus />}
size="sm"
onClick={() => commandPaletteStore.toggleCreatePageModal(true)}
>
Create Page Create Page
</Button> </Button>
</div> </div>

View File

@ -102,13 +102,7 @@ export const PageForm: React.FC<Props> = (props) => {
Cancel Cancel
</Button> </Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}> <Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
{data {data ? (isSubmitting ? "Updating..." : "Update page") : isSubmitting ? "Creating..." : "Create Page"}
? isSubmitting
? "Updating Page..."
: "Update Page"
: isSubmitting
? "Creating Page..."
: "Create Page"}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -1,7 +1,6 @@
import { FC, useState } from "react"; import { FC, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// icons
import { import {
AlertCircle, AlertCircle,
Archive, Archive,
@ -14,12 +13,13 @@ import {
Star, Star,
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import { useMobxStore } from "lib/mobx/store-provider";
// helpers // helpers
import { copyUrlToClipboard } from "helpers/string.helper"; import { copyUrlToClipboard } from "helpers/string.helper";
import { renderShortDate, render24HourFormatTime, renderLongDateFormat } from "helpers/date-time.helper"; import { render24HourFormatTime, renderFormattedDate } from "helpers/date-time.helper";
// ui // ui
import { CustomMenu, Tooltip } from "@plane/ui"; import { CustomMenu, Tooltip } from "@plane/ui";
// components // components
@ -39,10 +39,10 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
// states // states
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false); const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
const [deletePageModal, setDeletePageModal] = useState(false); const [deletePageModal, setDeletePageModal] = useState(false);
// store // mobx store
const { const {
page: { archivePage, removeFromFavorites, addToFavorites, makePublic, makePrivate, restorePage }, page: { archivePage, removeFromFavorites, addToFavorites, makePublic, makePrivate, restorePage },
user: { currentProjectRole }, user: { currentUser, currentProjectRole },
projectMember: { projectMembers }, projectMember: { projectMembers },
} = useMobxStore(); } = useMobxStore();
// hooks // hooks
@ -145,7 +145,15 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
setCreateUpdatePageModal(true); setCreateUpdatePageModal(true);
}; };
const userCanEdit = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const ownerDetails = projectMembers?.find((projectMember) => projectMember.member.id === page.owned_by)?.member;
const isCurrentUserOwner = page.owned_by === currentUser?.id;
const userCanEdit =
isCurrentUserOwner ||
(currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole));
const userCanChangeAccess = isCurrentUserOwner;
const userCanArchive = isCurrentUserOwner || currentProjectRole === EUserWorkspaceRoles.ADMIN;
const userCanDelete = isCurrentUserOwner || currentProjectRole === EUserWorkspaceRoles.ADMIN;
return ( return (
<> <>
@ -185,7 +193,7 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
{page.archived_at ? ( {page.archived_at ? (
<Tooltip <Tooltip
tooltipContent={`Archived at ${render24HourFormatTime(page.archived_at)} on ${renderShortDate( tooltipContent={`Archived at ${render24HourFormatTime(page.archived_at)} on ${renderFormattedDate(
page.archived_at page.archived_at
)}`} )}`}
> >
@ -193,27 +201,25 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
</Tooltip> </Tooltip>
) : ( ) : (
<Tooltip <Tooltip
tooltipContent={`Last updated at ${render24HourFormatTime(page.updated_at)} on ${renderShortDate( tooltipContent={`Last updated at ${render24HourFormatTime(
page.updated_at page.updated_at
)}`} )} on ${renderFormattedDate(page.updated_at)}`}
> >
<p className="text-sm text-custom-text-200">{render24HourFormatTime(page.updated_at)}</p> <p className="text-sm text-custom-text-200">{render24HourFormatTime(page.updated_at)}</p>
</Tooltip> </Tooltip>
)} )}
{!page.archived_at && userCanEdit && ( <Tooltip tooltipContent={`${page.is_favorite ? "Remove from favorites" : "Mark as favorite"}`}>
<Tooltip tooltipContent={`${page.is_favorite ? "Remove from favorites" : "Mark as favorite"}`}> {page.is_favorite ? (
{page.is_favorite ? ( <button type="button" onClick={handleRemoveFromFavorites}>
<button type="button" onClick={handleRemoveFromFavorites}> <Star className="h-3.5 w-3.5 text-orange-400 fill-orange-400" />
<Star className="h-3.5 w-3.5 text-orange-400 fill-orange-400" /> </button>
</button> ) : (
) : ( <button type="button" onClick={handleAddToFavorites}>
<button type="button" onClick={handleAddToFavorites}> <Star className="h-3.5 w-3.5" />
<Star className="h-3.5 w-3.5" /> </button>
</button> )}
)} </Tooltip>
</Tooltip> {userCanChangeAccess && (
)}
{!page.archived_at && userCanEdit && (
<Tooltip <Tooltip
tooltipContent={`${ tooltipContent={`${
page.access page.access
@ -234,64 +240,57 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
)} )}
<Tooltip <Tooltip
position="top-right" position="top-right"
tooltipContent={`Created by ${ tooltipContent={`Created by ${ownerDetails?.display_name} on ${renderFormattedDate(page.created_at)}`}
projectMembers?.find((projectMember) => projectMember.member.id === page.created_by)?.member
.display_name ?? ""
} on ${renderLongDateFormat(`${page.created_at}`)}`}
> >
<AlertCircle className="h-3.5 w-3.5" /> <AlertCircle className="h-3.5 w-3.5" />
</Tooltip> </Tooltip>
{page.archived_at ? ( <CustomMenu width="auto" placement="bottom-end" className="!-m-1" verticalEllipsis>
<CustomMenu width="auto" placement="bottom-end" className="!-m-1" verticalEllipsis> {page.archived_at ? (
{userCanEdit && ( <>
<> {userCanArchive && (
<CustomMenu.MenuItem onClick={handleRestorePage}> <CustomMenu.MenuItem onClick={handleRestorePage}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ArchiveRestoreIcon className="h-3 w-3" /> <ArchiveRestoreIcon className="h-3 w-3" />
<span>Restore page</span> <span>Restore page</span>
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
)}
{userCanDelete && (
<CustomMenu.MenuItem onClick={handleDeletePage}> <CustomMenu.MenuItem onClick={handleDeletePage}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
<span>Delete page</span> <span>Delete page</span>
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
</> )}
)} </>
<CustomMenu.MenuItem onClick={handleCopyUrl}> ) : (
<div className="flex items-center gap-2"> <>
<LinkIcon className="h-3 w-3" /> {userCanEdit && (
<span>Copy page link</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
) : (
<CustomMenu width="auto" placement="bottom-end" className="!-m-1" verticalEllipsis>
{userCanEdit && (
<>
<CustomMenu.MenuItem onClick={handleEditPage}> <CustomMenu.MenuItem onClick={handleEditPage}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Pencil className="h-3 w-3" /> <Pencil className="h-3 w-3" />
<span>Edit page</span> <span>Edit page</span>
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
)}
{userCanArchive && (
<CustomMenu.MenuItem onClick={handleArchivePage}> <CustomMenu.MenuItem onClick={handleArchivePage}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Archive className="h-3 w-3" /> <Archive className="h-3 w-3" />
<span>Archive page</span> <span>Archive page</span>
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
</> )}
)} </>
<CustomMenu.MenuItem onClick={handleCopyUrl}> )}
<div className="flex items-center gap-2"> <CustomMenu.MenuItem onClick={handleCopyUrl}>
<LinkIcon className="h-3 w-3" /> <div className="flex items-center gap-2">
<span>Copy page link</span> <LinkIcon className="h-3 w-3" />
</div> <span>Copy page link</span>
</CustomMenu.MenuItem> </div>
</CustomMenu> </CustomMenu.MenuItem>
)} </CustomMenu>
</div> </div>
</div> </div>
</div> </div>

View File

@ -10,9 +10,11 @@ import { NewEmptyState } from "components/common/new-empty-state";
// ui // ui
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// images // images
import emptyPage from "public/empty-state/empty_page.webp"; import emptyPage from "public/empty-state/empty_page.png";
// types // types
import { IPage } from "types"; import { IPage } from "types";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
type IPagesListView = { type IPagesListView = {
pages: IPage[]; pages: IPage[];
@ -20,11 +22,27 @@ type IPagesListView = {
export const PagesListView: FC<IPagesListView> = observer(({ pages }) => { export const PagesListView: FC<IPagesListView> = observer(({ pages }) => {
// store // store
const { commandPalette: commandPaletteStore } = useMobxStore(); const {
user: { currentProjectRole },
commandPalette: { toggleCreatePageModal },
} = useMobxStore();
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const canUserCreatePage =
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
const emptyStatePrimaryButton = canUserCreatePage
? {
primaryButton: {
icon: <Plus className="h-4 w-4" />,
text: "Create your first page",
onClick: () => toggleCreatePageModal(true),
},
}
: {};
return ( return (
<> <>
{pages && workspaceSlug && projectId ? ( {pages && workspaceSlug && projectId ? (
@ -51,11 +69,7 @@ export const PagesListView: FC<IPagesListView> = observer(({ pages }) => {
"We wrote Parth and Meeras love story. You could write your projects mission, goals, and eventual vision.", "We wrote Parth and Meeras love story. You could write your projects mission, goals, and eventual vision.",
direction: "right", direction: "right",
}} }}
primaryButton={{ {...emptyStatePrimaryButton}
icon: <Plus className="h-4 w-4" />,
text: "Create your first page",
onClick: () => commandPaletteStore.toggleCreatePageModal(true),
}}
/> />
)} )}
</div> </div>

View File

@ -9,7 +9,7 @@ import { NewEmptyState } from "components/common/new-empty-state";
// ui // ui
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// assets // assets
import emptyPage from "public/empty-state/empty_page.webp"; import emptyPage from "public/empty-state/empty_page.png";
// helpers // helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";

View File

@ -29,6 +29,7 @@ import { NextPageWithLayout } from "types/app";
import { IPage } from "types"; import { IPage } from "types";
// fetch-keys // fetch-keys
import { PAGE_DETAILS } from "constants/fetch-keys"; import { PAGE_DETAILS } from "constants/fetch-keys";
import { EUserWorkspaceRoles } from "constants/workspace";
// services // services
const fileService = new FileService(); const fileService = new FileService();
@ -42,6 +43,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
// store // store
const { const {
appConfig: { envConfig }, appConfig: { envConfig },
user: { currentProjectRole },
} = useMobxStore(); } = useMobxStore();
// router // router
const router = useRouter(); const router = useRouter();
@ -217,12 +219,24 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
/> />
); );
const isPageReadOnly =
pageDetails?.is_locked ||
(currentProjectRole && [EUserWorkspaceRoles.VIEWER, EUserWorkspaceRoles.GUEST].includes(currentProjectRole));
const isCurrentUserOwner = pageDetails?.owned_by === user?.id;
const userCanDuplicate =
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
const userCanArchive = isCurrentUserOwner || currentProjectRole === EUserWorkspaceRoles.ADMIN;
const userCanLock =
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
return ( return (
<> <>
{pageDetails ? ( {pageDetails ? (
<div className="flex h-full flex-col justify-between"> <div className="flex h-full flex-col justify-between">
<div className="h-full w-full overflow-hidden"> <div className="h-full w-full overflow-hidden">
{pageDetails.is_locked || pageDetails.archived_at ? ( {isPageReadOnly ? (
<DocumentReadOnlyEditorWithRef <DocumentReadOnlyEditorWithRef
ref={editorRef} ref={editorRef}
value={pageDetails.description_html} value={pageDetails.description_html}
@ -278,18 +292,16 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
setIsSubmitting("submitting"); setIsSubmitting("submitting");
debouncedFormSave(); debouncedFormSave();
}} }}
duplicationConfig={{ action: duplicate_page }} duplicationConfig={userCanDuplicate ? { action: duplicate_page } : undefined}
pageArchiveConfig={ pageArchiveConfig={
user && pageDetails.owned_by === user.id userCanArchive
? { ? {
is_archived: pageDetails.archived_at ? true : false, is_archived: pageDetails.archived_at ? true : false,
action: pageDetails.archived_at ? unArchivePage : archivePage, action: pageDetails.archived_at ? unArchivePage : archivePage,
} }
: undefined : undefined
} }
pageLockConfig={ pageLockConfig={userCanLock ? { is_locked: false, action: lockPage } : undefined}
user && pageDetails.owned_by === user.id ? { is_locked: false, action: lockPage } : undefined
}
/> />
)} )}
/> />

View File

@ -162,7 +162,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
ProjectPagesPage.getLayout = function getLayout(page: ReactElement) { ProjectPagesPage.getLayout = function getLayout(page: ReactElement) {
return ( return (
<AppLayout header={<PagesHeader showButton />} withProjectWrapper> <AppLayout header={<PagesHeader />} withProjectWrapper>
{page} {page}
</AppLayout> </AppLayout>
); );

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB