mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Improvement: High Performance MobX Integration for Pages ✈︎ (#3397)
* fix: removed parameters `workspace`, `project` & `id` from the patch calls * feat: modified components to work with new pages hooks * feat: modified stores * feat: modified initial component * feat: component implementation changes * feat: store implementation * refactor pages store * feat: updated page store to perform async operations faster * fix: added types for archive and restore pages * feat: implemented archive and restore pages * fix: page creating twice when form submit * feat: updated create-page-modal * feat: updated page form and delete page modal * fix: create page modal not updating isSubmitted prop * feat: list items and list view refactored for pages * feat: refactored project-page-store for inserting computed pagesids * chore: renamed project pages hook * feat: added favourite pages implementation * fix: implemented store for archived pages * fix: project page store for recent pages * fix: issue suggestions breaking pages * fix: issue embeds and suggestions breaking * feat: implemented page store and project page store in page editor * chore: lock file changes * fix: modified page details header to catch mobx updates instead of swr calls * fix: modified usePage hook to fetch page details when reloaded directly on page * fix: fixed deleting pages * fix: removed render on props changed * feat: implemented page store inside page details * fix: role change in pages archives * fix: rerending of pages on tab change * fix: reimplementation of peek overview inside pages * chore: typo fixes * fix: issue suggestion widget selecting wrong issues on click * feat: added labels in pages * fix: deepsource errors fixed * fix: build errors * fix: review comments * fix: removed swr hooks from the `usePage` store hook and refactored `issueEmbed` hook * fix: resolved reviewed comments --------- Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
This commit is contained in:
parent
d3dedc8e51
commit
06a7bdffd7
@ -1,5 +1,5 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
from datetime import timedelta, date, datetime
|
from datetime import date, datetime, timedelta
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
@ -7,30 +7,19 @@ from django.db.models import Exists, OuterRef, Q
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
# Module imports
|
|
||||||
from .base import BaseViewSet, BaseAPIView
|
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
from plane.db.models import (
|
from plane.app.serializers import (IssueLiteSerializer, PageFavoriteSerializer,
|
||||||
Page,
|
PageLogSerializer, PageSerializer,
|
||||||
PageFavorite,
|
SubPageSerializer)
|
||||||
Issue,
|
from plane.db.models import (Issue, IssueActivity, IssueAssignee, Page,
|
||||||
IssueAssignee,
|
PageFavorite, PageLog, ProjectMember)
|
||||||
IssueActivity,
|
|
||||||
PageLog,
|
# Module imports
|
||||||
ProjectMember,
|
from .base import BaseAPIView, BaseViewSet
|
||||||
)
|
|
||||||
from plane.app.serializers import (
|
|
||||||
PageSerializer,
|
|
||||||
PageFavoriteSerializer,
|
|
||||||
PageLogSerializer,
|
|
||||||
IssueLiteSerializer,
|
|
||||||
SubPageSerializer,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def unarchive_archive_page_and_descendants(page_id, archived_at):
|
def unarchive_archive_page_and_descendants(page_id, archived_at):
|
||||||
@ -175,7 +164,7 @@ class PageViewSet(BaseViewSet):
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
member=request.user,
|
member=request.user,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
role__gt=20,
|
role__gte=20,
|
||||||
).exists()
|
).exists()
|
||||||
or request.user.id != page.owned_by_id
|
or request.user.id != page.owned_by_id
|
||||||
):
|
):
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
"@plane/editor-core": "*",
|
"@plane/editor-core": "*",
|
||||||
"@plane/editor-extensions": "*",
|
"@plane/editor-extensions": "*",
|
||||||
"@plane/ui": "*",
|
"@plane/ui": "*",
|
||||||
|
"@tippyjs/react": "^4.2.6",
|
||||||
"@tiptap/core": "^2.1.13",
|
"@tiptap/core": "^2.1.13",
|
||||||
"@tiptap/extension-placeholder": "^2.1.13",
|
"@tiptap/extension-placeholder": "^2.1.13",
|
||||||
"@tiptap/pm": "^2.1.13",
|
"@tiptap/pm": "^2.1.13",
|
||||||
|
@ -18,7 +18,7 @@ import {
|
|||||||
|
|
||||||
type IPageRenderer = {
|
type IPageRenderer = {
|
||||||
documentDetails: DocumentDetails;
|
documentDetails: DocumentDetails;
|
||||||
updatePageTitle: (title: string) => Promise<void>;
|
updatePageTitle: (title: string) => void;
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
onActionCompleteHandler: (action: {
|
onActionCompleteHandler: (action: {
|
||||||
title: string;
|
title: string;
|
||||||
@ -30,18 +30,6 @@ type IPageRenderer = {
|
|||||||
readonly: boolean;
|
readonly: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const debounce = (func: (...args: any[]) => void, wait: number) => {
|
|
||||||
let timeout: NodeJS.Timeout | null = null;
|
|
||||||
return function executedFunction(...args: any[]) {
|
|
||||||
const later = () => {
|
|
||||||
if (timeout) clearTimeout(timeout);
|
|
||||||
func(...args);
|
|
||||||
};
|
|
||||||
if (timeout) clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(later, wait);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PageRenderer = (props: IPageRenderer) => {
|
export const PageRenderer = (props: IPageRenderer) => {
|
||||||
const { documentDetails, editor, editorClassNames, editorContentCustomClassNames, updatePageTitle, readonly } = props;
|
const { documentDetails, editor, editorClassNames, editorContentCustomClassNames, updatePageTitle, readonly } = props;
|
||||||
|
|
||||||
@ -64,11 +52,9 @@ export const PageRenderer = (props: IPageRenderer) => {
|
|||||||
|
|
||||||
const { getFloatingProps } = useInteractions([dismiss]);
|
const { getFloatingProps } = useInteractions([dismiss]);
|
||||||
|
|
||||||
const debouncedUpdatePageTitle = debounce(updatePageTitle, 300);
|
|
||||||
|
|
||||||
const handlePageTitleChange = (title: string) => {
|
const handlePageTitleChange = (title: string) => {
|
||||||
setPagetitle(title);
|
setPagetitle(title);
|
||||||
debouncedUpdatePageTitle(title);
|
updatePageTitle(title);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [cleanup, setcleanup] = useState(() => () => {});
|
const [cleanup, setcleanup] = useState(() => () => {});
|
||||||
|
@ -26,7 +26,7 @@ export const DocumentEditorExtensions = (
|
|||||||
.focus()
|
.focus()
|
||||||
.insertContentAt(
|
.insertContentAt(
|
||||||
range,
|
range,
|
||||||
"<p class='text-sm bg-gray-300 w-fit pl-3 pr-3 pt-1 pb-1 rounded shadow-sm'>#issue_</p>"
|
"<p class='text-sm bg-gray-300 w-fit pl-3 pr-3 pt-1 pb-1 rounded shadow-sm'>#issue_</p>\n"
|
||||||
)
|
)
|
||||||
.run();
|
.run();
|
||||||
},
|
},
|
||||||
|
@ -24,7 +24,7 @@ export const IssueSuggestions = (suggestions: any[]) => {
|
|||||||
title: suggestion.name,
|
title: suggestion.name,
|
||||||
priority: suggestion.priority.toString(),
|
priority: suggestion.priority.toString(),
|
||||||
identifier: `${suggestion.project_detail.identifier}-${suggestion.sequence_id}`,
|
identifier: `${suggestion.project_detail.identifier}-${suggestion.sequence_id}`,
|
||||||
state: suggestion.state_detail.name,
|
state: suggestion.state_detail && suggestion.state_detail.name ? suggestion.state_detail.name : "Todo",
|
||||||
command: ({ editor, range }) => {
|
command: ({ editor, range }) => {
|
||||||
editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
|
@ -9,6 +9,8 @@ export const IssueEmbedSuggestions = Extension.create({
|
|||||||
addOptions() {
|
addOptions() {
|
||||||
return {
|
return {
|
||||||
suggestion: {
|
suggestion: {
|
||||||
|
char: "#issue_",
|
||||||
|
allowSpaces: true,
|
||||||
command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
|
command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
|
||||||
props.command({ editor, range });
|
props.command({ editor, range });
|
||||||
},
|
},
|
||||||
@ -18,11 +20,8 @@ export const IssueEmbedSuggestions = Extension.create({
|
|||||||
addProseMirrorPlugins() {
|
addProseMirrorPlugins() {
|
||||||
return [
|
return [
|
||||||
Suggestion({
|
Suggestion({
|
||||||
char: "#issue_",
|
|
||||||
pluginKey: new PluginKey("issue-embed-suggestions"),
|
pluginKey: new PluginKey("issue-embed-suggestions"),
|
||||||
editor: this.editor,
|
editor: this.editor,
|
||||||
allowSpaces: true,
|
|
||||||
|
|
||||||
...this.options.suggestion,
|
...this.options.suggestion,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
@ -53,7 +53,7 @@ const IssueSuggestionList = ({
|
|||||||
const commandListContainer = useRef<HTMLDivElement>(null);
|
const commandListContainer = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let newDisplayedItems: { [key: string]: IssueSuggestionProps[] } = {};
|
const newDisplayedItems: { [key: string]: IssueSuggestionProps[] } = {};
|
||||||
let totalLength = 0;
|
let totalLength = 0;
|
||||||
sections.forEach((section) => {
|
sections.forEach((section) => {
|
||||||
newDisplayedItems[section] = items.filter((item) => item.state === section).slice(0, 5);
|
newDisplayedItems[section] = items.filter((item) => item.state === section).slice(0, 5);
|
||||||
@ -65,8 +65,8 @@ const IssueSuggestionList = ({
|
|||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
const selectItem = useCallback(
|
const selectItem = useCallback(
|
||||||
(index: number) => {
|
(section: string, index: number) => {
|
||||||
const item = displayedItems[currentSection][index];
|
const item = displayedItems[section][index];
|
||||||
if (item) {
|
if (item) {
|
||||||
command(item);
|
command(item);
|
||||||
}
|
}
|
||||||
@ -87,6 +87,7 @@ const IssueSuggestionList = ({
|
|||||||
setSelectedIndex(
|
setSelectedIndex(
|
||||||
(selectedIndex + displayedItems[currentSection].length - 1) % displayedItems[currentSection].length
|
(selectedIndex + displayedItems[currentSection].length - 1) % displayedItems[currentSection].length
|
||||||
);
|
);
|
||||||
|
e.stopPropagation();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (e.key === "ArrowDown") {
|
if (e.key === "ArrowDown") {
|
||||||
@ -101,10 +102,12 @@ const IssueSuggestionList = ({
|
|||||||
[currentSection]: [...prevItems[currentSection], ...nextItems],
|
[currentSection]: [...prevItems[currentSection], ...nextItems],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
e.stopPropagation();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
selectItem(selectedIndex);
|
selectItem(currentSection, selectedIndex);
|
||||||
|
e.stopPropagation();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (e.key === "Tab") {
|
if (e.key === "Tab") {
|
||||||
@ -112,6 +115,7 @@ const IssueSuggestionList = ({
|
|||||||
const nextSectionIndex = (currentSectionIndex + 1) % sections.length;
|
const nextSectionIndex = (currentSectionIndex + 1) % sections.length;
|
||||||
setCurrentSection(sections[nextSectionIndex]);
|
setCurrentSection(sections[nextSectionIndex]);
|
||||||
setSelectedIndex(0);
|
setSelectedIndex(0);
|
||||||
|
e.stopPropagation();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -172,7 +176,7 @@ const IssueSuggestionList = ({
|
|||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
key={item.identifier}
|
key={item.identifier}
|
||||||
onClick={() => selectItem(index)}
|
onClick={() => selectItem(section, index)}
|
||||||
>
|
>
|
||||||
<h5 className="whitespace-nowrap text-xs text-custom-text-300">{item.identifier}</h5>
|
<h5 className="whitespace-nowrap text-xs text-custom-text-300">{item.identifier}</h5>
|
||||||
<PriorityIcon priority={item.priority} />
|
<PriorityIcon priority={item.priority} />
|
||||||
@ -195,7 +199,7 @@ export const IssueListRenderer = () => {
|
|||||||
let popup: any | null = null;
|
let popup: any | null = null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
|
onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
|
||||||
component = new ReactRenderer(IssueSuggestionList, {
|
component = new ReactRenderer(IssueSuggestionList, {
|
||||||
props,
|
props,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -210,10 +214,10 @@ export const IssueListRenderer = () => {
|
|||||||
showOnCreate: true,
|
showOnCreate: true,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
trigger: "manual",
|
trigger: "manual",
|
||||||
placement: "right",
|
placement: "bottom-start",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
|
onUpdate: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
|
||||||
component?.updateProps(props);
|
component?.updateProps(props);
|
||||||
|
|
||||||
popup &&
|
popup &&
|
||||||
|
@ -15,8 +15,7 @@ export const IssueWidgetCard = (props) => {
|
|||||||
setIssueDetails(issue);
|
setIssueDetails(issue);
|
||||||
setLoading(0);
|
setLoading(0);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
console.log(error);
|
|
||||||
setLoading(-1);
|
setLoading(-1);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
@ -30,7 +29,9 @@ export const IssueWidgetCard = (props) => {
|
|||||||
{loading == 0 ? (
|
{loading == 0 ? (
|
||||||
<div
|
<div
|
||||||
onClick={completeIssueEmbedAction}
|
onClick={completeIssueEmbedAction}
|
||||||
className="w-full cursor-pointer space-y-2 rounded-md border-[0.5px] border-custom-border-200 p-3 shadow-custom-shadow-2xs"
|
className={`${
|
||||||
|
props.selected ? "border-custom-primary-200 border-[2px]" : ""
|
||||||
|
} w-full cursor-pointer space-y-2 rounded-md border-[0.5px] border-custom-border-200 p-3 shadow-custom-shadow-2xs`}
|
||||||
>
|
>
|
||||||
<h5 className="text-xs text-custom-text-300">
|
<h5 className="text-xs text-custom-text-300">
|
||||||
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
|
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
|
||||||
|
@ -16,7 +16,7 @@ interface IDocumentEditor {
|
|||||||
// document info
|
// document info
|
||||||
documentDetails: DocumentDetails;
|
documentDetails: DocumentDetails;
|
||||||
value: string;
|
value: string;
|
||||||
rerenderOnPropsChange: {
|
rerenderOnPropsChange?: {
|
||||||
id: string;
|
id: string;
|
||||||
description_html: string;
|
description_html: string;
|
||||||
};
|
};
|
||||||
@ -39,7 +39,7 @@ interface IDocumentEditor {
|
|||||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||||
setShouldShowAlert?: (showAlert: boolean) => void;
|
setShouldShowAlert?: (showAlert: boolean) => void;
|
||||||
forwardedRef?: any;
|
forwardedRef?: any;
|
||||||
updatePageTitle: (title: string) => Promise<void>;
|
updatePageTitle: (title: string) => void;
|
||||||
debouncedUpdatesEnabled?: boolean;
|
debouncedUpdatesEnabled?: boolean;
|
||||||
isSubmitting: "submitting" | "submitted" | "saved";
|
isSubmitting: "submitting" | "submitted" | "saved";
|
||||||
|
|
||||||
|
@ -1,23 +1,17 @@
|
|||||||
import { FC } from "react";
|
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 useSWR from "swr";
|
|
||||||
import { FileText, Plus } from "lucide-react";
|
import { FileText, Plus } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useProject } from "hooks/store";
|
import { useApplication, usePage, useProject } from "hooks/store";
|
||||||
// services
|
|
||||||
import { PageService } from "services/page.service";
|
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button } from "@plane/ui";
|
import { Breadcrumbs, Button } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
// fetch-keys
|
|
||||||
import { PAGE_DETAILS } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
export interface IPagesHeaderProps {
|
export interface IPagesHeaderProps {
|
||||||
showButton?: boolean;
|
showButton?: boolean;
|
||||||
}
|
}
|
||||||
const pageService = new PageService();
|
|
||||||
|
|
||||||
export const PageDetailsHeader: FC<IPagesHeaderProps> = observer((props) => {
|
export const PageDetailsHeader: FC<IPagesHeaderProps> = observer((props) => {
|
||||||
const { showButton = false } = props;
|
const { showButton = false } = props;
|
||||||
@ -28,12 +22,7 @@ export const PageDetailsHeader: FC<IPagesHeaderProps> = observer((props) => {
|
|||||||
const { commandPalette: commandPaletteStore } = useApplication();
|
const { commandPalette: commandPaletteStore } = useApplication();
|
||||||
const { currentProjectDetails } = useProject();
|
const { currentProjectDetails } = useProject();
|
||||||
|
|
||||||
const { data: pageDetails } = useSWR(
|
const pageDetails = usePage(pageId as string);
|
||||||
workspaceSlug && currentProjectDetails?.id && pageId ? PAGE_DETAILS(pageId as string) : null,
|
|
||||||
workspaceSlug && currentProjectDetails?.id
|
|
||||||
? () => pageService.getPageDetails(workspaceSlug as string, currentProjectDetails.id, pageId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row 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 z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||||
|
@ -2,132 +2,56 @@ import React, { FC } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, usePage, useWorkspace } from "hooks/store";
|
import { useApplication } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// components
|
// components
|
||||||
import { PageForm } from "./page-form";
|
import { PageForm } from "./page-form";
|
||||||
// types
|
// types
|
||||||
import { IPage } from "@plane/types";
|
import { IPage } from "@plane/types";
|
||||||
|
import { useProjectPages } from "hooks/store/use-project-page";
|
||||||
|
import { IPageStore } from "store/page.store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data?: IPage | null;
|
// data?: IPage | null;
|
||||||
|
pageStore?: IPageStore;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateUpdatePageModal: FC<Props> = (props) => {
|
export const CreateUpdatePageModal: FC<Props> = (props) => {
|
||||||
const { isOpen, handleClose, data, projectId } = props;
|
const { isOpen, handleClose, projectId, pageStore } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const { createPage } = useProjectPages();
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
eventTracker: { postHogEventTracker },
|
eventTracker: { postHogEventTracker },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const { createPage, updatePage } = usePage();
|
|
||||||
// toast alert
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
handleClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const createProjectPage = async (payload: IPage) => {
|
const createProjectPage = async (payload: IPage) => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
await createPage(workspaceSlug.toString(), projectId, payload);
|
||||||
// await createPage(workspaceSlug.toString(), projectId, payload)
|
|
||||||
// .then((res) => {
|
|
||||||
// router.push(`/${workspaceSlug}/projects/${projectId}/pages/${res.id}`);
|
|
||||||
// onClose();
|
|
||||||
// setToastAlert({
|
|
||||||
// type: "success",
|
|
||||||
// title: "Success!",
|
|
||||||
// message: "Page created successfully.",
|
|
||||||
// });
|
|
||||||
// postHogEventTracker(
|
|
||||||
// "PAGE_CREATED",
|
|
||||||
// {
|
|
||||||
// ...res,
|
|
||||||
// state: "SUCCESS",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// isGrouping: true,
|
|
||||||
// groupType: "Workspace_metrics",
|
|
||||||
// groupId: currentWorkspace?.id!,
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// })
|
|
||||||
// .catch((err) => {
|
|
||||||
// setToastAlert({
|
|
||||||
// type: "error",
|
|
||||||
// title: "Error!",
|
|
||||||
// message: err.detail ?? "Page could not be created. Please try again.",
|
|
||||||
// });
|
|
||||||
// postHogEventTracker(
|
|
||||||
// "PAGE_CREATED",
|
|
||||||
// {
|
|
||||||
// state: "FAILED",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// isGrouping: true,
|
|
||||||
// groupType: "Workspace_metrics",
|
|
||||||
// groupId: currentWorkspace?.id!,
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateProjectPage = async (payload: IPage) => {
|
|
||||||
if (!data || !workspaceSlug) return;
|
|
||||||
|
|
||||||
// await updatePage(workspaceSlug.toString(), projectId, data.id, payload)
|
|
||||||
// .then((res) => {
|
|
||||||
// onClose();
|
|
||||||
// setToastAlert({
|
|
||||||
// type: "success",
|
|
||||||
// title: "Success!",
|
|
||||||
// message: "Page updated successfully.",
|
|
||||||
// });
|
|
||||||
// postHogEventTracker(
|
|
||||||
// "PAGE_UPDATED",
|
|
||||||
// {
|
|
||||||
// ...res,
|
|
||||||
// state: "SUCCESS",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// isGrouping: true,
|
|
||||||
// groupType: "Workspace_metrics",
|
|
||||||
// groupId: currentWorkspace?.id!,
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// })
|
|
||||||
// .catch((err) => {
|
|
||||||
// setToastAlert({
|
|
||||||
// type: "error",
|
|
||||||
// title: "Error!",
|
|
||||||
// message: err.detail ?? "Page could not be updated. Please try again.",
|
|
||||||
// });
|
|
||||||
// postHogEventTracker(
|
|
||||||
// "PAGE_UPDATED",
|
|
||||||
// {
|
|
||||||
// state: "FAILED",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// isGrouping: true,
|
|
||||||
// groupType: "Workspace_metrics",
|
|
||||||
// groupId: currentWorkspace?.id!,
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = async (formData: IPage) => {
|
const handleFormSubmit = async (formData: IPage) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
try {
|
||||||
if (!data) await createProjectPage(formData);
|
if (pageStore) {
|
||||||
else await updateProjectPage(formData);
|
if (pageStore.name !== formData.name) {
|
||||||
|
await pageStore.updateName(formData.name);
|
||||||
|
}
|
||||||
|
if (pageStore.access !== formData.access) {
|
||||||
|
formData.access === 1 ? await pageStore.makePrivate() : await pageStore.makePublic();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await createProjectPage(formData);
|
||||||
|
}
|
||||||
|
handleClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -157,7 +81,7 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
|||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 px-4 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-2xl">
|
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 px-4 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-2xl">
|
||||||
<PageForm handleFormSubmit={handleFormSubmit} handleClose={handleClose} data={data} />
|
<PageForm handleFormSubmit={handleFormSubmit} handleClose={handleClose} pageStore={pageStore} />
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,37 +9,45 @@ import useToast from "hooks/use-toast";
|
|||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import type { IPage } from "@plane/types";
|
import { useProjectPages } from "hooks/store/use-project-page";
|
||||||
|
|
||||||
type TConfirmPageDeletionProps = {
|
type TConfirmPageDeletionProps = {
|
||||||
data?: IPage | null;
|
pageId: string;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((props) => {
|
export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((props) => {
|
||||||
const { data, isOpen, onClose } = props;
|
const { pageId, isOpen, onClose } = props;
|
||||||
|
|
||||||
// states
|
// states
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { deletePage } = usePage();
|
const { deletePage } = useProjectPages();
|
||||||
|
const pageStore = usePage(pageId);
|
||||||
|
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
if (!pageStore) return null;
|
||||||
|
|
||||||
|
const { name } = pageStore;
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!data || !workspaceSlug || !projectId) return;
|
if (!pageId || !workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
|
|
||||||
await deletePage(workspaceSlug.toString(), data.project, data.id)
|
// Delete Page will only delete the page from the archive page map, at this point only archived pages can be deleted
|
||||||
|
await deletePage(workspaceSlug.toString(), projectId as string, pageId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
handleClose();
|
handleClose();
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -99,8 +107,8 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
|
|||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-sm text-custom-text-200">
|
<p className="text-sm text-custom-text-200">
|
||||||
Are you sure you want to delete page-{" "}
|
Are you sure you want to delete page-{" "}
|
||||||
<span className="break-words font-medium text-custom-text-100">{data?.name}</span>? The Page
|
<span className="break-words font-medium text-custom-text-100">{name}</span>? The Page will be
|
||||||
will be deleted permanently. This action cannot be undone.
|
deleted permanently. This action cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,11 +5,12 @@ import { Button, Input, Tooltip } from "@plane/ui";
|
|||||||
import { IPage } from "@plane/types";
|
import { IPage } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { PAGE_ACCESS_SPECIFIERS } from "constants/page";
|
import { PAGE_ACCESS_SPECIFIERS } from "constants/page";
|
||||||
|
import { IPageStore } from "store/page.store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleFormSubmit: (values: IPage) => Promise<void>;
|
handleFormSubmit: (values: IPage) => Promise<void>;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
data?: IPage | null;
|
pageStore?: IPageStore;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
@ -19,24 +20,24 @@ const defaultValues = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PageForm: React.FC<Props> = (props) => {
|
export const PageForm: React.FC<Props> = (props) => {
|
||||||
const { handleFormSubmit, handleClose, data } = props;
|
const { handleFormSubmit, handleClose, pageStore } = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
} = useForm<IPage>({
|
} = useForm<IPage>({
|
||||||
defaultValues: { ...defaultValues, ...data },
|
defaultValues: pageStore
|
||||||
|
? { name: pageStore.name, description: pageStore.description, access: pageStore.access }
|
||||||
|
: defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCreateUpdatePage = async (formData: IPage) => {
|
const handleCreateUpdatePage = (formData: IPage) => handleFormSubmit(formData);
|
||||||
await handleFormSubmit(formData);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleCreateUpdatePage)}>
|
<form onSubmit={handleSubmit(handleCreateUpdatePage)}>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">{data ? "Update" : "Create"} Page</h3>
|
<h3 className="text-lg font-medium leading-6 text-custom-text-100">{pageStore ? "Update" : "Create"} Page</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<Controller
|
<Controller
|
||||||
@ -104,7 +105,7 @@ export const PageForm: React.FC<Props> = (props) => {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={5}>
|
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={5}>
|
||||||
{data ? (isSubmitting ? "Updating..." : "Update page") : isSubmitting ? "Creating..." : "Create Page"}
|
{pageStore ? (isSubmitting ? "Updating..." : "Update page") : isSubmitting ? "Creating..." : "Create Page"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePage } from "hooks/store";
|
|
||||||
// components
|
|
||||||
import { PagesListView } from "components/pages/pages-list";
|
import { PagesListView } from "components/pages/pages-list";
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
|
import { useProjectPages } from "hooks/store/use-project-specific-pages";
|
||||||
|
|
||||||
export const AllPagesList: FC = observer(() => {
|
export const AllPagesList: FC = observer(() => {
|
||||||
// store
|
const pageStores = useProjectPages();
|
||||||
const { projectPageIds } = usePage();
|
// subscribing to the projectPageStore
|
||||||
|
const { projectPageIds } = pageStores;
|
||||||
|
|
||||||
if (!projectPageIds)
|
if (!projectPageIds) {
|
||||||
return (
|
return (
|
||||||
<Loader className="space-y-4">
|
<Loader className="space-y-4">
|
||||||
<Loader.Item height="40px" />
|
<Loader.Item height="40px" />
|
||||||
@ -19,6 +19,6 @@ export const AllPagesList: FC = observer(() => {
|
|||||||
<Loader.Item height="40px" />
|
<Loader.Item height="40px" />
|
||||||
</Loader>
|
</Loader>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return <PagesListView pageIds={projectPageIds} />;
|
return <PagesListView pageIds={projectPageIds} />;
|
||||||
});
|
});
|
||||||
|
@ -3,14 +3,15 @@ import { observer } from "mobx-react-lite";
|
|||||||
// components
|
// components
|
||||||
import { PagesListView } from "components/pages/pages-list";
|
import { PagesListView } from "components/pages/pages-list";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePage } from "hooks/store";
|
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
|
import { useProjectPages } from "hooks/store/use-project-specific-pages";
|
||||||
|
|
||||||
export const ArchivedPagesList: FC = observer(() => {
|
export const ArchivedPagesList: FC = observer(() => {
|
||||||
const { archivedProjectPageIds } = usePage();
|
const projectPageStore = useProjectPages();
|
||||||
|
const { archivedPageIds } = projectPageStore;
|
||||||
|
|
||||||
if (!archivedProjectPageIds)
|
if (!archivedPageIds)
|
||||||
return (
|
return (
|
||||||
<Loader className="space-y-4">
|
<Loader className="space-y-4">
|
||||||
<Loader.Item height="40px" />
|
<Loader.Item height="40px" />
|
||||||
@ -19,5 +20,5 @@ export const ArchivedPagesList: FC = observer(() => {
|
|||||||
</Loader>
|
</Loader>
|
||||||
);
|
);
|
||||||
|
|
||||||
return <PagesListView pageIds={archivedProjectPageIds} />;
|
return <PagesListView pageIds={archivedPageIds} />;
|
||||||
});
|
});
|
||||||
|
@ -3,12 +3,13 @@ import { observer } from "mobx-react-lite";
|
|||||||
// components
|
// components
|
||||||
import { PagesListView } from "components/pages/pages-list";
|
import { PagesListView } from "components/pages/pages-list";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePage } from "hooks/store";
|
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
|
import { useProjectPages } from "hooks/store/use-project-specific-pages";
|
||||||
|
|
||||||
export const FavoritePagesList: FC = observer(() => {
|
export const FavoritePagesList: FC = observer(() => {
|
||||||
const { favoriteProjectPageIds } = usePage();
|
const projectPageStore = useProjectPages();
|
||||||
|
const { favoriteProjectPageIds } = projectPageStore;
|
||||||
|
|
||||||
if (!favoriteProjectPageIds)
|
if (!favoriteProjectPageIds)
|
||||||
return (
|
return (
|
||||||
|
@ -13,10 +13,6 @@ import {
|
|||||||
Star,
|
Star,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
// hooks
|
|
||||||
import { useMember, usePage, useUser } from "hooks/store";
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// helpers
|
|
||||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||||
import { renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper";
|
import { renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper";
|
||||||
// ui
|
// ui
|
||||||
@ -25,142 +21,120 @@ import { CustomMenu, Tooltip } from "@plane/ui";
|
|||||||
import { CreateUpdatePageModal, DeletePageModal } from "components/pages";
|
import { CreateUpdatePageModal, DeletePageModal } from "components/pages";
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useProjectPages } from "hooks/store/use-project-specific-pages";
|
||||||
|
import { useMember, usePage, useUser } from "hooks/store";
|
||||||
|
import { IIssueLabel } from "@plane/types";
|
||||||
|
|
||||||
export interface IPagesListItem {
|
export interface IPagesListItem {
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
pageId: string;
|
pageId: string;
|
||||||
|
projectId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
export const PagesListItem: FC<IPagesListItem> = observer(({ pageId, projectId }: IPagesListItem) => {
|
||||||
const { workspaceSlug, projectId, pageId } = props;
|
const projectPageStore = useProjectPages();
|
||||||
|
// Now, I am observing only the projectPages, out of the projectPageStore.
|
||||||
|
const { archivePage, restorePage } = projectPageStore;
|
||||||
|
|
||||||
|
const pageStore = usePage(pageId);
|
||||||
|
|
||||||
// states
|
// states
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
|
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
|
||||||
|
|
||||||
const [deletePageModal, setDeletePageModal] = useState(false);
|
const [deletePageModal, setDeletePageModal] = useState(false);
|
||||||
// store hooks
|
|
||||||
const {
|
const {
|
||||||
currentUser,
|
currentUser,
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const {
|
|
||||||
getArchivedPageById,
|
|
||||||
getUnArchivedPageById,
|
|
||||||
archivePage,
|
|
||||||
removeFromFavorites,
|
|
||||||
addToFavorites,
|
|
||||||
makePrivate,
|
|
||||||
makePublic,
|
|
||||||
restorePage,
|
|
||||||
} = usePage();
|
|
||||||
const {
|
const {
|
||||||
project: { getProjectMemberDetails },
|
project: { getProjectMemberDetails },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
// toast alert
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
// derived values
|
|
||||||
const pageDetails = getUnArchivedPageById(pageId) ?? getArchivedPageById(pageId);
|
|
||||||
|
|
||||||
const handleCopyUrl = (e: any) => {
|
if (!pageStore) return null;
|
||||||
|
|
||||||
|
const {
|
||||||
|
archived_at,
|
||||||
|
label_details,
|
||||||
|
access,
|
||||||
|
is_favorite,
|
||||||
|
owned_by,
|
||||||
|
name,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
makePublic,
|
||||||
|
makePrivate,
|
||||||
|
addToFavorites,
|
||||||
|
removeFromFavorites,
|
||||||
|
} = pageStore;
|
||||||
|
|
||||||
|
const handleCopyUrl = async (e: React.MouseEvent<HTMLElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/pages/${pageId}`).then(() => {
|
await copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/pages/${pageId}`);
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Link Copied!",
|
|
||||||
message: "Page link copied to clipboard.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddToFavorites = (e: any) => {
|
const handleAddToFavorites = (e: React.MouseEvent<HTMLElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
addToFavorites();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFromFavorites = (e: React.MouseEvent<HTMLElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
addToFavorites(workspaceSlug, projectId, pageId)
|
removeFromFavorites();
|
||||||
.then(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Success!",
|
|
||||||
message: "Successfully added the page to favorites.",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Couldn't add the page to favorites. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveFromFavorites = (e: any) => {
|
const handleMakePublic = (e: React.MouseEvent<HTMLElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
removeFromFavorites(workspaceSlug, projectId, pageId)
|
makePublic();
|
||||||
.then(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Success!",
|
|
||||||
message: "Successfully removed the page from favorites.",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Couldn't remove the page from favorites. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMakePublic = (e: any) => {
|
const handleMakePrivate = (e: React.MouseEvent<HTMLElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
makePublic(workspaceSlug, projectId, pageId);
|
makePrivate();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMakePrivate = (e: any) => {
|
const handleArchivePage = async (e: React.MouseEvent<HTMLElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
makePrivate(workspaceSlug, projectId, pageId);
|
await archivePage(workspaceSlug as string, projectId as string, pageId as string);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleArchivePage = (e: any) => {
|
const handleRestorePage = async (e: React.MouseEvent<HTMLElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
archivePage(workspaceSlug, projectId, pageId);
|
await restorePage(workspaceSlug as string, projectId as string, pageId as string);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRestorePage = (e: any) => {
|
const handleDeletePage = (e: React.MouseEvent<HTMLElement>) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
restorePage(workspaceSlug, projectId, pageId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeletePage = (e: any) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
setDeletePageModal(true);
|
setDeletePageModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditPage = (e: any) => {
|
const handleEditPage = (e: React.MouseEvent<HTMLElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
setCreateUpdatePageModal(true);
|
setCreateUpdatePageModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!pageDetails) return null;
|
const ownerDetails = getProjectMemberDetails(owned_by);
|
||||||
|
const isCurrentUserOwner = owned_by === currentUser?.id;
|
||||||
const ownerDetails = getProjectMemberDetails(pageDetails.owned_by);
|
|
||||||
const isCurrentUserOwner = pageDetails.owned_by === currentUser?.id;
|
|
||||||
|
|
||||||
const userCanEdit =
|
const userCanEdit =
|
||||||
isCurrentUserOwner ||
|
isCurrentUserOwner ||
|
||||||
@ -173,22 +147,21 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateUpdatePageModal
|
<CreateUpdatePageModal
|
||||||
|
pageStore={pageStore}
|
||||||
isOpen={createUpdatePageModal}
|
isOpen={createUpdatePageModal}
|
||||||
handleClose={() => setCreateUpdatePageModal(false)}
|
handleClose={() => setCreateUpdatePageModal(false)}
|
||||||
data={pageDetails}
|
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
/>
|
/>
|
||||||
<DeletePageModal isOpen={deletePageModal} onClose={() => setDeletePageModal(false)} data={pageDetails} />
|
<DeletePageModal isOpen={deletePageModal} onClose={() => setDeletePageModal(false)} pageId={pageId} />
|
||||||
<li>
|
<li>
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/pages/${pageDetails.id}`}>
|
<Link href={`/${workspaceSlug}/projects/${projectId}/pages/${pageId}`}>
|
||||||
<div className="relative rounded p-4 text-custom-text-200 hover:bg-custom-background-80">
|
<div className="relative rounded p-4 text-custom-text-200 hover:bg-custom-background-80">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2 overflow-hidden">
|
<div className="flex items-center gap-2 overflow-hidden">
|
||||||
<FileText className="h-4 w-4 shrink-0" />
|
<FileText className="h-4 w-4 shrink-0" />
|
||||||
<p className="mr-2 truncate text-sm text-custom-text-100">{pageDetails.name}</p>
|
<p className="mr-2 truncate text-sm text-custom-text-100">{name}</p>
|
||||||
{/* FIXME: replace any with proper type */}
|
{label_details.length > 0 &&
|
||||||
{pageDetails.label_details.length > 0 &&
|
label_details.map((label: IIssueLabel) => (
|
||||||
pageDetails.label_details.map((label: any) => (
|
|
||||||
<div
|
<div
|
||||||
key={label.id}
|
key={label.id}
|
||||||
className="group flex items-center gap-1 rounded-2xl border border-custom-border-200 px-2 py-0.5 text-xs"
|
className="group flex items-center gap-1 rounded-2xl border border-custom-border-200 px-2 py-0.5 text-xs"
|
||||||
@ -207,26 +180,26 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
{pageDetails.archived_at ? (
|
{archived_at ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={`Archived at ${renderFormattedTime(
|
tooltipContent={`Archived at ${renderFormattedTime(archived_at)} on ${renderFormattedDate(
|
||||||
pageDetails.archived_at
|
archived_at
|
||||||
)} on ${renderFormattedDate(pageDetails.archived_at)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
<p className="text-sm text-custom-text-200">{renderFormattedTime(pageDetails.archived_at)}</p>
|
<p className="text-sm text-custom-text-200">{renderFormattedTime(archived_at)}</p>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={`Last updated at ${renderFormattedTime(
|
tooltipContent={`Last updated at ${renderFormattedTime(updated_at)} on ${renderFormattedDate(
|
||||||
pageDetails.updated_at
|
updated_at
|
||||||
)} on ${renderFormattedDate(pageDetails.updated_at)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
<p className="text-sm text-custom-text-200">{renderFormattedTime(pageDetails.updated_at)}</p>
|
<p className="text-sm text-custom-text-200">{renderFormattedTime(updated_at)}</p>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{isEditingAllowed && (
|
{isEditingAllowed && (
|
||||||
<Tooltip tooltipContent={`${pageDetails.is_favorite ? "Remove from favorites" : "Mark as favorite"}`}>
|
<Tooltip tooltipContent={`${is_favorite ? "Remove from favorites" : "Mark as favorite"}`}>
|
||||||
{pageDetails.is_favorite ? (
|
{is_favorite ? (
|
||||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||||
<Star className="h-3.5 w-3.5 fill-orange-400 text-orange-400" />
|
<Star className="h-3.5 w-3.5 fill-orange-400 text-orange-400" />
|
||||||
</button>
|
</button>
|
||||||
@ -240,12 +213,10 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
|||||||
{userCanChangeAccess && (
|
{userCanChangeAccess && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={`${
|
tooltipContent={`${
|
||||||
pageDetails.access
|
access ? "This page is only visible to you" : "This page can be viewed by anyone in the project"
|
||||||
? "This page is only visible to you"
|
|
||||||
: "This page can be viewed by anyone in the project"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{pageDetails.access ? (
|
{access ? (
|
||||||
<button type="button" onClick={handleMakePublic}>
|
<button type="button" onClick={handleMakePublic}>
|
||||||
<Lock className="h-3.5 w-3.5" />
|
<Lock className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@ -259,13 +230,13 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
position="top-right"
|
position="top-right"
|
||||||
tooltipContent={`Created by ${ownerDetails?.member.display_name} on ${renderFormattedDate(
|
tooltipContent={`Created by ${ownerDetails?.member.display_name} on ${renderFormattedDate(
|
||||||
pageDetails.created_at
|
created_at
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
<AlertCircle className="h-3.5 w-3.5" />
|
<AlertCircle className="h-3.5 w-3.5" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<CustomMenu width="auto" placement="bottom-end" className="!-m-1" verticalEllipsis>
|
<CustomMenu width="auto" placement="bottom-end" className="!-m-1" verticalEllipsis>
|
||||||
{pageDetails.archived_at ? (
|
{archived_at ? (
|
||||||
<>
|
<>
|
||||||
{userCanArchive && (
|
{userCanArchive && (
|
||||||
<CustomMenu.MenuItem onClick={handleRestorePage}>
|
<CustomMenu.MenuItem onClick={handleRestorePage}>
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useUser } from "hooks/store";
|
import { useApplication, useUser } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { PagesListItem } from "./list-item";
|
|
||||||
import { NewEmptyState } from "components/common/new-empty-state";
|
import { NewEmptyState } from "components/common/new-empty-state";
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
@ -13,14 +11,17 @@ import { Loader } from "@plane/ui";
|
|||||||
import emptyPage from "public/empty-state/empty_page.png";
|
import emptyPage from "public/empty-state/empty_page.png";
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { PagesListItem } from "./list-item";
|
||||||
|
|
||||||
type IPagesListView = {
|
type IPagesListView = {
|
||||||
pageIds: string[];
|
pageIds: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PagesListView: FC<IPagesListView> = observer((props) => {
|
export const PagesListView: FC<IPagesListView> = (props) => {
|
||||||
const { pageIds } = props;
|
const { pageIds: projectPageIds } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
|
// trace(true);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
commandPalette: { toggleCreatePageModal },
|
commandPalette: { toggleCreatePageModal },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
@ -31,21 +32,18 @@ export const PagesListView: FC<IPagesListView> = observer((props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
// here we are only observing the projectPageStore, so that we can re-render the component when the projectPageStore changes
|
||||||
|
|
||||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{pageIds && workspaceSlug && projectId ? (
|
{projectPageIds && workspaceSlug && projectId ? (
|
||||||
<div className="h-full space-y-4 overflow-y-auto">
|
<div className="h-full space-y-4 overflow-y-auto">
|
||||||
{pageIds.length > 0 ? (
|
{projectPageIds.length > 0 ? (
|
||||||
<ul role="list" className="divide-y divide-custom-border-200">
|
<ul role="list" className="divide-y divide-custom-border-200">
|
||||||
{pageIds.map((pageId) => (
|
{projectPageIds.map((pageId: string) => (
|
||||||
<PagesListItem
|
<PagesListItem key={pageId} pageId={pageId} projectId={projectId.toString()} />
|
||||||
key={pageId}
|
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
|
||||||
projectId={projectId.toString()}
|
|
||||||
pageId={pageId}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
@ -77,4 +75,4 @@ export const PagesListView: FC<IPagesListView> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePage } from "hooks/store";
|
|
||||||
// components
|
// components
|
||||||
import { PagesListView } from "components/pages/pages-list";
|
import { PagesListView } from "components/pages/pages-list";
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
|
import { useProjectPages } from "hooks/store/use-project-specific-pages";
|
||||||
|
|
||||||
export const PrivatePagesList: FC = observer(() => {
|
export const PrivatePagesList: FC = observer(() => {
|
||||||
const { privateProjectPageIds } = usePage();
|
const projectPageStore = useProjectPages();
|
||||||
|
const { privateProjectPageIds } = projectPageStore;
|
||||||
|
|
||||||
if (!privateProjectPageIds)
|
if (!privateProjectPageIds)
|
||||||
return (
|
return (
|
||||||
|
@ -2,7 +2,7 @@ import React, { FC } from "react";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, usePage, useUser } from "hooks/store";
|
import { useApplication, useUser } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { PagesListView } from "components/pages/pages-list";
|
import { PagesListView } from "components/pages/pages-list";
|
||||||
import { NewEmptyState } from "components/common/new-empty-state";
|
import { NewEmptyState } from "components/common/new-empty-state";
|
||||||
@ -14,6 +14,7 @@ import emptyPage from "public/empty-state/empty_page.png";
|
|||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { useProjectPages } from "hooks/store/use-project-specific-pages";
|
||||||
|
|
||||||
export const RecentPagesList: FC = observer(() => {
|
export const RecentPagesList: FC = observer(() => {
|
||||||
// store hooks
|
// store hooks
|
||||||
@ -21,7 +22,7 @@ export const RecentPagesList: FC = observer(() => {
|
|||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { recentProjectPages } = usePage();
|
const { recentProjectPages } = useProjectPages();
|
||||||
|
|
||||||
// FIXME: replace any with proper type
|
// FIXME: replace any with proper type
|
||||||
const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value: any) => value.length === 0);
|
const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value: any) => value.length === 0);
|
||||||
|
@ -3,12 +3,13 @@ import { observer } from "mobx-react-lite";
|
|||||||
// components
|
// components
|
||||||
import { PagesListView } from "components/pages/pages-list";
|
import { PagesListView } from "components/pages/pages-list";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePage } from "hooks/store";
|
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
|
import { useProjectPages } from "hooks/store/use-project-specific-pages";
|
||||||
|
|
||||||
export const SharedPagesList: FC = observer(() => {
|
export const SharedPagesList: FC = observer(() => {
|
||||||
const { publicProjectPageIds } = usePage();
|
const projectPageStore = useProjectPages();
|
||||||
|
const { publicProjectPageIds } = projectPageStore;
|
||||||
|
|
||||||
if (!publicProjectPageIds)
|
if (!publicProjectPageIds)
|
||||||
return (
|
return (
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { StoreContext } from "contexts/store-context";
|
import { StoreContext } from "contexts/store-context";
|
||||||
// types
|
|
||||||
import { IPageStore } from "store/page.store";
|
|
||||||
|
|
||||||
export const usePage = (): IPageStore => {
|
export const usePage = (pageId: string) => {
|
||||||
const context = useContext(StoreContext);
|
const context = useContext(StoreContext);
|
||||||
if (context === undefined) throw new Error("usePage must be used within StoreProvider");
|
if (context === undefined) throw new Error("usePage must be used within StoreProvider");
|
||||||
return context.page;
|
|
||||||
|
const { projectPageMap, projectArchivedPageMap } = context.projectPages;
|
||||||
|
|
||||||
|
const { projectId, workspaceSlug } = context.app.router;
|
||||||
|
if (!projectId || !workspaceSlug) throw new Error("usePage must be used within ProjectProvider");
|
||||||
|
|
||||||
|
if (projectPageMap[projectId] && projectPageMap[projectId][pageId]) {
|
||||||
|
return projectPageMap[projectId][pageId];
|
||||||
|
} else if (projectArchivedPageMap[projectId] && projectArchivedPageMap[projectId][pageId]) {
|
||||||
|
return projectArchivedPageMap[projectId][pageId];
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { StoreContext } from "contexts/store-context";
|
import { StoreContext } from "contexts/store-context";
|
||||||
|
import { IProjectPageStore } from "store/project-page.store";
|
||||||
|
|
||||||
export const useProjectPages = () => {
|
export const useProjectPages = (): IProjectPageStore => {
|
||||||
const context = useContext(StoreContext);
|
const context = useContext(StoreContext);
|
||||||
if (context === undefined) throw new Error("useProjectPublish must be used within StoreProvider");
|
if (context === undefined) throw new Error("useProjectPublish must be used within StoreProvider");
|
||||||
return context.projectPages;
|
return context.projectPages;
|
||||||
|
11
web/hooks/store/use-project-specific-pages.ts
Normal file
11
web/hooks/store/use-project-specific-pages.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
// mobx store
|
||||||
|
import { StoreContext } from "contexts/store-context";
|
||||||
|
// types
|
||||||
|
import { IProjectPageStore } from "store/project-page.store";
|
||||||
|
|
||||||
|
export const useProjectPages = (): IProjectPageStore => {
|
||||||
|
const context = useContext(StoreContext);
|
||||||
|
if (context === undefined) throw new Error("usePage must be used within StoreProvider");
|
||||||
|
return context.projectPages;
|
||||||
|
};
|
48
web/hooks/use-issue-embeds.tsx
Normal file
48
web/hooks/use-issue-embeds.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { TIssue } from "@plane/types";
|
||||||
|
import { PROJECT_ISSUES_LIST, STATES_LIST } from "constants/fetch-keys";
|
||||||
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
import { StoreContext } from "contexts/store-context";
|
||||||
|
import { autorun, toJS } from "mobx";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { IssueService } from "services/issue";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { useIssueDetail, useIssues, useMember, useProject, useProjectState } from "./store";
|
||||||
|
|
||||||
|
const issueService = new IssueService();
|
||||||
|
|
||||||
|
export const useIssueEmbeds = () => {
|
||||||
|
const workspaceSlug = useContext(StoreContext).app.router.workspaceSlug;
|
||||||
|
const projectId = useContext(StoreContext).app.router.projectId;
|
||||||
|
|
||||||
|
const { getProjectById } = useProject();
|
||||||
|
const { setPeekIssue } = useIssueDetail();
|
||||||
|
const { getStateById } = useProjectState();
|
||||||
|
const { getUserDetails } = useMember();
|
||||||
|
|
||||||
|
const { data: issuesResponse } = useSWR(
|
||||||
|
workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
|
||||||
|
workspaceSlug && projectId ? () => issueService.getIssues(workspaceSlug as string, projectId as string) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
const issues = Object.values(issuesResponse ?? {});
|
||||||
|
const issuesWithStateAndProject = issues.map((issue) => ({
|
||||||
|
...issue,
|
||||||
|
state_detail: toJS(getStateById(issue.state_id)),
|
||||||
|
project_detail: toJS(getProjectById(issue.project_id)),
|
||||||
|
assignee_details: issue.assignee_ids.map((assigneeid) => toJS(getUserDetails(assigneeid))),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const fetchIssue = async (issueId: string) => issuesWithStateAndProject.find((issue) => issue.id === issueId);
|
||||||
|
|
||||||
|
const issueWidgetClickAction = (issueId: string) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
setPeekIssue({ workspaceSlug, projectId: projectId, issueId });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
issues: issuesWithStateAndProject,
|
||||||
|
fetchIssue,
|
||||||
|
issueWidgetClickAction,
|
||||||
|
};
|
||||||
|
};
|
@ -26,8 +26,8 @@
|
|||||||
"@plane/document-editor": "*",
|
"@plane/document-editor": "*",
|
||||||
"@plane/lite-text-editor": "*",
|
"@plane/lite-text-editor": "*",
|
||||||
"@plane/rich-text-editor": "*",
|
"@plane/rich-text-editor": "*",
|
||||||
"@plane/ui": "*",
|
|
||||||
"@plane/types": "*",
|
"@plane/types": "*",
|
||||||
|
"@plane/ui": "*",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@sentry/nextjs": "^7.85.0",
|
"@sentry/nextjs": "^7.85.0",
|
||||||
"axios": "^1.1.3",
|
"axios": "^1.1.3",
|
||||||
@ -39,7 +39,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.294.0",
|
||||||
"mobx": "^6.10.0",
|
"mobx": "^6.10.0",
|
||||||
"mobx-react-lite": "^4.0.3",
|
"mobx-react": "^9.1.0",
|
||||||
"next": "^14.0.3",
|
"next": "^14.0.3",
|
||||||
"next-pwa": "^5.6.0",
|
"next-pwa": "^5.6.0",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
|
@ -1,52 +1,50 @@
|
|||||||
import React, { useEffect, useRef, useState, ReactElement, useCallback } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import useSWR, { MutatorOptions } from "swr";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { Sparkle } from "lucide-react";
|
import { Sparkle } from "lucide-react";
|
||||||
import debounce from "lodash/debounce";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { ReactElement, useEffect, useRef, useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useUser } from "hooks/store";
|
import { useApplication, useIssues, usePage, useUser } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
// services
|
// services
|
||||||
import { PageService } from "services/page.service";
|
|
||||||
import { FileService } from "services/file.service";
|
import { FileService } from "services/file.service";
|
||||||
import { IssueService } from "services/issue";
|
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import { GptAssistantPopover } from "components/core";
|
import { GptAssistantPopover } from "components/core";
|
||||||
import { PageDetailsHeader } from "components/headers/page-details";
|
import { PageDetailsHeader } from "components/headers/page-details";
|
||||||
import { EmptyState } from "components/common";
|
|
||||||
// ui
|
// ui
|
||||||
import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor";
|
import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor";
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// assets
|
// assets
|
||||||
import emptyPage from "public/empty-state/page.svg";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
|
||||||
// types
|
// types
|
||||||
|
import { IPage } from "@plane/types";
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
import { IPage, TIssue } from "@plane/types";
|
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PAGE_DETAILS, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { useProjectPages } from "hooks/store/use-project-specific-pages";
|
||||||
|
import { useIssueEmbeds } from "hooks/use-issue-embeds";
|
||||||
|
import { IssuePeekOverview } from "components/issues";
|
||||||
|
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||||
|
import { IssueService } from "services/issue";
|
||||||
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
const fileService = new FileService();
|
const fileService = new FileService();
|
||||||
const pageService = new PageService();
|
|
||||||
const issueService = new IssueService();
|
const issueService = new IssueService();
|
||||||
|
|
||||||
const PageDetailsPage: NextPageWithLayout = observer(() => {
|
const PageDetailsPage: NextPageWithLayout = observer(() => {
|
||||||
// states
|
// states
|
||||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
|
||||||
const [gptModalOpen, setGptModal] = useState(false);
|
const [gptModalOpen, setGptModal] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { workspaceSlug, projectId, pageId } = router.query;
|
const { workspaceSlug, projectId, pageId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
@ -59,18 +57,82 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
//TODO:fix reload confirmations, with mobx
|
||||||
const { setShowAlert } = useReloadConfirmations();
|
const { setShowAlert } = useReloadConfirmations();
|
||||||
|
|
||||||
const { handleSubmit, setValue, watch, getValues, control, reset } = useForm<IPage>({
|
const { handleSubmit, setValue, watch, getValues, control, reset } = useForm<IPage>({
|
||||||
defaultValues: { name: "", description_html: "" },
|
defaultValues: { name: "", description_html: "" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: issuesResponse } = useSWR(
|
const {
|
||||||
workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
|
archivePage: archivePageAction,
|
||||||
workspaceSlug && projectId ? () => issueService.getIssues(workspaceSlug as string, projectId as string) : null
|
restorePage: restorePageAction,
|
||||||
|
createPage: createPageAction,
|
||||||
|
projectPageMap,
|
||||||
|
projectArchivedPageMap,
|
||||||
|
fetchProjectPages,
|
||||||
|
fetchArchivedProjectPages,
|
||||||
|
} = useProjectPages();
|
||||||
|
|
||||||
|
useSWR(
|
||||||
|
workspaceSlug && projectId ? `ALL_PAGES_LIST_${projectId}` : null,
|
||||||
|
workspaceSlug && projectId && !projectPageMap[projectId as string] && !projectArchivedPageMap[projectId as string]
|
||||||
|
? () => fetchProjectPages(workspaceSlug.toString(), projectId.toString())
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
// fetching archived pages from API
|
||||||
|
useSWR(
|
||||||
|
workspaceSlug && projectId ? `ALL_ARCHIVED_PAGES_LIST_${projectId}` : null,
|
||||||
|
workspaceSlug && projectId && !projectArchivedPageMap[projectId as string] && !projectPageMap[projectId as string]
|
||||||
|
? () => fetchArchivedProjectPages(workspaceSlug.toString(), projectId.toString())
|
||||||
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const issues = Object.values(issuesResponse ?? {});
|
const { issues, fetchIssue, issueWidgetClickAction } = useIssueEmbeds();
|
||||||
|
|
||||||
|
const pageStore = usePage(pageId as string);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
if (pageStore) {
|
||||||
|
pageStore.cleanup();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[pageStore]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!pageStore) {
|
||||||
|
return (
|
||||||
|
<div className="grid h-full w-full place-items-center">
|
||||||
|
<Spinner />
|
||||||
|
</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
|
||||||
|
const pageTitle = pageStore?.name;
|
||||||
|
const pageDescription = pageStore?.description_html;
|
||||||
|
const {
|
||||||
|
lockPage: lockPageAction,
|
||||||
|
unlockPage: unlockPageAction,
|
||||||
|
updateName: updateNameAction,
|
||||||
|
updateDescription: updateDescriptionAction,
|
||||||
|
id: pageIdMobx,
|
||||||
|
isSubmitting,
|
||||||
|
setIsSubmitting,
|
||||||
|
owned_by,
|
||||||
|
is_locked,
|
||||||
|
archived_at,
|
||||||
|
created_at,
|
||||||
|
created_by,
|
||||||
|
updated_at,
|
||||||
|
updated_by,
|
||||||
|
} = pageStore;
|
||||||
|
|
||||||
|
const updatePage = async (formData: IPage) => {
|
||||||
|
if (!workspaceSlug || !projectId || !pageId) return;
|
||||||
|
await updateDescriptionAction(formData.description_html);
|
||||||
|
};
|
||||||
|
|
||||||
const handleAiAssistance = async (response: string) => {
|
const handleAiAssistance = async (response: string) => {
|
||||||
if (!workspaceSlug || !projectId || !pageId) return;
|
if (!workspaceSlug || !projectId || !pageId) return;
|
||||||
@ -78,47 +140,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
const newDescription = `${watch("description_html")}<p>${response}</p>`;
|
const newDescription = `${watch("description_html")}<p>${response}</p>`;
|
||||||
setValue("description_html", newDescription);
|
setValue("description_html", newDescription);
|
||||||
editorRef.current?.setEditorValue(newDescription);
|
editorRef.current?.setEditorValue(newDescription);
|
||||||
|
updateDescriptionAction(newDescription);
|
||||||
pageService
|
|
||||||
.patchPage(workspaceSlug.toString(), projectId.toString(), pageId.toString(), {
|
|
||||||
description_html: newDescription,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
mutatePageDetails((prevData) => ({ ...prevData, description_html: newDescription } as IPage), false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// =================== Fetching Page Details ======================
|
|
||||||
const {
|
|
||||||
data: pageDetails,
|
|
||||||
mutate: mutatePageDetails,
|
|
||||||
error,
|
|
||||||
} = useSWR(
|
|
||||||
workspaceSlug && projectId && pageId ? PAGE_DETAILS(pageId.toString()) : null,
|
|
||||||
workspaceSlug && projectId && pageId
|
|
||||||
? () => pageService.getPageDetails(workspaceSlug.toString(), projectId.toString(), pageId.toString())
|
|
||||||
: null,
|
|
||||||
{
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchIssue = async (issueId: string) => {
|
|
||||||
const issue = await issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string);
|
|
||||||
return issue as TIssue;
|
|
||||||
};
|
|
||||||
|
|
||||||
const issueWidgetClickAction = (issueId: string) => {
|
|
||||||
const url = new URL(router.asPath, window.location.origin);
|
|
||||||
const params = new URLSearchParams(url.search);
|
|
||||||
|
|
||||||
if (params.has("peekIssueId")) {
|
|
||||||
params.set("peekIssueId", issueId);
|
|
||||||
} else {
|
|
||||||
params.append("peekIssueId", issueId);
|
|
||||||
}
|
|
||||||
// Replace the current URL with the new one
|
|
||||||
router.replace(`${url.pathname}?${params.toString()}`, undefined, { shallow: true });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionCompleteAlert = ({
|
const actionCompleteAlert = ({
|
||||||
@ -137,122 +159,14 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const updatePageTitle = (title: string) => {
|
||||||
if (isSubmitting === "submitted") {
|
|
||||||
setShowAlert(false);
|
|
||||||
setTimeout(async () => {
|
|
||||||
setIsSubmitting("saved");
|
|
||||||
}, 2000);
|
|
||||||
} else if (isSubmitting === "submitting") {
|
|
||||||
setShowAlert(true);
|
|
||||||
}
|
|
||||||
}, [isSubmitting, setShowAlert]);
|
|
||||||
|
|
||||||
// adding pageDetails.description_html to dependency array causes
|
|
||||||
// editor rerendering on every save
|
|
||||||
useEffect(() => {
|
|
||||||
if (pageDetails?.description_html) {
|
|
||||||
setLocalIssueDescription({ id: pageId as string, description_html: pageDetails.description_html });
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [pageDetails?.description_html]); // TODO: Verify the exhaustive-deps warning
|
|
||||||
|
|
||||||
function createObjectFromArray(keys: string[], options: any): any {
|
|
||||||
return keys.reduce((obj, key) => {
|
|
||||||
if (options[key] !== undefined) {
|
|
||||||
obj[key] = options[key];
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}, {} as { [key: string]: any });
|
|
||||||
}
|
|
||||||
|
|
||||||
const mutatePageDetailsHelper = (
|
|
||||||
serverMutatorFn: Promise<any>,
|
|
||||||
dataToMutate: Partial<IPage>,
|
|
||||||
formDataValues: Array<keyof IPage>,
|
|
||||||
onErrorAction: () => void
|
|
||||||
) => {
|
|
||||||
const commonSwrOptions: MutatorOptions = {
|
|
||||||
revalidate: false,
|
|
||||||
populateCache: false,
|
|
||||||
rollbackOnError: () => {
|
|
||||||
onErrorAction();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const formData = getValues();
|
|
||||||
const formDataMutationObject = createObjectFromArray(formDataValues, formData);
|
|
||||||
|
|
||||||
mutatePageDetails(async () => serverMutatorFn, {
|
|
||||||
optimisticData: (prevData) => {
|
|
||||||
if (!prevData) return;
|
|
||||||
return {
|
|
||||||
...prevData,
|
|
||||||
description_html: formData["description_html"],
|
|
||||||
...formDataMutationObject,
|
|
||||||
...dataToMutate,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
...commonSwrOptions,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
mutatePageDetails(undefined, {
|
|
||||||
revalidate: true,
|
|
||||||
populateCache: true,
|
|
||||||
rollbackOnError: () => {
|
|
||||||
actionCompleteAlert({
|
|
||||||
title: `Page could not be updated`,
|
|
||||||
message: `Sorry, page could not be updated, please try again later`,
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updatePage = async (formData: IPage) => {
|
|
||||||
if (!workspaceSlug || !projectId || !pageId) return;
|
if (!workspaceSlug || !projectId || !pageId) return;
|
||||||
|
updateNameAction(title);
|
||||||
formData.name = pageDetails?.name as string;
|
|
||||||
|
|
||||||
if (!formData?.name || formData?.name.length === 0) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await pageService.patchPage(workspaceSlug.toString(), projectId.toString(), pageId.toString(), formData);
|
|
||||||
} catch (error) {
|
|
||||||
actionCompleteAlert({
|
|
||||||
title: `Page could not be updated`,
|
|
||||||
message: `Sorry, page could not be updated, please try again later`,
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatePageTitle = async (title: string) => {
|
|
||||||
if (!workspaceSlug || !projectId || !pageId) return;
|
|
||||||
|
|
||||||
mutatePageDetailsHelper(
|
|
||||||
pageService.patchPage(workspaceSlug.toString(), projectId.toString(), pageId.toString(), { name: title }),
|
|
||||||
{
|
|
||||||
name: title,
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
() =>
|
|
||||||
actionCompleteAlert({
|
|
||||||
title: `Page Title could not be updated`,
|
|
||||||
message: `Sorry, page title could not be updated, please try again later`,
|
|
||||||
type: "error",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPage = async (payload: Partial<IPage>) => {
|
const createPage = async (payload: Partial<IPage>) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
await createPageAction(workspaceSlug as string, projectId as string, payload);
|
||||||
await pageService.createPage(workspaceSlug.toString(), projectId.toString(), payload);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ================ Page Menu Actions ==================
|
// ================ Page Menu Actions ==================
|
||||||
@ -260,121 +174,84 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
const currentPageValues = getValues();
|
const currentPageValues = getValues();
|
||||||
|
|
||||||
if (!currentPageValues?.description_html) {
|
if (!currentPageValues?.description_html) {
|
||||||
currentPageValues.description_html = pageDetails?.description_html as string;
|
// TODO: We need to get latest data the above variable will give us stale data
|
||||||
|
currentPageValues.description_html = pageDescription as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData: Partial<IPage> = {
|
const formData: Partial<IPage> = {
|
||||||
name: "Copy of " + pageDetails?.name,
|
name: "Copy of " + pageTitle,
|
||||||
description_html: currentPageValues.description_html,
|
description_html: currentPageValues.description_html,
|
||||||
};
|
};
|
||||||
await createPage(formData);
|
|
||||||
|
try {
|
||||||
|
await createPage(formData);
|
||||||
|
} catch (error) {
|
||||||
|
actionCompleteAlert({
|
||||||
|
title: `Page could not be duplicated`,
|
||||||
|
message: `Sorry, page could not be duplicated, please try again later`,
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const archivePage = async () => {
|
const archivePage = async () => {
|
||||||
if (!workspaceSlug || !projectId || !pageId) return;
|
if (!workspaceSlug || !projectId || !pageId) return;
|
||||||
mutatePageDetailsHelper(
|
try {
|
||||||
pageService.archivePage(workspaceSlug.toString(), projectId.toString(), pageId.toString()),
|
await archivePageAction(workspaceSlug as string, projectId as string, pageId as string);
|
||||||
{
|
} catch (error) {
|
||||||
archived_at: renderFormattedPayloadDate(new Date()),
|
actionCompleteAlert({
|
||||||
},
|
title: `Page could not be archived`,
|
||||||
["description_html"],
|
message: `Sorry, page could not be archived, please try again later`,
|
||||||
() =>
|
type: "error",
|
||||||
actionCompleteAlert({
|
});
|
||||||
title: `Page could not be Archived`,
|
}
|
||||||
message: `Sorry, page could not be Archived, please try again later`,
|
|
||||||
type: "error",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const unArchivePage = async () => {
|
const unArchivePage = async () => {
|
||||||
if (!workspaceSlug || !projectId || !pageId) return;
|
if (!workspaceSlug || !projectId || !pageId) return;
|
||||||
|
try {
|
||||||
mutatePageDetailsHelper(
|
await restorePageAction(workspaceSlug as string, projectId as string, pageId as string);
|
||||||
pageService.restorePage(workspaceSlug.toString(), projectId.toString(), pageId.toString()),
|
} catch (error) {
|
||||||
{
|
actionCompleteAlert({
|
||||||
archived_at: null,
|
title: `Page could not be restored`,
|
||||||
},
|
message: `Sorry, page could not be restored, please try again later`,
|
||||||
["description_html"],
|
type: "error",
|
||||||
() =>
|
});
|
||||||
actionCompleteAlert({
|
}
|
||||||
title: `Page could not be Restored`,
|
|
||||||
message: `Sorry, page could not be Restored, please try again later`,
|
|
||||||
type: "error",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========================= Page Lock ==========================
|
|
||||||
const lockPage = async () => {
|
const lockPage = async () => {
|
||||||
if (!workspaceSlug || !projectId || !pageId) return;
|
if (!workspaceSlug || !projectId || !pageId) return;
|
||||||
mutatePageDetailsHelper(
|
try {
|
||||||
pageService.lockPage(workspaceSlug.toString(), projectId.toString(), pageId.toString()),
|
await lockPageAction();
|
||||||
{
|
} catch (error) {
|
||||||
is_locked: true,
|
actionCompleteAlert({
|
||||||
},
|
title: `Page could not be locked`,
|
||||||
["description_html"],
|
message: `Sorry, page could not be locked, please try again later`,
|
||||||
() =>
|
type: "error",
|
||||||
actionCompleteAlert({
|
});
|
||||||
title: `Page cannot be Locked`,
|
}
|
||||||
message: `Sorry, page cannot be Locked, please try again later`,
|
|
||||||
type: "error",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const unlockPage = async () => {
|
const unlockPage = async () => {
|
||||||
if (!workspaceSlug || !projectId || !pageId) return;
|
if (!workspaceSlug || !projectId || !pageId) return;
|
||||||
|
try {
|
||||||
mutatePageDetailsHelper(
|
await unlockPageAction();
|
||||||
pageService.unlockPage(workspaceSlug.toString(), projectId.toString(), pageId.toString()),
|
} catch (error) {
|
||||||
{
|
actionCompleteAlert({
|
||||||
is_locked: false,
|
title: `Page could not be unlocked`,
|
||||||
},
|
message: `Sorry, page could not be unlocked, please try again later`,
|
||||||
["description_html"],
|
type: "error",
|
||||||
() =>
|
});
|
||||||
actionCompleteAlert({
|
}
|
||||||
title: `Page could not be Unlocked`,
|
|
||||||
message: `Sorry, page could not be Unlocked, please try again later`,
|
|
||||||
type: "error",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [localPageDescription, setLocalIssueDescription] = useState({
|
|
||||||
id: pageId as string,
|
|
||||||
description_html: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
// ADDING updatePage TO DEPENDENCY ARRAY PRODUCES ADVERSE EFFECTS
|
|
||||||
// TODO: Verify the exhaustive-deps warning
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
const debouncedFormSave = useCallback(
|
|
||||||
debounce(async () => {
|
|
||||||
handleSubmit(updatePage)().finally(() => setIsSubmitting("submitted"));
|
|
||||||
}, 1500),
|
|
||||||
[handleSubmit, pageDetails]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error)
|
|
||||||
return (
|
|
||||||
<EmptyState
|
|
||||||
image={emptyPage}
|
|
||||||
title="Page does not exist"
|
|
||||||
description="The page you are looking for does not exist or has been deleted."
|
|
||||||
primaryButton={{
|
|
||||||
text: "View other pages",
|
|
||||||
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/pages`),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const isPageReadOnly =
|
const isPageReadOnly =
|
||||||
pageDetails?.is_locked ||
|
is_locked ||
|
||||||
pageDetails?.archived_at ||
|
archived_at ||
|
||||||
(currentProjectRole && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(currentProjectRole));
|
(currentProjectRole && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(currentProjectRole));
|
||||||
|
|
||||||
const isCurrentUserOwner = pageDetails?.owned_by === currentUser?.id;
|
const isCurrentUserOwner = owned_by === currentUser?.id;
|
||||||
|
|
||||||
const userCanDuplicate =
|
const userCanDuplicate =
|
||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
@ -382,144 +259,132 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
const userCanLock =
|
const userCanLock =
|
||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
return (
|
return pageIdMobx && issues ? (
|
||||||
<>
|
<div className="flex h-full flex-col justify-between">
|
||||||
{pageDetails && issuesResponse ? (
|
<div className="h-full w-full overflow-hidden">
|
||||||
<div className="flex h-full flex-col justify-between">
|
{isPageReadOnly ? (
|
||||||
<div className="h-full w-full overflow-hidden">
|
<DocumentReadOnlyEditorWithRef
|
||||||
{isPageReadOnly ? (
|
onActionCompleteHandler={actionCompleteAlert}
|
||||||
<DocumentReadOnlyEditorWithRef
|
ref={editorRef}
|
||||||
onActionCompleteHandler={actionCompleteAlert}
|
value={pageDescription}
|
||||||
ref={editorRef}
|
customClassName={"tracking-tight w-full px-0"}
|
||||||
value={localPageDescription.description_html}
|
borderOnFocus={false}
|
||||||
rerenderOnPropsChange={localPageDescription}
|
noBorder
|
||||||
customClassName={"tracking-tight w-full px-0"}
|
documentDetails={{
|
||||||
borderOnFocus={false}
|
title: pageTitle,
|
||||||
noBorder
|
created_by: created_by,
|
||||||
documentDetails={{
|
created_on: created_at,
|
||||||
title: pageDetails.name,
|
last_updated_at: updated_at,
|
||||||
created_by: pageDetails.created_by,
|
last_updated_by: updated_by,
|
||||||
created_on: pageDetails.created_at,
|
}}
|
||||||
last_updated_at: pageDetails.updated_at,
|
pageLockConfig={userCanLock && !archived_at ? { action: unlockPage, is_locked: is_locked } : undefined}
|
||||||
last_updated_by: pageDetails.updated_by,
|
pageDuplicationConfig={userCanDuplicate && !archived_at ? { action: duplicate_page } : undefined}
|
||||||
}}
|
pageArchiveConfig={
|
||||||
pageLockConfig={
|
userCanArchive
|
||||||
userCanLock && !pageDetails.archived_at
|
? {
|
||||||
? { action: unlockPage, is_locked: pageDetails.is_locked }
|
action: archived_at ? unArchivePage : archivePage,
|
||||||
: undefined
|
is_archived: archived_at ? true : false,
|
||||||
}
|
archived_at: archived_at ? new Date(archived_at) : undefined,
|
||||||
pageDuplicationConfig={
|
}
|
||||||
userCanDuplicate && !pageDetails.archived_at ? { action: duplicate_page } : undefined
|
: undefined
|
||||||
}
|
}
|
||||||
pageArchiveConfig={
|
embedConfig={{
|
||||||
userCanArchive
|
issueEmbedConfig: {
|
||||||
? {
|
issues: issues,
|
||||||
action: pageDetails.archived_at ? unArchivePage : archivePage,
|
fetchIssue: fetchIssue,
|
||||||
is_archived: pageDetails.archived_at ? true : false,
|
clickAction: issueWidgetClickAction,
|
||||||
archived_at: pageDetails.archived_at ? new Date(pageDetails.archived_at) : undefined,
|
},
|
||||||
}
|
}}
|
||||||
: undefined
|
/>
|
||||||
}
|
) : (
|
||||||
embedConfig={{
|
<div className="relative h-full w-full overflow-hidden">
|
||||||
issueEmbedConfig: {
|
<Controller
|
||||||
issues: issues,
|
name="description_html"
|
||||||
fetchIssue: fetchIssue,
|
control={control}
|
||||||
clickAction: issueWidgetClickAction,
|
render={({ field: { onChange } }) => (
|
||||||
},
|
<DocumentEditorWithRef
|
||||||
}}
|
isSubmitting={isSubmitting}
|
||||||
/>
|
documentDetails={{
|
||||||
) : (
|
title: pageTitle,
|
||||||
<div className="relative h-full w-full overflow-hidden">
|
created_by: created_by,
|
||||||
<Controller
|
created_on: created_at,
|
||||||
name="description_html"
|
last_updated_at: updated_at,
|
||||||
control={control}
|
last_updated_by: updated_by,
|
||||||
render={({ field: { onChange } }) => (
|
}}
|
||||||
<DocumentEditorWithRef
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
isSubmitting={isSubmitting}
|
value={pageDescription}
|
||||||
documentDetails={{
|
setShouldShowAlert={setShowAlert}
|
||||||
title: pageDetails.name,
|
deleteFile={fileService.deleteImage}
|
||||||
created_by: pageDetails.created_by,
|
restoreFile={fileService.restoreImage}
|
||||||
created_on: pageDetails.created_at,
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
last_updated_at: pageDetails.updated_at,
|
ref={editorRef}
|
||||||
last_updated_by: pageDetails.updated_by,
|
debouncedUpdatesEnabled={false}
|
||||||
}}
|
setIsSubmitting={setIsSubmitting}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
updatePageTitle={updatePageTitle}
|
||||||
setShouldShowAlert={setShowAlert}
|
onActionCompleteHandler={actionCompleteAlert}
|
||||||
deleteFile={fileService.deleteImage}
|
customClassName="tracking-tight self-center px-0 h-full w-full"
|
||||||
restoreFile={fileService.restoreImage}
|
onChange={(_description_json: Object, description_html: string) => {
|
||||||
cancelUploadImage={fileService.cancelUpload}
|
setShowAlert(true);
|
||||||
ref={editorRef}
|
onChange(description_html);
|
||||||
debouncedUpdatesEnabled={false}
|
handleSubmit(updatePage)();
|
||||||
setIsSubmitting={setIsSubmitting}
|
}}
|
||||||
updatePageTitle={updatePageTitle}
|
duplicationConfig={userCanDuplicate ? { action: duplicate_page } : undefined}
|
||||||
value={localPageDescription.description_html}
|
pageArchiveConfig={
|
||||||
rerenderOnPropsChange={localPageDescription}
|
userCanArchive
|
||||||
onActionCompleteHandler={actionCompleteAlert}
|
? {
|
||||||
customClassName="tracking-tight self-center px-0 h-full w-full"
|
is_archived: archived_at ? true : false,
|
||||||
onChange={(_description_json: Object, description_html: string) => {
|
action: archived_at ? unArchivePage : archivePage,
|
||||||
setShowAlert(true);
|
}
|
||||||
onChange(description_html);
|
: undefined
|
||||||
setIsSubmitting("submitting");
|
}
|
||||||
debouncedFormSave();
|
pageLockConfig={userCanLock ? { is_locked: false, action: lockPage } : undefined}
|
||||||
}}
|
embedConfig={{
|
||||||
duplicationConfig={userCanDuplicate ? { action: duplicate_page } : undefined}
|
issueEmbedConfig: {
|
||||||
pageArchiveConfig={
|
issues: issues,
|
||||||
userCanArchive
|
fetchIssue: fetchIssue,
|
||||||
? {
|
clickAction: issueWidgetClickAction,
|
||||||
is_archived: pageDetails.archived_at ? true : false,
|
},
|
||||||
action: pageDetails.archived_at ? unArchivePage : archivePage,
|
}}
|
||||||
}
|
/>
|
||||||
: undefined
|
)}
|
||||||
}
|
/>
|
||||||
pageLockConfig={userCanLock ? { is_locked: false, action: lockPage } : undefined}
|
{projectId && envConfig?.has_openai_configured && (
|
||||||
embedConfig={{
|
<div className="absolute right-[68px] top-2.5">
|
||||||
issueEmbedConfig: {
|
<GptAssistantPopover
|
||||||
issues: issues,
|
isOpen={gptModalOpen}
|
||||||
fetchIssue: fetchIssue,
|
projectId={projectId.toString()}
|
||||||
clickAction: issueWidgetClickAction,
|
handleClose={() => {
|
||||||
},
|
setGptModal((prevData) => !prevData);
|
||||||
}}
|
// this is done so that the title do not reset after gpt popover closed
|
||||||
/>
|
reset(getValues());
|
||||||
)}
|
}}
|
||||||
|
onResponse={(response) => {
|
||||||
|
handleAiAssistance(response);
|
||||||
|
}}
|
||||||
|
placement="top-end"
|
||||||
|
button={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
|
||||||
|
onClick={() => setGptModal((prevData) => !prevData)}
|
||||||
|
>
|
||||||
|
<Sparkle className="h-4 w-4" />
|
||||||
|
AI
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
className="!min-w-[38rem]"
|
||||||
/>
|
/>
|
||||||
{projectId && envConfig?.has_openai_configured && (
|
|
||||||
<div className="absolute right-[68px] top-2.5">
|
|
||||||
<GptAssistantPopover
|
|
||||||
isOpen={gptModalOpen}
|
|
||||||
projectId={projectId.toString()}
|
|
||||||
handleClose={() => {
|
|
||||||
setGptModal((prevData) => !prevData);
|
|
||||||
// this is done so that the title do not reset after gpt popover closed
|
|
||||||
reset(getValues());
|
|
||||||
}}
|
|
||||||
onResponse={(response) => {
|
|
||||||
handleAiAssistance(response);
|
|
||||||
}}
|
|
||||||
placement="top-end"
|
|
||||||
button={
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
|
|
||||||
onClick={() => setGptModal((prevData) => !prevData)}
|
|
||||||
>
|
|
||||||
<Sparkle className="h-4 w-4" />
|
|
||||||
AI
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
className="!min-w-[38rem]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
) : (
|
<IssuePeekOverview />
|
||||||
<div className="grid h-full w-full place-items-center">
|
</div>
|
||||||
<Spinner />
|
</div>
|
||||||
</div>
|
) : (
|
||||||
)}
|
<div className="grid h-full w-full place-items-center">
|
||||||
</>
|
<Spinner />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { Tab } from "@headlessui/react";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePage, useUser } from "hooks/store";
|
import { useUser } from "hooks/store";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
// layouts
|
// layouts
|
||||||
@ -17,6 +17,7 @@ import { PagesHeader } from "components/headers";
|
|||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
// constants
|
// constants
|
||||||
import { PAGE_TABS_LIST } from "constants/page";
|
import { PAGE_TABS_LIST } from "constants/page";
|
||||||
|
import { useProjectPages } from "hooks/store/use-project-page";
|
||||||
|
|
||||||
const AllPagesList = dynamic<any>(() => import("components/pages").then((a) => a.AllPagesList), {
|
const AllPagesList = dynamic<any>(() => import("components/pages").then((a) => a.AllPagesList), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@ -45,8 +46,9 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
|||||||
// states
|
// states
|
||||||
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
|
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
|
||||||
// store
|
// store
|
||||||
const { fetchProjectPages, fetchArchivedProjectPages } = usePage();
|
|
||||||
const { currentUser, currentUserLoader } = useUser();
|
const { currentUser, currentUserLoader } = useUser();
|
||||||
|
|
||||||
|
const { fetchProjectPages, fetchArchivedProjectPages } = useProjectPages();
|
||||||
// hooks
|
// hooks
|
||||||
const {} = useUserAuth({ user: currentUser, isLoading: currentUserLoader });
|
const {} = useUserAuth({ user: currentUser, isLoading: currentUserLoader });
|
||||||
// local storage
|
// local storage
|
||||||
|
@ -1,374 +1,277 @@
|
|||||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
import { action, makeObservable, observable, reaction, runInAction } from "mobx";
|
||||||
import set from "lodash/set";
|
|
||||||
import omit from "lodash/omit";
|
import { IIssueLabel, IPage } from "@plane/types";
|
||||||
import isToday from "date-fns/isToday";
|
|
||||||
import isThisWeek from "date-fns/isThisWeek";
|
|
||||||
import isYesterday from "date-fns/isYesterday";
|
|
||||||
// services
|
|
||||||
import { PageService } from "services/page.service";
|
import { PageService } from "services/page.service";
|
||||||
// helpers
|
|
||||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
|
||||||
// types
|
|
||||||
import { IPage, IRecentPages } from "@plane/types";
|
|
||||||
// store
|
|
||||||
import { RootStore } from "./root.store";
|
import { RootStore } from "./root.store";
|
||||||
|
|
||||||
export interface IPageStore {
|
export interface IPageStore {
|
||||||
pages: Record<string, IPage>;
|
// Page Properties
|
||||||
archivedPages: Record<string, IPage>;
|
access: number;
|
||||||
// project computed
|
archived_at: string | null;
|
||||||
projectPageIds: string[] | null;
|
color: string;
|
||||||
favoriteProjectPageIds: string[] | null;
|
created_at: Date;
|
||||||
privateProjectPageIds: string[] | null;
|
created_by: string;
|
||||||
publicProjectPageIds: string[] | null;
|
description: string;
|
||||||
archivedProjectPageIds: string[] | null;
|
description_html: string;
|
||||||
recentProjectPages: IRecentPages | null;
|
description_stripped: string | null;
|
||||||
// fetch page information actions
|
id: string;
|
||||||
getUnArchivedPageById: (pageId: string) => IPage | null;
|
is_favorite: boolean;
|
||||||
getArchivedPageById: (pageId: string) => IPage | null;
|
label_details: IIssueLabel[];
|
||||||
// fetch actions
|
is_locked: boolean;
|
||||||
fetchProjectPages: (workspaceSlug: string, projectId: string) => Promise<IPage[]>;
|
labels: string[];
|
||||||
fetchArchivedProjectPages: (workspaceSlug: string, projectId: string) => Promise<IPage[]>;
|
name: string;
|
||||||
// favorites actions
|
owned_by: string;
|
||||||
addToFavorites: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
project: string;
|
||||||
removeFromFavorites: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
updated_at: Date;
|
||||||
// crud
|
updated_by: string;
|
||||||
createPage: (workspaceSlug: string, projectId: string, data: Partial<IPage>) => Promise<IPage>;
|
workspace: string;
|
||||||
updatePage: (workspaceSlug: string, projectId: string, pageId: string, data: Partial<IPage>) => Promise<IPage>;
|
|
||||||
deletePage: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
// Actions
|
||||||
// access control actions
|
makePublic: () => Promise<void>;
|
||||||
makePublic: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
makePrivate: () => Promise<void>;
|
||||||
makePrivate: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
lockPage: () => Promise<void>;
|
||||||
// archive actions
|
unlockPage: () => Promise<void>;
|
||||||
archivePage: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
addToFavorites: () => Promise<void>;
|
||||||
restorePage: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
removeFromFavorites: () => Promise<void>;
|
||||||
|
updateName: (name: string) => Promise<void>;
|
||||||
|
updateDescription: (description: string) => Promise<void>;
|
||||||
|
|
||||||
|
// Reactions
|
||||||
|
disposers: Array<() => void>;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
oldName: string;
|
||||||
|
cleanup: () => void;
|
||||||
|
isSubmitting: "submitting" | "submitted" | "saved";
|
||||||
|
setIsSubmitting: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PageStore implements IPageStore {
|
export class PageStore implements IPageStore {
|
||||||
pages: Record<string, IPage> = {};
|
access = 0;
|
||||||
archivedPages: Record<string, IPage> = {};
|
isSubmitting: "submitting" | "submitted" | "saved" = "saved";
|
||||||
// services
|
archived_at: string | null;
|
||||||
|
color: string;
|
||||||
|
created_at: Date;
|
||||||
|
created_by: string;
|
||||||
|
description: string;
|
||||||
|
description_html = "";
|
||||||
|
description_stripped: string | null;
|
||||||
|
id: string;
|
||||||
|
is_favorite = false;
|
||||||
|
is_locked = true;
|
||||||
|
labels: string[];
|
||||||
|
name = "";
|
||||||
|
owned_by: string;
|
||||||
|
project: string;
|
||||||
|
updated_at: Date;
|
||||||
|
updated_by: string;
|
||||||
|
workspace: string;
|
||||||
|
oldName = "";
|
||||||
|
label_details: IIssueLabel[] = [];
|
||||||
|
disposers: Array<() => void> = [];
|
||||||
|
|
||||||
pageService;
|
pageService;
|
||||||
// stores
|
// root store
|
||||||
rootStore;
|
rootStore;
|
||||||
|
|
||||||
constructor(rootStore: RootStore) {
|
constructor(page: IPage, _rootStore: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
pages: observable,
|
name: observable.ref,
|
||||||
archivedPages: observable,
|
description_html: observable.ref,
|
||||||
// computed
|
is_favorite: observable.ref,
|
||||||
projectPageIds: computed,
|
is_locked: observable.ref,
|
||||||
favoriteProjectPageIds: computed,
|
isSubmitting: observable.ref,
|
||||||
publicProjectPageIds: computed,
|
access: observable.ref,
|
||||||
privateProjectPageIds: computed,
|
|
||||||
archivedProjectPageIds: computed,
|
|
||||||
recentProjectPages: computed,
|
|
||||||
// computed actions
|
|
||||||
getUnArchivedPageById: action,
|
|
||||||
getArchivedPageById: action,
|
|
||||||
// fetch actions
|
|
||||||
fetchProjectPages: action,
|
|
||||||
fetchArchivedProjectPages: action,
|
|
||||||
// favorites actions
|
|
||||||
addToFavorites: action,
|
|
||||||
removeFromFavorites: action,
|
|
||||||
// crud
|
|
||||||
createPage: action,
|
|
||||||
updatePage: action,
|
|
||||||
deletePage: action,
|
|
||||||
// access control actions
|
|
||||||
makePublic: action,
|
makePublic: action,
|
||||||
makePrivate: action,
|
makePrivate: action,
|
||||||
// archive actions
|
addToFavorites: action,
|
||||||
archivePage: action,
|
removeFromFavorites: action,
|
||||||
restorePage: action,
|
updateName: action,
|
||||||
|
updateDescription: action,
|
||||||
|
setIsSubmitting: action,
|
||||||
|
cleanup: action,
|
||||||
});
|
});
|
||||||
// stores
|
this.created_by = page?.created_by || "";
|
||||||
this.rootStore = rootStore;
|
this.created_at = page?.created_at || new Date();
|
||||||
// services
|
this.color = page?.color || "";
|
||||||
|
this.archived_at = page?.archived_at || null;
|
||||||
|
this.name = page?.name || "";
|
||||||
|
this.description = page?.description || "";
|
||||||
|
this.description_stripped = page?.description_stripped || "";
|
||||||
|
this.description_html = page?.description_html || "";
|
||||||
|
this.access = page?.access || 0;
|
||||||
|
this.workspace = page?.workspace || "";
|
||||||
|
this.updated_by = page?.updated_by || "";
|
||||||
|
this.updated_at = page?.updated_at || new Date();
|
||||||
|
this.project = page?.project || "";
|
||||||
|
this.owned_by = page?.owned_by || "";
|
||||||
|
this.labels = page?.labels || [];
|
||||||
|
this.label_details = page?.label_details || [];
|
||||||
|
this.is_locked = page?.is_locked || false;
|
||||||
|
this.id = page?.id || "";
|
||||||
|
this.is_favorite = page?.is_favorite || false;
|
||||||
|
this.oldName = page?.name || "";
|
||||||
|
|
||||||
|
this.rootStore = _rootStore;
|
||||||
this.pageService = new PageService();
|
this.pageService = new PageService();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const descriptionDisposer = reaction(
|
||||||
* retrieves all pages for a projectId that is available in the url.
|
() => this.description_html,
|
||||||
*/
|
(description_html) => {
|
||||||
get projectPageIds() {
|
//TODO: Fix reaction to only run when the data is changed, not when the page is loaded
|
||||||
const projectId = this.rootStore.app.router.projectId;
|
const { projectId, workspaceSlug } = this.rootStore.app.router;
|
||||||
if (!projectId) return null;
|
if (!projectId || !workspaceSlug) return;
|
||||||
const projectPageIds = Object.keys(this.pages).filter((pageId) => this.pages?.[pageId]?.project === projectId);
|
this.isSubmitting = "submitting";
|
||||||
return projectPageIds ?? null;
|
this.pageService.patchPage(workspaceSlug, projectId, this.id, { description_html }).finally(() => {
|
||||||
}
|
runInAction(() => {
|
||||||
|
this.isSubmitting = "submitted";
|
||||||
/**
|
|
||||||
* retrieves all favorite pages for a projectId that is available in the url.
|
|
||||||
*/
|
|
||||||
get favoriteProjectPageIds() {
|
|
||||||
if (!this.projectPageIds) return null;
|
|
||||||
const favoritePagesIds = Object.keys(this.projectPageIds).filter((pageId) => this.pages?.[pageId]?.is_favorite);
|
|
||||||
return favoritePagesIds ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* retrieves all private pages for a projectId that is available in the url.
|
|
||||||
*/
|
|
||||||
get privateProjectPageIds() {
|
|
||||||
if (!this.projectPageIds) return null;
|
|
||||||
const privatePagesIds = Object.keys(this.projectPageIds).filter((pageId) => this.pages?.[pageId]?.access === 1);
|
|
||||||
return privatePagesIds ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* retrieves all shared pages which are public to everyone in the project for a projectId that is available in the url.
|
|
||||||
*/
|
|
||||||
get publicProjectPageIds() {
|
|
||||||
if (!this.projectPageIds) return null;
|
|
||||||
const publicPagesIds = Object.keys(this.projectPageIds).filter((pageId) => this.pages?.[pageId]?.access === 0);
|
|
||||||
return publicPagesIds ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* retrieves all recent pages for a projectId that is available in the url.
|
|
||||||
* In format where today, yesterday, this_week, older are keys.
|
|
||||||
*/
|
|
||||||
get recentProjectPages() {
|
|
||||||
if (!this.projectPageIds) return null;
|
|
||||||
const data: IRecentPages = { today: [], yesterday: [], this_week: [], older: [] };
|
|
||||||
data.today = this.projectPageIds.filter((p) => isToday(new Date(this.pages?.[p]?.updated_at))) || [];
|
|
||||||
data.yesterday = this.projectPageIds.filter((p) => isYesterday(new Date(this.pages?.[p]?.updated_at))) || [];
|
|
||||||
data.this_week =
|
|
||||||
this.projectPageIds.filter((p) => {
|
|
||||||
const pageUpdatedAt = this.pages?.[p]?.updated_at;
|
|
||||||
return (
|
|
||||||
isThisWeek(new Date(pageUpdatedAt)) &&
|
|
||||||
!isToday(new Date(pageUpdatedAt)) &&
|
|
||||||
!isYesterday(new Date(pageUpdatedAt))
|
|
||||||
);
|
|
||||||
}) || [];
|
|
||||||
data.older =
|
|
||||||
this.projectPageIds.filter((p) => {
|
|
||||||
const pageUpdatedAt = this.pages?.[p]?.updated_at;
|
|
||||||
return !isThisWeek(new Date(pageUpdatedAt)) && !isYesterday(new Date(pageUpdatedAt));
|
|
||||||
}) || [];
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* retrieves all archived pages for a projectId that is available in the url.
|
|
||||||
*/
|
|
||||||
get archivedProjectPageIds() {
|
|
||||||
const projectId = this.rootStore.app.router.projectId;
|
|
||||||
if (!projectId) return null;
|
|
||||||
const archivedProjectPageIds = Object.keys(this.archivedPages).filter(
|
|
||||||
(pageId) => this.archivedPages?.[pageId]?.project === projectId
|
|
||||||
);
|
|
||||||
return archivedProjectPageIds ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* retrieves a page from pages by id.
|
|
||||||
* @param pageId
|
|
||||||
* @returns IPage | null
|
|
||||||
*/
|
|
||||||
getUnArchivedPageById = (pageId: string) => this.pages?.[pageId] ?? null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* retrieves a page from archived pages by id.
|
|
||||||
* @param pageId
|
|
||||||
* @returns IPage | null
|
|
||||||
*/
|
|
||||||
getArchivedPageById = (pageId: string) => this.archivedPages?.[pageId] ?? null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* fetches all pages for a project.
|
|
||||||
* @param workspaceSlug
|
|
||||||
* @param projectId
|
|
||||||
* @returns Promise<IPage[]>
|
|
||||||
*/
|
|
||||||
fetchProjectPages = async (workspaceSlug: string, projectId: string) => {
|
|
||||||
try {
|
|
||||||
return await this.pageService.getProjectPages(workspaceSlug, projectId).then((response) => {
|
|
||||||
console.log("Response from backend 1", response);
|
|
||||||
runInAction(() => {
|
|
||||||
response.forEach((page) => {
|
|
||||||
set(this.pages, [page.id], page);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return response;
|
},
|
||||||
});
|
{ delay: 3000 }
|
||||||
} catch (error) {
|
);
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
const pageTitleDisposer = reaction(
|
||||||
* fetches all archived pages for a project.
|
() => this.name,
|
||||||
* @param workspaceSlug
|
(name) => {
|
||||||
* @param projectId
|
const { projectId, workspaceSlug } = this.rootStore.app.router;
|
||||||
* @returns Promise<IPage[]>
|
if (!projectId || !workspaceSlug) return;
|
||||||
*/
|
this.isSubmitting = "submitting";
|
||||||
fetchArchivedProjectPages = async (workspaceSlug: string, projectId: string) =>
|
this.pageService
|
||||||
await this.pageService.getArchivedPages(workspaceSlug, projectId).then((response) => {
|
.patchPage(workspaceSlug, projectId, this.id, { name })
|
||||||
runInAction(() => {
|
.catch(() => {
|
||||||
response.forEach((page) => {
|
runInAction(() => {
|
||||||
set(this.archivedPages, [page.id], page);
|
this.name = this.oldName;
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
return response;
|
.finally(() => {
|
||||||
|
runInAction(() => {
|
||||||
|
this.isSubmitting = "submitted";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ delay: 2000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.disposers.push(descriptionDisposer, pageTitleDisposer);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateName = action("updateName", async (name: string) => {
|
||||||
|
const { projectId, workspaceSlug } = this.rootStore.app.router;
|
||||||
|
if (!projectId || !workspaceSlug) return;
|
||||||
|
|
||||||
|
this.oldName = this.name;
|
||||||
|
this.name = name;
|
||||||
|
});
|
||||||
|
|
||||||
|
updateDescription = action("updateDescription", async (description_html: string) => {
|
||||||
|
const { projectId, workspaceSlug } = this.rootStore.app.router;
|
||||||
|
if (!projectId || !workspaceSlug) return;
|
||||||
|
|
||||||
|
this.description_html = description_html;
|
||||||
|
});
|
||||||
|
|
||||||
|
cleanup = action("cleanup", () => {
|
||||||
|
this.disposers.forEach((disposer) => {
|
||||||
|
disposer();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsSubmitting = action("setIsSubmitting", (isSubmitting: "submitting" | "submitted" | "saved") => {
|
||||||
|
this.isSubmitting = isSubmitting;
|
||||||
|
});
|
||||||
|
|
||||||
|
lockPage = action("lockPage", async () => {
|
||||||
|
const { projectId, workspaceSlug } = this.rootStore.app.router;
|
||||||
|
if (!projectId || !workspaceSlug) return;
|
||||||
|
|
||||||
|
this.is_locked = true;
|
||||||
|
|
||||||
|
await this.pageService.lockPage(workspaceSlug, projectId, this.id).catch(() => {
|
||||||
|
runInAction(() => {
|
||||||
|
this.is_locked = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
unlockPage = action("unlockPage", async () => {
|
||||||
|
const { projectId, workspaceSlug } = this.rootStore.app.router;
|
||||||
|
if (!projectId || !workspaceSlug) return;
|
||||||
|
|
||||||
|
this.is_locked = false;
|
||||||
|
|
||||||
|
await this.pageService.unlockPage(workspaceSlug, projectId, this.id).catch(() => {
|
||||||
|
runInAction(() => {
|
||||||
|
this.is_locked = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add Page to users favorites list
|
* Add Page to users favorites list
|
||||||
* @param workspaceSlug
|
|
||||||
* @param projectId
|
|
||||||
* @param pageId
|
|
||||||
*/
|
*/
|
||||||
addToFavorites = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
addToFavorites = action("addToFavorites", async () => {
|
||||||
try {
|
const { projectId, workspaceSlug } = this.rootStore.app.router;
|
||||||
|
if (!projectId || !workspaceSlug) return;
|
||||||
|
|
||||||
|
this.is_favorite = true;
|
||||||
|
|
||||||
|
await this.pageService.addPageToFavorites(workspaceSlug, projectId, this.id).catch(() => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(this.pages, [pageId, "is_favorite"], true);
|
this.is_favorite = false;
|
||||||
});
|
});
|
||||||
await this.pageService.addPageToFavorites(workspaceSlug, projectId, pageId);
|
});
|
||||||
} catch (error) {
|
});
|
||||||
runInAction(() => {
|
|
||||||
set(this.pages, [pageId, "is_favorite"], false);
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove page from the users favorites list
|
* Remove page from the users favorites list
|
||||||
* @param workspaceSlug
|
|
||||||
* @param projectId
|
|
||||||
* @param pageId
|
|
||||||
*/
|
*/
|
||||||
removeFromFavorites = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
removeFromFavorites = action("removeFromFavorites", async () => {
|
||||||
try {
|
const { projectId, workspaceSlug } = this.rootStore.app.router;
|
||||||
runInAction(() => {
|
if (!projectId || !workspaceSlug) return;
|
||||||
set(this.pages, [pageId, "is_favorite"], false);
|
|
||||||
});
|
|
||||||
await this.pageService.removePageFromFavorites(workspaceSlug, projectId, pageId);
|
|
||||||
} catch (error) {
|
|
||||||
runInAction(() => {
|
|
||||||
set(this.pages, [pageId, "is_favorite"], true);
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Creates a new page using the api and updated the local state in store
|
|
||||||
* @param workspaceSlug
|
|
||||||
* @param projectId
|
|
||||||
* @param data
|
|
||||||
*/
|
|
||||||
createPage = async (workspaceSlug: string, projectId: string, data: Partial<IPage>) =>
|
|
||||||
await this.pageService.createPage(workspaceSlug, projectId, data).then((response) => {
|
|
||||||
runInAction(() => {
|
|
||||||
set(this.pages, [response.id], response);
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
this.is_favorite = false;
|
||||||
* updates the page using the api and updates the local state in store
|
|
||||||
* @param workspaceSlug
|
|
||||||
* @param projectId
|
|
||||||
* @param pageId
|
|
||||||
* @param data
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
updatePage = async (workspaceSlug: string, projectId: string, pageId: string, data: Partial<IPage>) =>
|
|
||||||
await this.pageService.patchPage(workspaceSlug, projectId, pageId, data).then((response) => {
|
|
||||||
const originalPage = this.getUnArchivedPageById(pageId);
|
|
||||||
runInAction(() => {
|
|
||||||
set(this.pages, [pageId], { ...originalPage, ...data });
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
await this.pageService.removePageFromFavorites(workspaceSlug, projectId, this.id).catch(() => {
|
||||||
* delete a page using the api and updates the local state in store
|
|
||||||
* @param workspaceSlug
|
|
||||||
* @param projectId
|
|
||||||
* @param pageId
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
deletePage = async (workspaceSlug: string, projectId: string, pageId: string) =>
|
|
||||||
await this.pageService.deletePage(workspaceSlug, projectId, pageId).then((response) => {
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
omit(this.archivedPages, [pageId]);
|
this.is_favorite = true;
|
||||||
});
|
});
|
||||||
return response;
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* make a page public
|
* make a page public
|
||||||
* @param workspaceSlug
|
|
||||||
* @param projectId
|
|
||||||
* @param pageId
|
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
makePublic = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
makePublic = action("makePublic", async () => {
|
||||||
try {
|
const { projectId, workspaceSlug } = this.rootStore.app.router;
|
||||||
|
if (!projectId || !workspaceSlug) return;
|
||||||
|
|
||||||
|
this.access = 0;
|
||||||
|
|
||||||
|
this.pageService.patchPage(workspaceSlug, projectId, this.id, { access: 0 }).catch(() => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(this.pages, [pageId, "access"], 0);
|
this.access = 1;
|
||||||
});
|
});
|
||||||
await this.pageService.patchPage(workspaceSlug, projectId, pageId, { access: 0 });
|
});
|
||||||
} catch (error) {
|
});
|
||||||
runInAction(() => {
|
|
||||||
set(this.pages, [pageId, "access"], 1);
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a page private
|
* Make a page private
|
||||||
* @param workspaceSlug
|
|
||||||
* @param projectId
|
|
||||||
* @param pageId
|
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
makePrivate = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
makePrivate = action("makePrivate", async () => {
|
||||||
try {
|
const { projectId, workspaceSlug } = this.rootStore.app.router;
|
||||||
runInAction(() => {
|
if (!projectId || !workspaceSlug) return;
|
||||||
set(this.pages, [pageId, "access"], 1);
|
|
||||||
});
|
|
||||||
await this.pageService.patchPage(workspaceSlug, projectId, pageId, { access: 1 });
|
|
||||||
} catch (error) {
|
|
||||||
runInAction(() => {
|
|
||||||
set(this.pages, [pageId, "access"], 0);
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
this.access = 1;
|
||||||
* Mark a page archived
|
|
||||||
* @param workspaceSlug
|
this.pageService.patchPage(workspaceSlug, projectId, this.id, { access: 1 }).catch(() => {
|
||||||
* @param projectId
|
|
||||||
* @param pageId
|
|
||||||
*/
|
|
||||||
archivePage = async (workspaceSlug: string, projectId: string, pageId: string) =>
|
|
||||||
await this.pageService.archivePage(workspaceSlug, projectId, pageId).then(() => {
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(this.archivedPages, [pageId], this.pages[pageId]);
|
this.access = 0;
|
||||||
set(this.archivedPages, [pageId, "archived_at"], renderFormattedPayloadDate(new Date()));
|
|
||||||
omit(this.pages, [pageId]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore a page from archived pages to pages
|
|
||||||
* @param workspaceSlug
|
|
||||||
* @param projectId
|
|
||||||
* @param pageId
|
|
||||||
*/
|
|
||||||
restorePage = async (workspaceSlug: string, projectId: string, pageId: string) =>
|
|
||||||
await this.pageService.restorePage(workspaceSlug, projectId, pageId).then(() => {
|
|
||||||
runInAction(() => {
|
|
||||||
set(this.pages, [pageId], this.archivedPages[pageId]);
|
|
||||||
omit(this.archivedPages, [pageId]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,54 @@
|
|||||||
import { makeObservable, observable, runInAction, action } from "mobx";
|
import { makeObservable, observable, runInAction, action, computed } from "mobx";
|
||||||
import { set } from "lodash";
|
import { set } from "lodash";
|
||||||
// services
|
// services
|
||||||
import { PageService } from "services/page.service";
|
import { PageService } from "services/page.service";
|
||||||
// store
|
// store
|
||||||
import { PageStore, IPageStore } from "store/page.store";
|
import { PageStore, IPageStore } from "store/page.store";
|
||||||
// types
|
// types
|
||||||
import { IPage } from "@plane/types";
|
import { IPage, IRecentPages } from "@plane/types";
|
||||||
|
import { RootStore } from "./root.store";
|
||||||
|
import { isThisWeek, isToday, isYesterday } from "date-fns";
|
||||||
|
|
||||||
export interface IProjectPageStore {
|
export interface IProjectPageStore {
|
||||||
projectPages: Record<string, IPageStore[]>;
|
projectPageMap: Record<string, Record<string, IPageStore>>;
|
||||||
projectArchivedPages: Record<string, IPageStore[]>;
|
projectArchivedPageMap: Record<string, Record<string, IPageStore>>;
|
||||||
|
|
||||||
|
projectPageIds: string[] | undefined;
|
||||||
|
archivedPageIds: string[] | undefined;
|
||||||
|
favoriteProjectPageIds: string[] | undefined;
|
||||||
|
privateProjectPageIds: string[] | undefined;
|
||||||
|
publicProjectPageIds: string[] | undefined;
|
||||||
|
recentProjectPages: IRecentPages | undefined;
|
||||||
// fetch actions
|
// fetch actions
|
||||||
fetchProjectPages: (workspaceSlug: string, projectId: string) => void;
|
fetchProjectPages: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||||
fetchArchivedProjectPages: (workspaceSlug: string, projectId: string) => void;
|
fetchArchivedProjectPages: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||||
// crud actions
|
// crud actions
|
||||||
createPage: (workspaceSlug: string, projectId: string, data: Partial<IPage>) => void;
|
createPage: (workspaceSlug: string, projectId: string, data: Partial<IPage>) => Promise<IPage>;
|
||||||
deletePage: (workspaceSlug: string, projectId: string, pageId: string) => void;
|
deletePage: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||||
|
archivePage: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||||
|
restorePage: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProjectPageStore implements IProjectPageStore {
|
export class ProjectPageStore implements IProjectPageStore {
|
||||||
projectPages: Record<string, IPageStore[]> = {}; // { projectId: [page1, page2] }
|
projectPageMap: Record<string, Record<string, IPageStore>> = {}; // { projectId: [page1, page2] }
|
||||||
projectArchivedPages: Record<string, IPageStore[]> = {}; // { projectId: [page1, page2] }
|
projectArchivedPageMap: Record<string, Record<string, IPageStore>> = {}; // { projectId: [page1, page2] }
|
||||||
|
|
||||||
|
// root store
|
||||||
|
rootStore;
|
||||||
|
|
||||||
pageService;
|
pageService;
|
||||||
|
constructor(_rootStore: RootStore) {
|
||||||
constructor() {
|
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
projectPages: observable,
|
projectPageMap: observable,
|
||||||
projectArchivedPages: observable,
|
projectArchivedPageMap: observable,
|
||||||
|
|
||||||
|
projectPageIds: computed,
|
||||||
|
archivedPageIds: computed,
|
||||||
|
favoriteProjectPageIds: computed,
|
||||||
|
privateProjectPageIds: computed,
|
||||||
|
publicProjectPageIds: computed,
|
||||||
|
recentProjectPages: computed,
|
||||||
|
|
||||||
// fetch actions
|
// fetch actions
|
||||||
fetchProjectPages: action,
|
fetchProjectPages: action,
|
||||||
fetchArchivedProjectPages: action,
|
fetchArchivedProjectPages: action,
|
||||||
@ -35,19 +56,113 @@ export class ProjectPageStore implements IProjectPageStore {
|
|||||||
createPage: action,
|
createPage: action,
|
||||||
deletePage: action,
|
deletePage: action,
|
||||||
});
|
});
|
||||||
|
this.rootStore = _rootStore;
|
||||||
|
|
||||||
this.pageService = new PageService();
|
this.pageService = new PageService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get projectPageIds() {
|
||||||
|
const projectId = this.rootStore.app.router.projectId;
|
||||||
|
if (!projectId || !this.projectPageMap?.[projectId]) return [];
|
||||||
|
|
||||||
|
const allProjectIds = Object.keys(this.projectPageMap[projectId]);
|
||||||
|
return allProjectIds.sort((a, b) => {
|
||||||
|
const dateA = new Date(this.projectPageMap[projectId][a].created_at).getTime();
|
||||||
|
const dateB = new Date(this.projectPageMap[projectId][b].created_at).getTime();
|
||||||
|
return dateB - dateA;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get archivedPageIds() {
|
||||||
|
const projectId = this.rootStore.app.router.projectId;
|
||||||
|
if (!projectId || !this.projectArchivedPageMap[projectId]) return [];
|
||||||
|
const archivedPages = Object.keys(this.projectArchivedPageMap[projectId]);
|
||||||
|
return archivedPages.sort((a, b) => {
|
||||||
|
const dateA = new Date(this.projectArchivedPageMap[projectId][a].created_at).getTime();
|
||||||
|
const dateB = new Date(this.projectArchivedPageMap[projectId][b].created_at).getTime();
|
||||||
|
return dateB - dateA;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get favoriteProjectPageIds() {
|
||||||
|
const projectId = this.rootStore.app.router.projectId;
|
||||||
|
if (!this.projectPageIds || !projectId) return [];
|
||||||
|
|
||||||
|
const favouritePages: string[] = this.projectPageIds.filter(
|
||||||
|
(page) => this.projectPageMap[projectId][page].is_favorite
|
||||||
|
);
|
||||||
|
return favouritePages;
|
||||||
|
}
|
||||||
|
|
||||||
|
get privateProjectPageIds() {
|
||||||
|
const projectId = this.rootStore.app.router.projectId;
|
||||||
|
if (!this.projectPageIds || !projectId) return [];
|
||||||
|
|
||||||
|
const privatePages: string[] = this.projectPageIds.filter(
|
||||||
|
(page) => this.projectPageMap[projectId][page].access === 1
|
||||||
|
);
|
||||||
|
return privatePages;
|
||||||
|
}
|
||||||
|
|
||||||
|
get publicProjectPageIds() {
|
||||||
|
const projectId = this.rootStore.app.router.projectId;
|
||||||
|
const userId = this.rootStore.user.currentUser?.id;
|
||||||
|
if (!this.projectPageIds || !projectId || !userId) return [];
|
||||||
|
|
||||||
|
const publicPages: string[] = this.projectPageIds.filter(
|
||||||
|
(page) =>
|
||||||
|
this.projectPageMap[projectId][page].access === 0 && this.projectPageMap[projectId][page].owned_by === userId
|
||||||
|
);
|
||||||
|
return publicPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
get recentProjectPages() {
|
||||||
|
const projectId = this.rootStore.app.router.projectId;
|
||||||
|
if (!this.projectPageIds || !projectId) return;
|
||||||
|
|
||||||
|
const today: string[] = this.projectPageIds.filter((page) =>
|
||||||
|
isToday(new Date(this.projectPageMap[projectId][page].updated_at))
|
||||||
|
);
|
||||||
|
|
||||||
|
const yesterday: string[] = this.projectPageIds.filter((page) =>
|
||||||
|
isYesterday(new Date(this.projectPageMap[projectId][page].updated_at))
|
||||||
|
);
|
||||||
|
|
||||||
|
const this_week: string[] = this.projectPageIds.filter((page) => {
|
||||||
|
const pageUpdatedAt = this.projectPageMap[projectId][page].updated_at;
|
||||||
|
return (
|
||||||
|
isThisWeek(new Date(pageUpdatedAt)) &&
|
||||||
|
!isToday(new Date(pageUpdatedAt)) &&
|
||||||
|
!isYesterday(new Date(pageUpdatedAt))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const older: string[] = this.projectPageIds.filter((page) => {
|
||||||
|
const pageUpdatedAt = this.projectPageMap[projectId][page].updated_at;
|
||||||
|
return !isThisWeek(new Date(pageUpdatedAt)) && !isYesterday(new Date(pageUpdatedAt));
|
||||||
|
});
|
||||||
|
|
||||||
|
return { today, yesterday, this_week, older };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetching all the pages for a specific project
|
* Fetching all the pages for a specific project
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
* @param projectId
|
* @param projectId
|
||||||
*/
|
*/
|
||||||
fetchProjectPages = async (workspaceSlug: string, projectId: string) => {
|
fetchProjectPages = async (workspaceSlug: string, projectId: string) => {
|
||||||
const response = await this.pageService.getProjectPages(workspaceSlug, projectId);
|
try {
|
||||||
runInAction(() => {
|
await this.pageService.getProjectPages(workspaceSlug, projectId).then((response) => {
|
||||||
this.projectPages[projectId] = response?.map((page) => new PageStore(page as any));
|
runInAction(() => {
|
||||||
});
|
for (const page of response) {
|
||||||
|
set(this.projectPageMap, [projectId, page.id], new PageStore(page, this.rootStore));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,13 +171,20 @@ export class ProjectPageStore implements IProjectPageStore {
|
|||||||
* @param projectId
|
* @param projectId
|
||||||
* @returns Promise<IPage[]>
|
* @returns Promise<IPage[]>
|
||||||
*/
|
*/
|
||||||
fetchArchivedProjectPages = async (workspaceSlug: string, projectId: string) =>
|
fetchArchivedProjectPages = async (workspaceSlug: string, projectId: string) => {
|
||||||
await this.pageService.getArchivedPages(workspaceSlug, projectId).then((response) => {
|
try {
|
||||||
runInAction(() => {
|
await this.pageService.getArchivedPages(workspaceSlug, projectId).then((response) => {
|
||||||
this.projectArchivedPages[projectId] = response?.map((page) => new PageStore(page as any));
|
runInAction(() => {
|
||||||
|
for (const page of response) {
|
||||||
|
set(this.projectArchivedPageMap, [projectId, page.id], new PageStore(page, this.rootStore));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response;
|
||||||
});
|
});
|
||||||
return response;
|
} catch (e) {
|
||||||
});
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new page using the api and updated the local state in store
|
* Creates a new page using the api and updated the local state in store
|
||||||
@ -73,7 +195,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
|||||||
createPage = async (workspaceSlug: string, projectId: string, data: Partial<IPage>) => {
|
createPage = async (workspaceSlug: string, projectId: string, data: Partial<IPage>) => {
|
||||||
const response = await this.pageService.createPage(workspaceSlug, projectId, data);
|
const response = await this.pageService.createPage(workspaceSlug, projectId, data);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.projectPages[projectId] = [...this.projectPages[projectId], new PageStore(response as any)];
|
set(this.projectPageMap, [projectId, response.id], new PageStore(response, this.rootStore));
|
||||||
});
|
});
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
@ -88,11 +210,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
|||||||
deletePage = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
deletePage = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||||
const response = await this.pageService.deletePage(workspaceSlug, projectId, pageId);
|
const response = await this.pageService.deletePage(workspaceSlug, projectId, pageId);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.projectPages = set(
|
delete this.projectArchivedPageMap[projectId][pageId];
|
||||||
this.projectPages,
|
|
||||||
[projectId],
|
|
||||||
this.projectPages[projectId].filter((page: any) => page.id !== pageId)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
@ -104,14 +222,17 @@ export class ProjectPageStore implements IProjectPageStore {
|
|||||||
* @param pageId
|
* @param pageId
|
||||||
*/
|
*/
|
||||||
archivePage = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
archivePage = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||||
const response = await this.pageService.archivePage(workspaceSlug, projectId, pageId);
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(
|
set(this.projectArchivedPageMap, [projectId, pageId], this.projectPageMap[projectId][pageId]);
|
||||||
this.projectPages,
|
set(this.projectArchivedPageMap[projectId][pageId], "archived_at", new Date().toISOString());
|
||||||
[projectId],
|
delete this.projectPageMap[projectId][pageId];
|
||||||
this.projectPages[projectId].filter((page: any) => page.id !== pageId)
|
});
|
||||||
);
|
const response = await this.pageService.archivePage(workspaceSlug, projectId, pageId).catch(() => {
|
||||||
this.projectArchivedPages = set(this.projectArchivedPages, [projectId], this.projectArchivedPages[projectId]);
|
runInAction(() => {
|
||||||
|
set(this.projectPageMap, [projectId, pageId], this.projectArchivedPageMap[projectId][pageId]);
|
||||||
|
set(this.projectPageMap[projectId][pageId], "archived_at", null);
|
||||||
|
delete this.projectArchivedPageMap[projectId][pageId];
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
@ -122,15 +243,19 @@ export class ProjectPageStore implements IProjectPageStore {
|
|||||||
* @param projectId
|
* @param projectId
|
||||||
* @param pageId
|
* @param pageId
|
||||||
*/
|
*/
|
||||||
restorePage = async (workspaceSlug: string, projectId: string, pageId: string) =>
|
restorePage = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||||
await this.pageService.restorePage(workspaceSlug, projectId, pageId).then(() => {
|
const pageArchivedAt = this.projectArchivedPageMap[projectId][pageId].archived_at;
|
||||||
|
runInAction(() => {
|
||||||
|
set(this.projectPageMap, [projectId, pageId], this.projectArchivedPageMap[projectId][pageId]);
|
||||||
|
set(this.projectPageMap[projectId][pageId], "archived_at", null);
|
||||||
|
delete this.projectArchivedPageMap[projectId][pageId];
|
||||||
|
});
|
||||||
|
await this.pageService.restorePage(workspaceSlug, projectId, pageId).catch(() => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(
|
set(this.projectArchivedPageMap, [projectId, pageId], this.projectPageMap[projectId][pageId]);
|
||||||
this.projectArchivedPages,
|
set(this.projectArchivedPageMap[projectId][pageId], "archived_at", pageArchivedAt);
|
||||||
[projectId],
|
delete this.projectPageMap[projectId][pageId];
|
||||||
this.projectArchivedPages[projectId].filter((page: any) => page.id !== pageId)
|
|
||||||
);
|
|
||||||
set(this.projectPages, [projectId], [...this.projectPages[projectId]]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ import { IUserRootStore, UserRootStore } from "./user";
|
|||||||
import { IWorkspaceRootStore, WorkspaceRootStore } from "./workspace";
|
import { IWorkspaceRootStore, WorkspaceRootStore } from "./workspace";
|
||||||
import { IssueRootStore, IIssueRootStore } from "./issue/root.store";
|
import { IssueRootStore, IIssueRootStore } from "./issue/root.store";
|
||||||
import { IStateStore, StateStore } from "./state.store";
|
import { IStateStore, StateStore } from "./state.store";
|
||||||
import { IPageStore, PageStore } from "./page.store";
|
|
||||||
import { ILabelRootStore, LabelRootStore } from "./label";
|
import { ILabelRootStore, LabelRootStore } from "./label";
|
||||||
import { IMemberRootStore, MemberRootStore } from "./member";
|
import { IMemberRootStore, MemberRootStore } from "./member";
|
||||||
import { IInboxRootStore, InboxRootStore } from "./inbox";
|
import { IInboxRootStore, InboxRootStore } from "./inbox";
|
||||||
@ -33,7 +32,6 @@ export class RootStore {
|
|||||||
module: IModuleStore;
|
module: IModuleStore;
|
||||||
projectView: IProjectViewStore;
|
projectView: IProjectViewStore;
|
||||||
globalView: IGlobalViewStore;
|
globalView: IGlobalViewStore;
|
||||||
page: IPageStore;
|
|
||||||
issue: IIssueRootStore;
|
issue: IIssueRootStore;
|
||||||
state: IStateStore;
|
state: IStateStore;
|
||||||
estimate: IEstimateStore;
|
estimate: IEstimateStore;
|
||||||
@ -58,8 +56,7 @@ export class RootStore {
|
|||||||
this.state = new StateStore(this);
|
this.state = new StateStore(this);
|
||||||
this.estimate = new EstimateStore(this);
|
this.estimate = new EstimateStore(this);
|
||||||
this.mention = new MentionStore(this);
|
this.mention = new MentionStore(this);
|
||||||
|
this.projectPages = new ProjectPageStore(this);
|
||||||
this.dashboard = new DashboardStore(this);
|
this.dashboard = new DashboardStore(this);
|
||||||
this.projectPages = new ProjectPageStore();
|
|
||||||
this.page = new PageStore(this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
yarn.lock
24
yarn.lock
@ -6617,13 +6617,35 @@ mkdirp@^0.5.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.6"
|
minimist "^1.2.6"
|
||||||
|
|
||||||
mobx-react-lite@^4.0.3:
|
mobx-devtools-mst@^0.9.30:
|
||||||
|
version "0.9.30"
|
||||||
|
resolved "https://registry.yarnpkg.com/mobx-devtools-mst/-/mobx-devtools-mst-0.9.30.tgz#0d1cad8b3d97e1f3f94bb9afb701cd9c8b5b164d"
|
||||||
|
integrity sha512-6fIYeFG4xT4syIeKddmK55zQbc3ZZZr/272/cCbfaAAM5YiuFdteGZGUgdsz8wxf/mGxWZbFOM3WmASAnpwrbw==
|
||||||
|
|
||||||
|
mobx-react-devtools@^6.1.1:
|
||||||
|
version "6.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mobx-react-devtools/-/mobx-react-devtools-6.1.1.tgz#a462b944085cf11ff96fc937d12bf31dab4c8984"
|
||||||
|
integrity sha512-nc5IXLdEUFLn3wZal65KF3/JFEFd+mbH4KTz/IG5BOPyw7jo8z29w/8qm7+wiCyqVfUIgJ1gL4+HVKmcXIOgqA==
|
||||||
|
|
||||||
|
mobx-react-lite@^4.0.3, mobx-react-lite@^4.0.4:
|
||||||
version "4.0.5"
|
version "4.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-4.0.5.tgz#e2cb98f813e118917bcc463638f5bf6ea053a67b"
|
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-4.0.5.tgz#e2cb98f813e118917bcc463638f5bf6ea053a67b"
|
||||||
integrity sha512-StfB2wxE8imKj1f6T8WWPf4lVMx3cYH9Iy60bbKXEs21+HQ4tvvfIBZfSmMXgQAefi8xYEwQIz4GN9s0d2h7dg==
|
integrity sha512-StfB2wxE8imKj1f6T8WWPf4lVMx3cYH9Iy60bbKXEs21+HQ4tvvfIBZfSmMXgQAefi8xYEwQIz4GN9s0d2h7dg==
|
||||||
dependencies:
|
dependencies:
|
||||||
use-sync-external-store "^1.2.0"
|
use-sync-external-store "^1.2.0"
|
||||||
|
|
||||||
|
mobx-react@^9.1.0:
|
||||||
|
version "9.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-9.1.0.tgz#5e54919ca27ffad5f2c0d835148a1f681cebdbc1"
|
||||||
|
integrity sha512-DeDRTYw4AlgHw8xEXtiZdKKEnp+c5/jeUgTbTQXEqnAzfkrgYRWP3p3Nv3Whc2CEcM/mDycbDWGjxKokQdlffg==
|
||||||
|
dependencies:
|
||||||
|
mobx-react-lite "^4.0.4"
|
||||||
|
|
||||||
|
mobx-state-tree@^5.4.0:
|
||||||
|
version "5.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mobx-state-tree/-/mobx-state-tree-5.4.0.tgz#d41b7fd90b8d4b063bc32526758417f1100751df"
|
||||||
|
integrity sha512-2VuUhAqFklxgGqFNqaZUXYYSQINo8C2SUEP9YfCQrwatHWHqJLlEC7Xb+5WChkev7fubzn3aVuby26Q6h+JeBg==
|
||||||
|
|
||||||
mobx@^6.10.0:
|
mobx@^6.10.0:
|
||||||
version "6.12.0"
|
version "6.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.12.0.tgz#72b2685ca5af031aaa49e77a4d76ed67fcbf9135"
|
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.12.0.tgz#72b2685ca5af031aaa49e77a4d76ed67fcbf9135"
|
||||||
|
Loading…
Reference in New Issue
Block a user