Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d238bac
dev: support for edition specific options in pages
aaryan610 Nov 14, 2024
9ceb91c
refactor: page quick actions
aaryan610 Nov 18, 2024
bb00042
chore: add customizable page actions
aaryan610 Nov 20, 2024
835440f
fix: type errors
aaryan610 Nov 20, 2024
1314d3d
dev: hook to get page operations
aaryan610 Nov 20, 2024
8416b48
refactor: remove unnecessary props
aaryan610 Nov 20, 2024
436e4ac
chore: add permisssions to duplicate page endpoint
aaryan610 Nov 20, 2024
c060024
chore: memoize arranged options
aaryan610 Nov 21, 2024
84acc60
Merge branch 'preview' of https://github.com/makeplane/plane into dev…
aaryan610 Nov 21, 2024
f0a41bd
chore: use enum for page access
aaryan610 Nov 21, 2024
9bcbf24
chore: add type assertion
aaryan610 Nov 21, 2024
8806a67
fix: auth for access change and delete
aaryan610 Nov 21, 2024
3b6892d
fix: merge conflicts resolved from preview
aaryan610 Nov 22, 2024
d29ab80
fix: removing readonly editor
Palanikannan1437 Dec 9, 2024
383fb56
fix: merge conflicts resolved from preview
aaryan610 Dec 16, 2024
d52b747
chore: add sync for page access cahnge
aaryan610 Dec 16, 2024
2b06dfa
fix: sync state
Palanikannan1437 Dec 16, 2024
09f2be4
fix: indexeddb sync loader added
Palanikannan1437 Dec 16, 2024
7959a19
fix: remove node error fixed
Palanikannan1437 Dec 16, 2024
a7ddf3d
style: page title and checkbox
aaryan610 Dec 17, 2024
96c2f6d
chore: removing the syncing logic
Palanikannan1437 Dec 17, 2024
2fc1ac7
revert: is editable check removed in display message
Palanikannan1437 Dec 17, 2024
25abb9e
fix: editable field optional
Palanikannan1437 Dec 17, 2024
6189411
fix: editable removed as optional prop
Palanikannan1437 Dec 17, 2024
f24d8a1
fix: extra options import fix
Palanikannan1437 Dec 17, 2024
4dd215c
Merge branch 'dev/page-options' into fix/realtime-ref
Palanikannan1437 Dec 17, 2024
6fe2eef
fix: remove readonly stuff
Palanikannan1437 Dec 17, 2024
43db528
fix: added toggle access
Palanikannan1437 Dec 17, 2024
e0958b4
fix: merge conflicts resolved from preview
aaryan610 Dec 18, 2024
3992bd2
chore: add access change sync
aaryan610 Dec 18, 2024
63c59df
fix: full width toggle
aaryan610 Dec 19, 2024
732da3e
refactor: types and enums added
aaryan610 Dec 19, 2024
a5bae52
refactore: update store action
aaryan610 Dec 19, 2024
ba90bfc
chore: changed the duplicate viewset
NarayanBavisetti Dec 30, 2024
0c963ff
fix: merge conflicts resolved from preview
NarayanBavisetti Dec 30, 2024
3527e6d
fix: remove the page binary
NarayanBavisetti Dec 30, 2024
190d0c6
fix: duplicate page action
aaryan610 Dec 30, 2024
941d4bb
fix: merge conflicts
Palanikannan1437 Dec 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apiserver/plane/app/serializers/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ def create(self, validated_data):
labels = validated_data.pop("labels", None)
project_id = self.context["project_id"]
owned_by_id = self.context["owned_by_id"]
description = self.context["description"]
description_binary = self.context["description_binary"]
description_html = self.context["description_html"]

# Get the workspace id from the project
Expand All @@ -62,6 +64,8 @@ def create(self, validated_data):
# Create the page
page = Page.objects.create(
**validated_data,
description=description,
description_binary=description_binary,
description_html=description_html,
owned_by_id=owned_by_id,
workspace_id=project.workspace_id,
Expand Down
6 changes: 6 additions & 0 deletions apiserver/plane/app/urls/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
SubPagesEndpoint,
PagesDescriptionViewSet,
PageVersionEndpoint,
PageDuplicateEndpoint,
)


Expand Down Expand Up @@ -78,4 +79,9 @@
PageVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/duplicate/",
PageDuplicateEndpoint.as_view(),
name="page-duplicate",
),
]
1 change: 1 addition & 0 deletions apiserver/plane/app/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
PageLogEndpoint,
SubPagesEndpoint,
PagesDescriptionViewSet,
PageDuplicateEndpoint,
)
from .page.version import PageVersionEndpoint

Expand Down
36 changes: 36 additions & 0 deletions apiserver/plane/app/views/page/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ def create(self, request, slug, project_id):
context={
"project_id": project_id,
"owned_by_id": request.user.id,
"description": request.data.get("description", {}),
"description_binary": request.data.get("description_binary", None),
"description_html": request.data.get("description_html", "<p></p>"),
},
)
Expand Down Expand Up @@ -553,3 +555,37 @@ def partial_update(self, request, slug, project_id, pk):
return Response({"message": "Updated successfully"})
else:
return Response({"error": "No binary data provided"})


class PageDuplicateEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def post(self, request, slug, project_id, page_id):
page = Page.objects.filter(
pk=page_id, workspace__slug=slug, projects__id=project_id
).first()

# get all the project ids where page is present
project_ids = ProjectPage.objects.filter(page_id=page_id).values_list(
"project_id", flat=True
)

page.pk = None
page.name = f"{page.name} (Copy)"
page.description_binary = None
page.save()

for project_id in project_ids:
ProjectPage.objects.create(
workspace_id=page.workspace_id,
project_id=project_id,
page_id=page.id,
created_by_id=page.created_by_id,
updated_by_id=page.updated_by_id,
)

page_transaction.delay(
{"description_html": page.description_html}, None, page.id
)
page = Page.objects.get(pk=page.id)
serializer = PageDetailSerializer(page)
return Response(serializer.data, status=status.HTTP_201_CREATED)
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export const DocumentCollaborativeEvents = {
unlock: { client: "unlocked", server: "unlock" },
archive: { client: "archived", server: "archive" },
unarchive: { client: "unarchived", server: "unarchive" },
"make-public": { client: "made-public", server: "make-public" },
"make-private": { client: "made-private", server: "make-private" },
} as const;
30 changes: 17 additions & 13 deletions packages/ui/src/dropdowns/context-menu/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,23 @@ export const ContextMenuItem: React.FC<ContextMenuItemProps> = (props) => {
onMouseEnter={handleActiveItem}
disabled={item.disabled}
>
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
<div>
<h5>{item.title}</h5>
{item.description && (
<p
className={cn("text-custom-text-300 whitespace-pre-line", {
"text-custom-text-400": item.disabled,
})}
>
{item.description}
</p>
)}
</div>
{item.customContent ?? (
<>
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
<div>
<h5>{item.title}</h5>
{item.description && (
<p
className={cn("text-custom-text-300 whitespace-pre-line", {
"text-custom-text-400": item.disabled,
})}
>
{item.description}
</p>
)}
</div>
</>
)}
</button>
);
};
3 changes: 2 additions & 1 deletion packages/ui/src/dropdowns/context-menu/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { usePlatformOS } from "../../hooks/use-platform-os";

