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 GitHub
parent 5317de5919
commit d204cc7d6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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 { observer } from "mobx-react-lite";
import { FileText, Plus } from "lucide-react";
@ -8,18 +7,22 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { Breadcrumbs, Button } from "@plane/ui";
// helper
import { renderEmoji } from "helpers/emoji.helper";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export interface IPagesHeaderProps {
showButton?: boolean;
}
export const PagesHeader: FC<IPagesHeaderProps> = observer((props) => {
const { showButton = false } = props;
export const PagesHeader = observer(() => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// mobx store
const {
user: { currentProjectRole },
project: { currentProjectDetails },
commandPalette: { toggleCreatePageModal },
} = useMobxStore();
const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore();
const { currentProjectDetails } = projectStore;
const canUserCreatePage =
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
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">
@ -50,14 +53,9 @@ export const PagesHeader: FC<IPagesHeaderProps> = observer((props) => {
</Breadcrumbs>
</div>
</div>
{showButton && (
{canUserCreatePage && (
<div className="flex items-center gap-2">
<Button
variant="primary"
prependIcon={<Plus />}
size="sm"
onClick={() => commandPaletteStore.toggleCreatePageModal(true)}
>
<Button variant="primary" prependIcon={<Plus />} size="sm" onClick={() => toggleCreatePageModal(true)}>
Create Page
</Button>
</div>

View File

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

View File

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

View File

@ -10,9 +10,11 @@ import { NewEmptyState } from "components/common/new-empty-state";
// ui
import { Loader } from "@plane/ui";
// images
import emptyPage from "public/empty-state/empty_page.webp";
import emptyPage from "public/empty-state/empty_page.png";
// types
import { IPage } from "types";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
type IPagesListView = {
pages: IPage[];
@ -20,11 +22,27 @@ type IPagesListView = {
export const PagesListView: FC<IPagesListView> = observer(({ pages }) => {
// store
const { commandPalette: commandPaletteStore } = useMobxStore();
const {
user: { currentProjectRole },
commandPalette: { toggleCreatePageModal },
} = useMobxStore();
// router
const router = useRouter();
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 (
<>
{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.",
direction: "right",
}}
primaryButton={{
icon: <Plus className="h-4 w-4" />,
text: "Create your first page",
onClick: () => commandPaletteStore.toggleCreatePageModal(true),
}}
{...emptyStatePrimaryButton}
/>
)}
</div>

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB