forked from github/plane
chore: added authorization to pages (#3006)
* chore: updated pages authorization * chore: updated pages empty state image
This commit is contained in:
parent
ddb6f8ed47
commit
f8585e87f8
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 Meera’s love story. You could write your project’s 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>
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -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>
|
||||
);
|
||||
|
BIN
web/public/empty-state/empty_page.png
Normal file
BIN
web/public/empty-state/empty_page.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
Before Width: | Height: | Size: 46 KiB |
Loading…
Reference in New Issue
Block a user