export type TContextMenuItem = {
key: string;
title: string;
customContent?: React.ReactNode;
title?: string;
description?: string;
icon?: React.FC<any>;
action: () => void;
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/dropdowns/custom-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => {
isOpen && onMenuClose && onMenuClose();
if (isOpen) onMenuClose?.();
setIsOpen(false);
};

Expand Down Expand Up @@ -216,7 +216,7 @@ const MenuItem: React.FC<ICustomMenuItemProps> = (props) => {
)}
onClick={(e) => {
close();
onClick && onClick(e);
onClick?.(e);
}}
disabled={disabled}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const PageDetailsPage = observer(() => {
const { getWorkspaceBySlug } = useWorkspace();
// derived values
const workspaceId = workspaceSlug ? (getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "") : "";
const { id, name, updateDescription } = page;
const { canCurrentUserAccessPage, id, name, updateDescription } = page;
// entity search handler
const fetchEntityCallback = useCallback(
async (payload: TSearchEntityRequestPayload) =>
Expand Down Expand Up @@ -129,7 +129,7 @@ const PageDetailsPage = observer(() => {
</div>
);

if (pageDetailsError)
if (pageDetailsError || !canCurrentUserAccessPage)
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>
Expand Down
1 change: 1 addition & 0 deletions web/ce/components/pages/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./editor";
export * from "./modals";
export * from "./extra-actions";
1 change: 1 addition & 0 deletions web/ce/components/pages/modals/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./move-page-modal";
10 changes: 10 additions & 0 deletions web/ce/components/pages/modals/move-page-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// store types
import { TPageInstance } from "@/store/pages/base-page";

export type TMovePageModalProps = {
isOpen: boolean;
onClose: () => void;
page: TPageInstance;
};

export const MovePageModal: React.FC<TMovePageModalProps> = () => null;
195 changes: 195 additions & 0 deletions web/core/components/pages/dropdowns/actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
"use client";

import { useMemo, useState } from "react";
import { observer } from "mobx-react";
import {
ArchiveRestoreIcon,
Copy,
ExternalLink,
FileOutput,
Globe2,
Link,
Lock,
LockKeyhole,
LockKeyholeOpen,
Trash2,
} from "lucide-react";
// plane editor
import { EditorRefApi } from "@plane/editor";
// plane ui
import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui";
// components
import { DeletePageModal } from "@/components/pages";
// constants
import { EPageAccess } from "@/constants/page";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { usePageOperations } from "@/hooks/use-page-operations";
// plane web components
import { MovePageModal } from "@/plane-web/components/pages";
// store types
import { TPageInstance } from "@/store/pages/base-page";

export type TPageActions =
| "full-screen"
| "copy-markdown"
| "toggle-lock"
| "toggle-access"
| "open-in-new-tab"
| "copy-link"
| "make-a-copy"
| "archive-restore"
| "delete"
| "version-history"
| "export"
| "move";

type Props = {
editorRef?: EditorRefApi | null;
extraOptions?: (TContextMenuItem & { key: TPageActions })[];
optionsOrder: TPageActions[];
page: TPageInstance;
parentRef?: React.RefObject<HTMLElement>;
};

export const PageActions: React.FC<Props> = observer((props) => {
const { editorRef, extraOptions, optionsOrder, page, parentRef } = props;
// states
const [deletePageModal, setDeletePageModal] = useState(false);
const [movePageModal, setMovePageModal] = useState(false);
// page operations
const { pageOperations } = usePageOperations({
editorRef,
page,
});
// derived values
const {
access,
archived_at,
is_locked,
canCurrentUserArchivePage,
canCurrentUserChangeAccess,
canCurrentUserDeletePage,
canCurrentUserDuplicatePage,
canCurrentUserLockPage,
canCurrentUserMovePage,
} = page;
// menu items
const MENU_ITEMS: (TContextMenuItem & { key: TPageActions })[] = useMemo(() => {
const menuItems: (TContextMenuItem & { key: TPageActions })[] = [
{
key: "toggle-lock",
action: pageOperations.toggleLock,
title: is_locked ? "Unlock" : "Lock",
icon: is_locked ? LockKeyholeOpen : LockKeyhole,
shouldRender: canCurrentUserLockPage,
},
{
key: "toggle-access",
action: pageOperations.toggleAccess,
title: access === EPageAccess.PUBLIC ? "Make private" : "Make public",
icon: access === EPageAccess.PUBLIC ? Lock : Globe2,
shouldRender: canCurrentUserChangeAccess && !archived_at,
},
{
key: "open-in-new-tab",
action: pageOperations.openInNewTab,
title: "Open in new tab",
icon: ExternalLink,
shouldRender: true,
},
{
key: "copy-link",
action: pageOperations.copyLink,
title: "Copy link",
icon: Link,
shouldRender: true,
},
{
key: "make-a-copy",
action: pageOperations.duplicate,
title: "Make a copy",
icon: Copy,
shouldRender: canCurrentUserDuplicatePage,
},
{
key: "archive-restore",
action: pageOperations.toggleArchive,
title: archived_at ? "Restore" : "Archive",
icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon,
shouldRender: canCurrentUserArchivePage,
},
{
key: "delete",
action: () => setDeletePageModal(true),
title: "Delete",
icon: Trash2,
shouldRender: canCurrentUserDeletePage && !!archived_at,
},
{
key: "move",
action: () => setMovePageModal(true),
title: "Move",
icon: FileOutput,
shouldRender: canCurrentUserMovePage,
},
];
if (extraOptions) {
menuItems.push(...extraOptions);
}
return menuItems;
}, [
access,
archived_at,
extraOptions,
is_locked,
canCurrentUserArchivePage,
canCurrentUserChangeAccess,
canCurrentUserDeletePage,
canCurrentUserDuplicatePage,
canCurrentUserLockPage,
canCurrentUserMovePage,
pageOperations,
]);
// arrange options
const arrangedOptions = useMemo(
() =>
optionsOrder
.map((key) => MENU_ITEMS.find((item) => item.key === key))
.filter((item) => !!item) as (TContextMenuItem & { key: TPageActions })[],
[optionsOrder, MENU_ITEMS]
);

return (
<>
<MovePageModal isOpen={movePageModal} onClose={() => setMovePageModal(false)} page={page} />
<DeletePageModal isOpen={deletePageModal} onClose={() => setDeletePageModal(false)} page={page} />
{parentRef && <ContextMenu parentRef={parentRef} items={arrangedOptions} />}
<CustomMenu placement="bottom-end" optionsClassName="max-h-[90vh]" ellipsis closeOnSelect>
{arrangedOptions.map((item) => {
if (item.shouldRender === false) return null;
return (
<CustomMenu.MenuItem
key={item.key}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
item.action?.();
}}
className={cn("flex items-center gap-2", item.className)}
disabled={item.disabled}
>
{item.customContent ?? (
<>
{item.icon && <item.icon className="size-3" />}
{item.title}
</>
)}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</>
);
});
2 changes: 1 addition & 1 deletion web/core/components/pages/dropdowns/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from "./actions";
export * from "./edit-information-popover";
export * from "./quick-actions";
Loading
Loading