forked from github/plane
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
|
||||
from datetime import timedelta, date, datetime
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
# Django imports
|
||||
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.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from .base import BaseViewSet, BaseAPIView
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.db.models import (
|
||||
Page,
|
||||
PageFavorite,
|
||||
Issue,
|
||||
IssueAssignee,
|
||||
IssueActivity,
|
||||
PageLog,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
PageSerializer,
|
||||
PageFavoriteSerializer,
|
||||
PageLogSerializer,
|
||||
IssueLiteSerializer,
|
||||
SubPageSerializer,
|
||||
)
|
||||
from plane.app.serializers import (IssueLiteSerializer, PageFavoriteSerializer,
|
||||
PageLogSerializer, PageSerializer,
|
||||
SubPageSerializer)
|
||||
from plane.db.models import (Issue, IssueActivity, IssueAssignee, Page,
|
||||
PageFavorite, PageLog, ProjectMember)
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView, BaseViewSet
|
||||
|
||||
|
||||
def unarchive_archive_page_and_descendants(page_id, archived_at):
|
||||
@ -175,7 +164,7 @@ class PageViewSet(BaseViewSet):
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
is_active=True,
|
||||
role__gt=20,
|
||||
role__gte=20,
|
||||
).exists()
|
||||
or request.user.id != page.owned_by_id
|
||||
):
|
||||
|
@ -32,6 +32,7 @@
|
||||
"@plane/editor-core": "*",
|
||||
"@plane/editor-extensions": "*",
|
||||
"@plane/ui": "*",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@tiptap/core": "^2.1.13",
|
||||
"@tiptap/extension-placeholder": "^2.1.13",
|
||||
"@tiptap/pm": "^2.1.13",
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
|
||||
type IPageRenderer = {
|
||||
documentDetails: DocumentDetails;
|
||||
updatePageTitle: (title: string) => Promise<void>;
|
||||
updatePageTitle: (title: string) => void;
|
||||
editor: Editor;
|
||||
onActionCompleteHandler: (action: {
|
||||
title: string;
|
||||
@ -30,18 +30,6 @@ type IPageRenderer = {
|
||||
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) => {
|
||||
const { documentDetails, editor, editorClassNames, editorContentCustomClassNames, updatePageTitle, readonly } = props;
|
||||
|
||||
@ -64,11 +52,9 @@ export const PageRenderer = (props: IPageRenderer) => {
|
||||
|
||||
const { getFloatingProps } = useInteractions([dismiss]);
|
||||
|
||||
const debouncedUpdatePageTitle = debounce(updatePageTitle, 300);
|
||||
|
||||
const handlePageTitleChange = (title: string) => {
|
||||
setPagetitle(title);
|
||||
debouncedUpdatePageTitle(title);
|
||||
updatePageTitle(title);
|
||||
};
|
||||
|
||||
const [cleanup, setcleanup] = useState(() => () => {});
|
||||
|
@ -26,7 +26,7 @@ export const DocumentEditorExtensions = (
|
||||
.focus()
|
||||
.insertContentAt(
|
||||
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();
|
||||
},
|
||||
|
@ -24,7 +24,7 @@ export const IssueSuggestions = (suggestions: any[]) => {
|
||||
title: suggestion.name,
|
||||
priority: suggestion.priority.toString(),
|
||||
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 }) => {
|
||||
editor
|
||||
.chain()
|
||||
|
@ -9,6 +9,8 @@ export const IssueEmbedSuggestions = Extension.create({
|
||||
addOptions() {
|
||||
return {
|
||||
suggestion: {
|
||||
char: "#issue_",
|
||||
allowSpaces: true,
|
||||
command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
|
||||
props.command({ editor, range });
|
||||
},
|
||||
@ -18,11 +20,8 @@ export const IssueEmbedSuggestions = Extension.create({
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
Suggestion({
|
||||
char: "#issue_",
|
||||
pluginKey: new PluginKey("issue-embed-suggestions"),
|
||||
editor: this.editor,
|
||||
allowSpaces: true,
|
||||
|
||||
...this.options.suggestion,
|
||||
}),
|
||||
];
|
||||
|
@ -53,7 +53,7 @@ const IssueSuggestionList = ({
|
||||
const commandListContainer = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let newDisplayedItems: { [key: string]: IssueSuggestionProps[] } = {};
|
||||
const newDisplayedItems: { [key: string]: IssueSuggestionProps[] } = {};
|
||||
let totalLength = 0;
|
||||
sections.forEach((section) => {
|
||||
newDisplayedItems[section] = items.filter((item) => item.state === section).slice(0, 5);
|
||||
@ -65,8 +65,8 @@ const IssueSuggestionList = ({
|
||||
}, [items]);
|
||||
|
||||
const selectItem = useCallback(
|
||||
(index: number) => {
|
||||
const item = displayedItems[currentSection][index];
|
||||
(section: string, index: number) => {
|
||||
const item = displayedItems[section][index];
|
||||
if (item) {
|
||||
command(item);
|
||||
}
|
||||
@ -87,6 +87,7 @@ const IssueSuggestionList = ({
|
||||
setSelectedIndex(
|
||||
(selectedIndex + displayedItems[currentSection].length - 1) % displayedItems[currentSection].length
|
||||
);
|
||||
e.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
if (e.key === "ArrowDown") {
|
||||
@ -101,10 +102,12 @@ const IssueSuggestionList = ({
|
||||
[currentSection]: [...prevItems[currentSection], ...nextItems],
|
||||
}));
|
||||
}
|
||||
e.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
selectItem(selectedIndex);
|
||||
selectItem(currentSection, selectedIndex);
|
||||
e.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
if (e.key === "Tab") {
|
||||
@ -112,6 +115,7 @@ const IssueSuggestionList = ({
|
||||
const nextSectionIndex = (currentSectionIndex + 1) % sections.length;
|
||||
setCurrentSection(sections[nextSectionIndex]);
|
||||
setSelectedIndex(0);
|
||||
e.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -172,7 +176,7 @@ const IssueSuggestionList = ({
|
||||
}
|
||||
)}
|
||||
key={item.identifier}
|
||||
onClick={() => selectItem(index)}
|
||||
onClick={() => selectItem(section, index)}
|
||||
>
|
||||
<h5 className="whitespace-nowrap text-xs text-custom-text-300">{item.identifier}</h5>
|
||||
<PriorityIcon priority={item.priority} />
|
||||
@ -195,7 +199,7 @@ export const IssueListRenderer = () => {
|
||||
let popup: any | null = null;
|
||||
|
||||
return {
|
||||
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
|
||||
onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
|
||||
component = new ReactRenderer(IssueSuggestionList, {
|
||||
props,
|
||||
// @ts-ignore
|
||||
@ -210,10 +214,10 @@ export const IssueListRenderer = () => {
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: "manual",
|
||||
placement: "right",
|
||||
placement: "bottom-start",
|
||||
});
|
||||
},
|
||||
onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
|
||||
onUpdate: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
|
||||
component?.updateProps(props);
|
||||
|
||||
popup &&
|
||||
|
@ -15,8 +15,7 @@ export const IssueWidgetCard = (props) => {
|
||||
setIssueDetails(issue);
|
||||
setLoading(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
.catch(() => {
|
||||
setLoading(-1);
|
||||
});
|
||||
}, []);
|
||||
@ -30,7 +29,9 @@ export const IssueWidgetCard = (props) => {
|
||||
{loading == 0 ? (
|
||||
<div
|
||||
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">
|
||||
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
|
||||
|
@ -16,7 +16,7 @@ interface IDocumentEditor {
|
||||
// document info
|
||||
documentDetails: DocumentDetails;
|
||||
value: string;
|
||||
rerenderOnPropsChange: {
|
||||
rerenderOnPropsChange?: {
|
||||
id: string;
|
||||
description_html: string;
|
||||
};
|
||||
@ -39,7 +39,7 @@ interface IDocumentEditor {
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
setShouldShowAlert?: (showAlert: boolean) => void;
|
||||
forwardedRef?: any;
|
||||
updatePageTitle: (title: string) => Promise<void>;
|
||||
updatePageTitle: (title: string) => void;
|
||||
debouncedUpdatesEnabled?: boolean;
|
||||
isSubmitting: "submitting" | "submitted" | "saved";
|
||||
|
||||
|
@ -1,23 +1,17 @@
|
||||
import { FC } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
import { FileText, Plus } from "lucide-react";
|
||||
// hooks
|
||||
import { useApplication, useProject } from "hooks/store";
|
||||
// services
|
||||
import { PageService } from "services/page.service";
|
||||
import { useApplication, usePage, useProject } from "hooks/store";
|
||||
// ui
|
||||
import { Breadcrumbs, Button } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// fetch-keys
|
||||
import { PAGE_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
export interface IPagesHeaderProps {
|
||||
showButton?: boolean;
|
||||
}
|
||||
const pageService = new PageService();
|
||||
|
||||
export const PageDetailsHeader: FC<IPagesHeaderProps> = observer((props) => {
|
||||
const { showButton = false } = props;
|
||||
@ -28,12 +22,7 @@ export const PageDetailsHeader: FC<IPagesHeaderProps> = observer((props) => {
|
||||
const { commandPalette: commandPaletteStore } = useApplication();
|
||||
const { currentProjectDetails } = useProject();
|
||||
|
||||
const { data: pageDetails } = useSWR(
|
||||
workspaceSlug && currentProjectDetails?.id && pageId ? PAGE_DETAILS(pageId as string) : null,
|
||||
workspaceSlug && currentProjectDetails?.id
|
||||
? () => pageService.getPageDetails(workspaceSlug as string, currentProjectDetails.id, pageId as string)
|
||||
: null
|
||||
);
|
||||
const pageDetails = usePage(pageId as string);
|
||||
|
||||
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">
|
||||
|
@ -2,132 +2,56 @@ import React, { FC } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import { useApplication, usePage, useWorkspace } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useApplication } from "hooks/store";
|
||||
// components
|
||||
import { PageForm } from "./page-form";
|
||||
// types
|
||||
import { IPage } from "@plane/types";
|
||||
import { useProjectPages } from "hooks/store/use-project-page";
|
||||
import { IPageStore } from "store/page.store";
|
||||
|
||||
type Props = {
|
||||
data?: IPage | null;
|
||||
// data?: IPage | null;
|
||||
pageStore?: IPageStore;
|
||||
handleClose: () => void;
|
||||
isOpen: boolean;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const CreateUpdatePageModal: FC<Props> = (props) => {
|
||||
const { isOpen, handleClose, data, projectId } = props;
|
||||
const { isOpen, handleClose, projectId, pageStore } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { createPage } = useProjectPages();
|
||||
// store hooks
|
||||
const {
|
||||
eventTracker: { postHogEventTracker },
|
||||
} = useApplication();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { createPage, updatePage } = usePage();
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const onClose = () => {
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const createProjectPage = async (payload: IPage) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
// 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!,
|
||||
// }
|
||||
// );
|
||||
// });
|
||||
await createPage(workspaceSlug.toString(), projectId, payload);
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (formData: IPage) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
if (!data) await createProjectPage(formData);
|
||||
else await updateProjectPage(formData);
|
||||
try {
|
||||
if (pageStore) {
|
||||
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 (
|
||||
@ -157,7 +81,7 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
||||
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">
|
||||
<PageForm handleFormSubmit={handleFormSubmit} handleClose={handleClose} data={data} />
|
||||
<PageForm handleFormSubmit={handleFormSubmit} handleClose={handleClose} pageStore={pageStore} />
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
|
@ -9,37 +9,45 @@ import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import type { IPage } from "@plane/types";
|
||||
import { useProjectPages } from "hooks/store/use-project-page";
|
||||
|
||||
type TConfirmPageDeletionProps = {
|
||||
data?: IPage | null;
|
||||
pageId: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((props) => {
|
||||
const { data, isOpen, onClose } = props;
|
||||
const { pageId, isOpen, onClose } = props;
|
||||
|
||||
// states
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// store hooks
|
||||
const { deletePage } = usePage();
|
||||
const { deletePage } = useProjectPages();
|
||||
const pageStore = usePage(pageId);
|
||||
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
if (!pageStore) return null;
|
||||
|
||||
const { name } = pageStore;
|
||||
|
||||
const handleClose = () => {
|
||||
setIsDeleting(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!data || !workspaceSlug || !projectId) return;
|
||||
if (!pageId || !workspaceSlug || !projectId) return;
|
||||
|
||||
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(() => {
|
||||
handleClose();
|
||||
setToastAlert({
|
||||
@ -99,8 +107,8 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to delete page-{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">{data?.name}</span>? The Page
|
||||
will be deleted permanently. This action cannot be undone.
|
||||
<span className="break-words font-medium text-custom-text-100">{name}</span>? The Page will be
|
||||
deleted permanently. This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,11 +5,12 @@ import { Button, Input, Tooltip } from "@plane/ui";
|
||||
import { IPage } from "@plane/types";
|
||||
// constants
|
||||
import { PAGE_ACCESS_SPECIFIERS } from "constants/page";
|
||||
import { IPageStore } from "store/page.store";
|
||||
|
||||
type Props = {
|
||||
handleFormSubmit: (values: IPage) => Promise<void>;
|
||||
handleClose: () => void;
|
||||
data?: IPage | null;
|
||||
pageStore?: IPageStore;
|
||||
};
|
||||
|
||||
const defaultValues = {
|
||||
@ -19,24 +20,24 @@ const defaultValues = {
|
||||
};
|
||||
|
||||
export const PageForm: React.FC<Props> = (props) => {
|
||||
const { handleFormSubmit, handleClose, data } = props;
|
||||
const { handleFormSubmit, handleClose, pageStore } = props;
|
||||
|
||||
const {
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
control,
|
||||
} = useForm<IPage>({
|
||||
defaultValues: { ...defaultValues, ...data },
|
||||
defaultValues: pageStore
|
||||
? { name: pageStore.name, description: pageStore.description, access: pageStore.access }
|
||||
: defaultValues,
|
||||
});
|
||||
|
||||
const handleCreateUpdatePage = async (formData: IPage) => {
|
||||
await handleFormSubmit(formData);
|
||||
};
|
||||
const handleCreateUpdatePage = (formData: IPage) => handleFormSubmit(formData);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleCreateUpdatePage)}>
|
||||
<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>
|
||||
<Controller
|
||||
@ -104,7 +105,7 @@ export const PageForm: React.FC<Props> = (props) => {
|
||||
Cancel
|
||||
</Button>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { usePage } from "hooks/store";
|
||||
// components
|
||||
import { PagesListView } from "components/pages/pages-list";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
import { useProjectPages } from "hooks/store/use-project-specific-pages";
|
||||
|
||||
export const AllPagesList: FC = observer(() => {
|
||||
// store
|
||||
const { projectPageIds } = usePage();
|
||||
const pageStores = useProjectPages();
|
||||
// subscribing to the projectPageStore
|
||||
const { projectPageIds } = pageStores;
|
||||
|
||||
if (!projectPageIds)
|
||||
if (!projectPageIds) {
|
||||
return (
|
||||
<Loader className="space-y-4">
|
||||
<Loader.Item height="40px" />
|
||||
@ -19,6 +19,6 @@ export const AllPagesList: FC = observer(() => {
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
);
|
||||
|
||||
}
|
||||
return <PagesListView pageIds={projectPageIds} />;
|
||||
});
|
||||
|
@ -3,14 +3,15 @@ import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { PagesListView } from "components/pages/pages-list";
|
||||
// hooks
|
||||
import { usePage } from "hooks/store";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
import { useProjectPages } from "hooks/store/use-project-specific-pages";
|
||||
|
||||
export const ArchivedPagesList: FC = observer(() => {
|
||||
const { archivedProjectPageIds } = usePage();
|
||||
const projectPageStore = useProjectPages();
|
||||
const { archivedPageIds } = projectPageStore;
|
||||
|
||||
if (!archivedProjectPageIds)
|
||||
if (!archivedPageIds)
|
||||
return (
|
||||
<Loader className="space-y-4">
|
||||
<Loader.Item height="40px" />
|
||||
@ -19,5 +20,5 @@ export const ArchivedPagesList: FC = observer(() => {
|
||||
</Loader>
|
||||
);
|
||||
|
||||
return <PagesListView pageIds={archivedProjectPageIds} />;
|
||||
return <PagesListView pageIds={archivedPageIds} />;
|
||||
});
|
||||
|
@ -3,12 +3,13 @@ import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { PagesListView } from "components/pages/pages-list";
|
||||
// hooks
|
||||
import { usePage } from "hooks/store";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
import { useProjectPages } from "hooks/store/use-project-specific-pages";
|
||||
|
||||
export const FavoritePagesList: FC = observer(() => {
|
||||
const { favoriteProjectPageIds } = usePage();
|
||||
const projectPageStore = useProjectPages();
|
||||
const { favoriteProjectPageIds } = projectPageStore;
|
||||
|
||||
if (!favoriteProjectPageIds)
|
||||
return (
|
||||
|
@ -13,10 +13,6 @@ import {
|
||||
Star,
|
||||
Trash2,
|
||||
} 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 { renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper";
|
||||
// ui
|
||||
@ -25,142 +21,120 @@ import { CustomMenu, Tooltip } from "@plane/ui";
|
||||
import { CreateUpdatePageModal, DeletePageModal } from "components/pages";
|
||||
// constants
|
||||
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 {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
pageId: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
||||
const { workspaceSlug, projectId, pageId } = props;
|
||||
export const PagesListItem: FC<IPagesListItem> = observer(({ pageId, projectId }: IPagesListItem) => {
|
||||
const projectPageStore = useProjectPages();
|
||||
// Now, I am observing only the projectPages, out of the projectPageStore.
|
||||
const { archivePage, restorePage } = projectPageStore;
|
||||
|
||||
const pageStore = usePage(pageId);
|
||||
|
||||
// states
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
|
||||
|
||||
const [deletePageModal, setDeletePageModal] = useState(false);
|
||||
// store hooks
|
||||
|
||||
const {
|
||||
currentUser,
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const {
|
||||
getArchivedPageById,
|
||||
getUnArchivedPageById,
|
||||
archivePage,
|
||||
removeFromFavorites,
|
||||
addToFavorites,
|
||||
makePrivate,
|
||||
makePublic,
|
||||
restorePage,
|
||||
} = usePage();
|
||||
|
||||
const {
|
||||
project: { getProjectMemberDetails },
|
||||
} = 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.stopPropagation();
|
||||
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/pages/${pageId}`).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
message: "Page link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
await copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/pages/${pageId}`);
|
||||
};
|
||||
|
||||
const handleAddToFavorites = (e: any) => {
|
||||
const handleAddToFavorites = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
addToFavorites();
|
||||
};
|
||||
|
||||
const handleRemoveFromFavorites = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
addToFavorites(workspaceSlug, projectId, pageId)
|
||||
.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.",
|
||||
});
|
||||
});
|
||||
removeFromFavorites();
|
||||
};
|
||||
|
||||
const handleRemoveFromFavorites = (e: any) => {
|
||||
const handleMakePublic = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
removeFromFavorites(workspaceSlug, projectId, pageId)
|
||||
.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.",
|
||||
});
|
||||
});
|
||||
makePublic();
|
||||
};
|
||||
|
||||
const handleMakePublic = (e: any) => {
|
||||
const handleMakePrivate = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
makePublic(workspaceSlug, projectId, pageId);
|
||||
makePrivate();
|
||||
};
|
||||
|
||||
const handleMakePrivate = (e: any) => {
|
||||
const handleArchivePage = async (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
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.stopPropagation();
|
||||
|
||||
archivePage(workspaceSlug, projectId, pageId);
|
||||
await restorePage(workspaceSlug as string, projectId as string, pageId as string);
|
||||
};
|
||||
|
||||
const handleRestorePage = (e: any) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
restorePage(workspaceSlug, projectId, pageId);
|
||||
};
|
||||
|
||||
const handleDeletePage = (e: any) => {
|
||||
const handleDeletePage = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
setDeletePageModal(true);
|
||||
};
|
||||
|
||||
const handleEditPage = (e: any) => {
|
||||
const handleEditPage = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
setCreateUpdatePageModal(true);
|
||||
};
|
||||
|
||||
if (!pageDetails) return null;
|
||||
|
||||
const ownerDetails = getProjectMemberDetails(pageDetails.owned_by);
|
||||
const isCurrentUserOwner = pageDetails.owned_by === currentUser?.id;
|
||||
const ownerDetails = getProjectMemberDetails(owned_by);
|
||||
const isCurrentUserOwner = owned_by === currentUser?.id;
|
||||
|
||||
const userCanEdit =
|
||||
isCurrentUserOwner ||
|
||||
@ -173,22 +147,21 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
||||
return (
|
||||
<>
|
||||
<CreateUpdatePageModal
|
||||
pageStore={pageStore}
|
||||
isOpen={createUpdatePageModal}
|
||||
handleClose={() => setCreateUpdatePageModal(false)}
|
||||
data={pageDetails}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<DeletePageModal isOpen={deletePageModal} onClose={() => setDeletePageModal(false)} data={pageDetails} />
|
||||
<DeletePageModal isOpen={deletePageModal} onClose={() => setDeletePageModal(false)} pageId={pageId} />
|
||||
<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="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
<FileText className="h-4 w-4 shrink-0" />
|
||||
<p className="mr-2 truncate text-sm text-custom-text-100">{pageDetails.name}</p>
|
||||
{/* FIXME: replace any with proper type */}
|
||||
{pageDetails.label_details.length > 0 &&
|
||||
pageDetails.label_details.map((label: any) => (
|
||||
<p className="mr-2 truncate text-sm text-custom-text-100">{name}</p>
|
||||
{label_details.length > 0 &&
|
||||
label_details.map((label: IIssueLabel) => (
|
||||
<div
|
||||
key={label.id}
|
||||
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 className="flex items-center gap-2.5">
|
||||
{pageDetails.archived_at ? (
|
||||
{archived_at ? (
|
||||
<Tooltip
|
||||
tooltipContent={`Archived at ${renderFormattedTime(
|
||||
pageDetails.archived_at
|
||||
)} on ${renderFormattedDate(pageDetails.archived_at)}`}
|
||||
tooltipContent={`Archived at ${renderFormattedTime(archived_at)} on ${renderFormattedDate(
|
||||
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
|
||||
tooltipContent={`Last updated at ${renderFormattedTime(
|
||||
pageDetails.updated_at
|
||||
)} on ${renderFormattedDate(pageDetails.updated_at)}`}
|
||||
tooltipContent={`Last updated at ${renderFormattedTime(updated_at)} on ${renderFormattedDate(
|
||||
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>
|
||||
)}
|
||||
{isEditingAllowed && (
|
||||
<Tooltip tooltipContent={`${pageDetails.is_favorite ? "Remove from favorites" : "Mark as favorite"}`}>
|
||||
{pageDetails.is_favorite ? (
|
||||
<Tooltip tooltipContent={`${is_favorite ? "Remove from favorites" : "Mark as favorite"}`}>
|
||||
{is_favorite ? (
|
||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||
<Star className="h-3.5 w-3.5 fill-orange-400 text-orange-400" />
|
||||
</button>
|
||||
@ -240,12 +213,10 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
||||
{userCanChangeAccess && (
|
||||
<Tooltip
|
||||
tooltipContent={`${
|
||||
pageDetails.access
|
||||
? "This page is only visible to you"
|
||||
: "This page can be viewed by anyone in the project"
|
||||
access ? "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}>
|
||||
<Lock className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@ -259,13 +230,13 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
||||
<Tooltip
|
||||
position="top-right"
|
||||
tooltipContent={`Created by ${ownerDetails?.member.display_name} on ${renderFormattedDate(
|
||||
pageDetails.created_at
|
||||
created_at
|
||||
)}`}
|
||||
>
|
||||
<AlertCircle className="h-3.5 w-3.5" />
|
||||
</Tooltip>
|
||||
<CustomMenu width="auto" placement="bottom-end" className="!-m-1" verticalEllipsis>
|
||||
{pageDetails.archived_at ? (
|
||||
{archived_at ? (
|
||||
<>
|
||||
{userCanArchive && (
|
||||
<CustomMenu.MenuItem onClick={handleRestorePage}>
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { FC } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Plus } from "lucide-react";
|
||||
// hooks
|
||||
import { useApplication, useUser } from "hooks/store";
|
||||
// components
|
||||
import { PagesListItem } from "./list-item";
|
||||
import { NewEmptyState } from "components/common/new-empty-state";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
@ -13,14 +11,17 @@ import { Loader } from "@plane/ui";
|
||||
import emptyPage from "public/empty-state/empty_page.png";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { PagesListItem } from "./list-item";
|
||||
|
||||
type IPagesListView = {
|
||||
pageIds: string[];
|
||||
};
|
||||
|
||||
export const PagesListView: FC<IPagesListView> = observer((props) => {
|
||||
const { pageIds } = props;
|
||||
export const PagesListView: FC<IPagesListView> = (props) => {
|
||||
const { pageIds: projectPageIds } = props;
|
||||
// store hooks
|
||||
// trace(true);
|
||||
|
||||
const {
|
||||
commandPalette: { toggleCreatePageModal },
|
||||
} = useApplication();
|
||||
@ -31,21 +32,18 @@ export const PagesListView: FC<IPagesListView> = observer((props) => {
|
||||
const router = useRouter();
|
||||
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;
|
||||
|
||||
return (
|
||||
<>
|
||||
{pageIds && workspaceSlug && projectId ? (
|
||||
{projectPageIds && workspaceSlug && projectId ? (
|
||||
<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">
|
||||
{pageIds.map((pageId) => (
|
||||
<PagesListItem
|
||||
key={pageId}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
pageId={pageId}
|
||||
/>
|
||||
{projectPageIds.map((pageId: string) => (
|
||||
<PagesListItem key={pageId} pageId={pageId} projectId={projectId.toString()} />
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
@ -77,4 +75,4 @@ export const PagesListView: FC<IPagesListView> = observer((props) => {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { usePage } from "hooks/store";
|
||||
// components
|
||||
import { PagesListView } from "components/pages/pages-list";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
import { useProjectPages } from "hooks/store/use-project-specific-pages";
|
||||
|
||||
export const PrivatePagesList: FC = observer(() => {
|
||||
const { privateProjectPageIds } = usePage();
|
||||
const projectPageStore = useProjectPages();
|
||||
const { privateProjectPageIds } = projectPageStore;
|
||||
|
||||
if (!privateProjectPageIds)
|
||||
return (
|
||||
|
@ -2,7 +2,7 @@ import React, { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Plus } from "lucide-react";
|
||||
// hooks
|
||||
import { useApplication, usePage, useUser } from "hooks/store";
|
||||
import { useApplication, useUser } from "hooks/store";
|
||||
// components
|
||||
import { PagesListView } from "components/pages/pages-list";
|
||||
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";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { useProjectPages } from "hooks/store/use-project-specific-pages";
|
||||
|
||||
export const RecentPagesList: FC = observer(() => {
|
||||
// store hooks
|
||||
@ -21,7 +22,7 @@ export const RecentPagesList: FC = observer(() => {
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { recentProjectPages } = usePage();
|
||||
const { recentProjectPages } = useProjectPages();
|
||||
|
||||
// FIXME: replace any with proper type
|
||||
const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value: any) => value.length === 0);
|
||||
|
@ -3,12 +3,13 @@ import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { PagesListView } from "components/pages/pages-list";
|
||||
// hooks
|
||||
import { usePage } from "hooks/store";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
import { useProjectPages } from "hooks/store/use-project-specific-pages";
|
||||
|
||||
export const SharedPagesList: FC = observer(() => {
|
||||
const { publicProjectPageIds } = usePage();
|
||||
const projectPageStore = useProjectPages();
|
||||
const { publicProjectPageIds } = projectPageStore;
|
||||
|
||||
if (!publicProjectPageIds)
|
||||
return (
|
||||
|
@ -1,11 +1,21 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
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);
|
||||
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";
|
||||
// mobx store
|
||||
import { StoreContext } from "contexts/store-context";
|
||||
import { IProjectPageStore } from "store/project-page.store";
|
||||
|
||||
export const useProjectPages = () => {
|
||||
export const useProjectPages = (): IProjectPageStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useProjectPublish must be used within StoreProvider");
|
||||
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/lite-text-editor": "*",
|
||||
"@plane/rich-text-editor": "*",
|
||||
"@plane/ui": "*",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/nextjs": "^7.85.0",
|
||||
"axios": "^1.1.3",
|
||||
@ -39,7 +39,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.294.0",
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react-lite": "^4.0.3",
|
||||
"mobx-react": "^9.1.0",
|
||||
"next": "^14.0.3",
|
||||
"next-pwa": "^5.6.0",
|
||||
"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 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
|
||||
import { useApplication, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useApplication, useIssues, usePage, useUser } from "hooks/store";
|
||||
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||
import useToast from "hooks/use-toast";
|
||||
// services
|
||||
import { PageService } from "services/page.service";
|
||||
import { FileService } from "services/file.service";
|
||||
import { IssueService } from "services/issue";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { GptAssistantPopover } from "components/core";
|
||||
import { PageDetailsHeader } from "components/headers/page-details";
|
||||
import { EmptyState } from "components/common";
|
||||
// ui
|
||||
import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// assets
|
||||
import emptyPage from "public/empty-state/page.svg";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IPage } from "@plane/types";
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
import { IPage, TIssue } from "@plane/types";
|
||||
// fetch-keys
|
||||
import { PAGE_DETAILS, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||
// constants
|
||||
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
|
||||
const fileService = new FileService();
|
||||
const pageService = new PageService();
|
||||
const issueService = new IssueService();
|
||||
|
||||
const PageDetailsPage: NextPageWithLayout = observer(() => {
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||
const [gptModalOpen, setGptModal] = useState(false);
|
||||
// refs
|
||||
const editorRef = useRef<any>(null);
|
||||
// router
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaceSlug, projectId, pageId } = router.query;
|
||||
// store hooks
|
||||
const {
|
||||
@ -59,18 +57,82 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
//TODO:fix reload confirmations, with mobx
|
||||
const { setShowAlert } = useReloadConfirmations();
|
||||
|
||||
const { handleSubmit, setValue, watch, getValues, control, reset } = useForm<IPage>({
|
||||
defaultValues: { name: "", description_html: "" },
|
||||
});
|
||||
|
||||
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 {
|
||||
archivePage: archivePageAction,
|
||||
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) => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
@ -78,47 +140,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
||||
const newDescription = `${watch("description_html")}<p>${response}</p>`;
|
||||
setValue("description_html", newDescription);
|
||||
editorRef.current?.setEditorValue(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 });
|
||||
updateDescriptionAction(newDescription);
|
||||
};
|
||||
|
||||
const actionCompleteAlert = ({
|
||||
@ -137,122 +159,14 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
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) => {
|
||||
const updatePageTitle = (title: string) => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
|
||||
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",
|
||||
})
|
||||
);
|
||||
updateNameAction(title);
|
||||
};
|
||||
|
||||
const createPage = async (payload: Partial<IPage>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
await pageService.createPage(workspaceSlug.toString(), projectId.toString(), payload);
|
||||
await createPageAction(workspaceSlug as string, projectId as string, payload);
|
||||
};
|
||||
|
||||
// ================ Page Menu Actions ==================
|
||||
@ -260,121 +174,84 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
||||
const currentPageValues = getValues();
|
||||
|
||||
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> = {
|
||||
name: "Copy of " + pageDetails?.name,
|
||||
name: "Copy of " + pageTitle,
|
||||
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 () => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
mutatePageDetailsHelper(
|
||||
pageService.archivePage(workspaceSlug.toString(), projectId.toString(), pageId.toString()),
|
||||
{
|
||||
archived_at: renderFormattedPayloadDate(new Date()),
|
||||
},
|
||||
["description_html"],
|
||||
() =>
|
||||
actionCompleteAlert({
|
||||
title: `Page could not be Archived`,
|
||||
message: `Sorry, page could not be Archived, please try again later`,
|
||||
type: "error",
|
||||
})
|
||||
);
|
||||
try {
|
||||
await archivePageAction(workspaceSlug as string, projectId as string, pageId as string);
|
||||
} catch (error) {
|
||||
actionCompleteAlert({
|
||||
title: `Page could not be archived`,
|
||||
message: `Sorry, page could not be archived, please try again later`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const unArchivePage = async () => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
|
||||
mutatePageDetailsHelper(
|
||||
pageService.restorePage(workspaceSlug.toString(), projectId.toString(), pageId.toString()),
|
||||
{
|
||||
archived_at: null,
|
||||
},
|
||||
["description_html"],
|
||||
() =>
|
||||
actionCompleteAlert({
|
||||
title: `Page could not be Restored`,
|
||||
message: `Sorry, page could not be Restored, please try again later`,
|
||||
type: "error",
|
||||
})
|
||||
);
|
||||
try {
|
||||
await restorePageAction(workspaceSlug as string, projectId as string, pageId as string);
|
||||
} catch (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 () => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
mutatePageDetailsHelper(
|
||||
pageService.lockPage(workspaceSlug.toString(), projectId.toString(), pageId.toString()),
|
||||
{
|
||||
is_locked: true,
|
||||
},
|
||||
["description_html"],
|
||||
() =>
|
||||
actionCompleteAlert({
|
||||
title: `Page cannot be Locked`,
|
||||
message: `Sorry, page cannot be Locked, please try again later`,
|
||||
type: "error",
|
||||
})
|
||||
);
|
||||
try {
|
||||
await lockPageAction();
|
||||
} catch (error) {
|
||||
actionCompleteAlert({
|
||||
title: `Page could not be locked`,
|
||||
message: `Sorry, page could not be locked, please try again later`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const unlockPage = async () => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
|
||||
mutatePageDetailsHelper(
|
||||
pageService.unlockPage(workspaceSlug.toString(), projectId.toString(), pageId.toString()),
|
||||
{
|
||||
is_locked: false,
|
||||
},
|
||||
["description_html"],
|
||||
() =>
|
||||
actionCompleteAlert({
|
||||
title: `Page could not be Unlocked`,
|
||||
message: `Sorry, page could not be Unlocked, please try again later`,
|
||||
type: "error",
|
||||
})
|
||||
);
|
||||
try {
|
||||
await unlockPageAction();
|
||||
} catch (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 =
|
||||
pageDetails?.is_locked ||
|
||||
pageDetails?.archived_at ||
|
||||
is_locked ||
|
||||
archived_at ||
|
||||
(currentProjectRole && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(currentProjectRole));
|
||||
|
||||
const isCurrentUserOwner = pageDetails?.owned_by === currentUser?.id;
|
||||
const isCurrentUserOwner = owned_by === currentUser?.id;
|
||||
|
||||
const userCanDuplicate =
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
@ -382,144 +259,132 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
||||
const userCanLock =
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<>
|
||||
{pageDetails && issuesResponse ? (
|
||||
<div className="flex h-full flex-col justify-between">
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
{isPageReadOnly ? (
|
||||
<DocumentReadOnlyEditorWithRef
|
||||
onActionCompleteHandler={actionCompleteAlert}
|
||||
ref={editorRef}
|
||||
value={localPageDescription.description_html}
|
||||
rerenderOnPropsChange={localPageDescription}
|
||||
customClassName={"tracking-tight w-full px-0"}
|
||||
borderOnFocus={false}
|
||||
noBorder
|
||||
documentDetails={{
|
||||
title: pageDetails.name,
|
||||
created_by: pageDetails.created_by,
|
||||
created_on: pageDetails.created_at,
|
||||
last_updated_at: pageDetails.updated_at,
|
||||
last_updated_by: pageDetails.updated_by,
|
||||
}}
|
||||
pageLockConfig={
|
||||
userCanLock && !pageDetails.archived_at
|
||||
? { action: unlockPage, is_locked: pageDetails.is_locked }
|
||||
: undefined
|
||||
}
|
||||
pageDuplicationConfig={
|
||||
userCanDuplicate && !pageDetails.archived_at ? { action: duplicate_page } : undefined
|
||||
}
|
||||
pageArchiveConfig={
|
||||
userCanArchive
|
||||
? {
|
||||
action: pageDetails.archived_at ? unArchivePage : archivePage,
|
||||
is_archived: pageDetails.archived_at ? true : false,
|
||||
archived_at: pageDetails.archived_at ? new Date(pageDetails.archived_at) : undefined,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
embedConfig={{
|
||||
issueEmbedConfig: {
|
||||
issues: issues,
|
||||
fetchIssue: fetchIssue,
|
||||
clickAction: issueWidgetClickAction,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { onChange } }) => (
|
||||
<DocumentEditorWithRef
|
||||
isSubmitting={isSubmitting}
|
||||
documentDetails={{
|
||||
title: pageDetails.name,
|
||||
created_by: pageDetails.created_by,
|
||||
created_on: pageDetails.created_at,
|
||||
last_updated_at: pageDetails.updated_at,
|
||||
last_updated_by: pageDetails.updated_by,
|
||||
}}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
setShouldShowAlert={setShowAlert}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
ref={editorRef}
|
||||
debouncedUpdatesEnabled={false}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
updatePageTitle={updatePageTitle}
|
||||
value={localPageDescription.description_html}
|
||||
rerenderOnPropsChange={localPageDescription}
|
||||
onActionCompleteHandler={actionCompleteAlert}
|
||||
customClassName="tracking-tight self-center px-0 h-full w-full"
|
||||
onChange={(_description_json: Object, description_html: string) => {
|
||||
setShowAlert(true);
|
||||
onChange(description_html);
|
||||
setIsSubmitting("submitting");
|
||||
debouncedFormSave();
|
||||
}}
|
||||
duplicationConfig={userCanDuplicate ? { action: duplicate_page } : undefined}
|
||||
pageArchiveConfig={
|
||||
userCanArchive
|
||||
? {
|
||||
is_archived: pageDetails.archived_at ? true : false,
|
||||
action: pageDetails.archived_at ? unArchivePage : archivePage,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
pageLockConfig={userCanLock ? { is_locked: false, action: lockPage } : undefined}
|
||||
embedConfig={{
|
||||
issueEmbedConfig: {
|
||||
issues: issues,
|
||||
fetchIssue: fetchIssue,
|
||||
clickAction: issueWidgetClickAction,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
return pageIdMobx && issues ? (
|
||||
<div className="flex h-full flex-col justify-between">
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
{isPageReadOnly ? (
|
||||
<DocumentReadOnlyEditorWithRef
|
||||
onActionCompleteHandler={actionCompleteAlert}
|
||||
ref={editorRef}
|
||||
value={pageDescription}
|
||||
customClassName={"tracking-tight w-full px-0"}
|
||||
borderOnFocus={false}
|
||||
noBorder
|
||||
documentDetails={{
|
||||
title: pageTitle,
|
||||
created_by: created_by,
|
||||
created_on: created_at,
|
||||
last_updated_at: updated_at,
|
||||
last_updated_by: updated_by,
|
||||
}}
|
||||
pageLockConfig={userCanLock && !archived_at ? { action: unlockPage, is_locked: is_locked } : undefined}
|
||||
pageDuplicationConfig={userCanDuplicate && !archived_at ? { action: duplicate_page } : undefined}
|
||||
pageArchiveConfig={
|
||||
userCanArchive
|
||||
? {
|
||||
action: archived_at ? unArchivePage : archivePage,
|
||||
is_archived: archived_at ? true : false,
|
||||
archived_at: archived_at ? new Date(archived_at) : undefined,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
embedConfig={{
|
||||
issueEmbedConfig: {
|
||||
issues: issues,
|
||||
fetchIssue: fetchIssue,
|
||||
clickAction: issueWidgetClickAction,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { onChange } }) => (
|
||||
<DocumentEditorWithRef
|
||||
isSubmitting={isSubmitting}
|
||||
documentDetails={{
|
||||
title: pageTitle,
|
||||
created_by: created_by,
|
||||
created_on: created_at,
|
||||
last_updated_at: updated_at,
|
||||
last_updated_by: updated_by,
|
||||
}}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
value={pageDescription}
|
||||
setShouldShowAlert={setShowAlert}
|
||||
deleteFile={fileService.deleteImage}
|
||||
restoreFile={fileService.restoreImage}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
ref={editorRef}
|
||||
debouncedUpdatesEnabled={false}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
updatePageTitle={updatePageTitle}
|
||||
onActionCompleteHandler={actionCompleteAlert}
|
||||
customClassName="tracking-tight self-center px-0 h-full w-full"
|
||||
onChange={(_description_json: Object, description_html: string) => {
|
||||
setShowAlert(true);
|
||||
onChange(description_html);
|
||||
handleSubmit(updatePage)();
|
||||
}}
|
||||
duplicationConfig={userCanDuplicate ? { action: duplicate_page } : undefined}
|
||||
pageArchiveConfig={
|
||||
userCanArchive
|
||||
? {
|
||||
is_archived: archived_at ? true : false,
|
||||
action: archived_at ? unArchivePage : archivePage,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
pageLockConfig={userCanLock ? { is_locked: false, action: lockPage } : undefined}
|
||||
embedConfig={{
|
||||
issueEmbedConfig: {
|
||||
issues: issues,
|
||||
fetchIssue: fetchIssue,
|
||||
clickAction: issueWidgetClickAction,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{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]"
|
||||
/>
|
||||
{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 className="grid h-full w-full place-items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<IssuePeekOverview />
|
||||
</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 { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { usePage, useUser } from "hooks/store";
|
||||
import { useUser } from "hooks/store";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// layouts
|
||||
@ -17,6 +17,7 @@ import { PagesHeader } from "components/headers";
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
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), {
|
||||
ssr: false,
|
||||
@ -45,8 +46,9 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
||||
// states
|
||||
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
|
||||
// store
|
||||
const { fetchProjectPages, fetchArchivedProjectPages } = usePage();
|
||||
const { currentUser, currentUserLoader } = useUser();
|
||||
|
||||
const { fetchProjectPages, fetchArchivedProjectPages } = useProjectPages();
|
||||
// hooks
|
||||
const {} = useUserAuth({ user: currentUser, isLoading: currentUserLoader });
|
||||
// local storage
|
||||
|
@ -1,374 +1,277 @@
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import set from "lodash/set";
|
||||
import omit from "lodash/omit";
|
||||
import isToday from "date-fns/isToday";
|
||||
import isThisWeek from "date-fns/isThisWeek";
|
||||
import isYesterday from "date-fns/isYesterday";
|
||||
// services
|
||||
import { action, makeObservable, observable, reaction, runInAction } from "mobx";
|
||||
|
||||
import { IIssueLabel, IPage } from "@plane/types";
|
||||
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";
|
||||
|
||||
export interface IPageStore {
|
||||
pages: Record<string, IPage>;
|
||||
archivedPages: Record<string, IPage>;
|
||||
// project computed
|
||||
projectPageIds: string[] | null;
|
||||
favoriteProjectPageIds: string[] | null;
|
||||
privateProjectPageIds: string[] | null;
|
||||
publicProjectPageIds: string[] | null;
|
||||
archivedProjectPageIds: string[] | null;
|
||||
recentProjectPages: IRecentPages | null;
|
||||
// fetch page information actions
|
||||
getUnArchivedPageById: (pageId: string) => IPage | null;
|
||||
getArchivedPageById: (pageId: string) => IPage | null;
|
||||
// fetch actions
|
||||
fetchProjectPages: (workspaceSlug: string, projectId: string) => Promise<IPage[]>;
|
||||
fetchArchivedProjectPages: (workspaceSlug: string, projectId: string) => Promise<IPage[]>;
|
||||
// favorites actions
|
||||
addToFavorites: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||
removeFromFavorites: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||
// crud
|
||||
createPage: (workspaceSlug: string, projectId: string, data: Partial<IPage>) => Promise<IPage>;
|
||||
updatePage: (workspaceSlug: string, projectId: string, pageId: string, data: Partial<IPage>) => Promise<IPage>;
|
||||
deletePage: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||
// access control actions
|
||||
makePublic: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||
makePrivate: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||
// archive actions
|
||||
archivePage: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||
restorePage: (workspaceSlug: string, projectId: string, pageId: string) => Promise<void>;
|
||||
// Page Properties
|
||||
access: number;
|
||||
archived_at: string | null;
|
||||
color: string;
|
||||
created_at: Date;
|
||||
created_by: string;
|
||||
description: string;
|
||||
description_html: string;
|
||||
description_stripped: string | null;
|
||||
id: string;
|
||||
is_favorite: boolean;
|
||||
label_details: IIssueLabel[];
|
||||
is_locked: boolean;
|
||||
labels: string[];
|
||||
name: string;
|
||||
owned_by: string;
|
||||
project: string;
|
||||
updated_at: Date;
|
||||
updated_by: string;
|
||||
workspace: string;
|
||||
|
||||
// Actions
|
||||
makePublic: () => Promise<void>;
|
||||
makePrivate: () => Promise<void>;
|
||||
lockPage: () => Promise<void>;
|
||||
unlockPage: () => Promise<void>;
|
||||
addToFavorites: () => 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 {
|
||||
pages: Record<string, IPage> = {};
|
||||
archivedPages: Record<string, IPage> = {};
|
||||
// services
|
||||
access = 0;
|
||||
isSubmitting: "submitting" | "submitted" | "saved" = "saved";
|
||||
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;
|
||||
// stores
|
||||
// root store
|
||||
rootStore;
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
constructor(page: IPage, _rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
pages: observable,
|
||||
archivedPages: observable,
|
||||
// computed
|
||||
projectPageIds: computed,
|
||||
favoriteProjectPageIds: computed,
|
||||
publicProjectPageIds: computed,
|
||||
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
|
||||
name: observable.ref,
|
||||
description_html: observable.ref,
|
||||
is_favorite: observable.ref,
|
||||
is_locked: observable.ref,
|
||||
isSubmitting: observable.ref,
|
||||
access: observable.ref,
|
||||
|
||||
makePublic: action,
|
||||
makePrivate: action,
|
||||
// archive actions
|
||||
archivePage: action,
|
||||
restorePage: action,
|
||||
addToFavorites: action,
|
||||
removeFromFavorites: action,
|
||||
updateName: action,
|
||||
updateDescription: action,
|
||||
setIsSubmitting: action,
|
||||
cleanup: action,
|
||||
});
|
||||
// stores
|
||||
this.rootStore = rootStore;
|
||||
// services
|
||||
this.created_by = page?.created_by || "";
|
||||
this.created_at = page?.created_at || new Date();
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves all pages for a projectId that is available in the url.
|
||||
*/
|
||||
get projectPageIds() {
|
||||
const projectId = this.rootStore.app.router.projectId;
|
||||
if (!projectId) return null;
|
||||
const projectPageIds = Object.keys(this.pages).filter((pageId) => this.pages?.[pageId]?.project === projectId);
|
||||
return projectPageIds ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
const descriptionDisposer = reaction(
|
||||
() => this.description_html,
|
||||
(description_html) => {
|
||||
//TODO: Fix reaction to only run when the data is changed, not when the page is loaded
|
||||
const { projectId, workspaceSlug } = this.rootStore.app.router;
|
||||
if (!projectId || !workspaceSlug) return;
|
||||
this.isSubmitting = "submitting";
|
||||
this.pageService.patchPage(workspaceSlug, projectId, this.id, { description_html }).finally(() => {
|
||||
runInAction(() => {
|
||||
this.isSubmitting = "submitted";
|
||||
});
|
||||
});
|
||||
return response;
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
},
|
||||
{ delay: 3000 }
|
||||
);
|
||||
|
||||
/**
|
||||
* fetches all archived pages for a project.
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @returns Promise<IPage[]>
|
||||
*/
|
||||
fetchArchivedProjectPages = async (workspaceSlug: string, projectId: string) =>
|
||||
await this.pageService.getArchivedPages(workspaceSlug, projectId).then((response) => {
|
||||
runInAction(() => {
|
||||
response.forEach((page) => {
|
||||
set(this.archivedPages, [page.id], page);
|
||||
});
|
||||
});
|
||||
return response;
|
||||
const pageTitleDisposer = reaction(
|
||||
() => this.name,
|
||||
(name) => {
|
||||
const { projectId, workspaceSlug } = this.rootStore.app.router;
|
||||
if (!projectId || !workspaceSlug) return;
|
||||
this.isSubmitting = "submitting";
|
||||
this.pageService
|
||||
.patchPage(workspaceSlug, projectId, this.id, { name })
|
||||
.catch(() => {
|
||||
runInAction(() => {
|
||||
this.name = this.oldName;
|
||||
});
|
||||
})
|
||||
.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
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param pageId
|
||||
*/
|
||||
addToFavorites = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||
try {
|
||||
addToFavorites = action("addToFavorites", async () => {
|
||||
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(() => {
|
||||
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
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param pageId
|
||||
*/
|
||||
removeFromFavorites = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
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;
|
||||
});
|
||||
removeFromFavorites = action("removeFromFavorites", async () => {
|
||||
const { projectId, workspaceSlug } = this.rootStore.app.router;
|
||||
if (!projectId || !workspaceSlug) return;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
});
|
||||
this.is_favorite = false;
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
await this.pageService.removePageFromFavorites(workspaceSlug, projectId, this.id).catch(() => {
|
||||
runInAction(() => {
|
||||
omit(this.archivedPages, [pageId]);
|
||||
this.is_favorite = true;
|
||||
});
|
||||
return response;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* make a page public
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param pageId
|
||||
* @returns
|
||||
*/
|
||||
makePublic = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||
try {
|
||||
makePublic = action("makePublic", async () => {
|
||||
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(() => {
|
||||
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
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param pageId
|
||||
* @returns
|
||||
*/
|
||||
makePrivate = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
makePrivate = action("makePrivate", async () => {
|
||||
const { projectId, workspaceSlug } = this.rootStore.app.router;
|
||||
if (!projectId || !workspaceSlug) return;
|
||||
|
||||
/**
|
||||
* Mark a page archived
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param pageId
|
||||
*/
|
||||
archivePage = async (workspaceSlug: string, projectId: string, pageId: string) =>
|
||||
await this.pageService.archivePage(workspaceSlug, projectId, pageId).then(() => {
|
||||
this.access = 1;
|
||||
|
||||
this.pageService.patchPage(workspaceSlug, projectId, this.id, { access: 1 }).catch(() => {
|
||||
runInAction(() => {
|
||||
set(this.archivedPages, [pageId], this.pages[pageId]);
|
||||
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]);
|
||||
this.access = 0;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,33 +1,54 @@
|
||||
import { makeObservable, observable, runInAction, action } from "mobx";
|
||||
import { makeObservable, observable, runInAction, action, computed } from "mobx";
|
||||
import { set } from "lodash";
|
||||
// services
|
||||
import { PageService } from "services/page.service";
|
||||
// store
|
||||
import { PageStore, IPageStore } from "store/page.store";
|
||||
// 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 {
|
||||
projectPages: Record<string, IPageStore[]>;
|
||||
projectArchivedPages: Record<string, IPageStore[]>;
|
||||
projectPageMap: Record<string, 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
|
||||
fetchProjectPages: (workspaceSlug: string, projectId: string) => void;
|
||||
fetchArchivedProjectPages: (workspaceSlug: string, projectId: string) => void;
|
||||
fetchProjectPages: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
fetchArchivedProjectPages: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
// crud actions
|
||||
createPage: (workspaceSlug: string, projectId: string, data: Partial<IPage>) => void;
|
||||
deletePage: (workspaceSlug: string, projectId: string, pageId: string) => void;
|
||||
createPage: (workspaceSlug: string, projectId: string, data: Partial<IPage>) => Promise<IPage>;
|
||||
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 {
|
||||
projectPages: Record<string, IPageStore[]> = {}; // { projectId: [page1, page2] }
|
||||
projectArchivedPages: Record<string, IPageStore[]> = {}; // { projectId: [page1, page2] }
|
||||
projectPageMap: Record<string, Record<string, IPageStore>> = {}; // { projectId: [page1, page2] }
|
||||
projectArchivedPageMap: Record<string, Record<string, IPageStore>> = {}; // { projectId: [page1, page2] }
|
||||
|
||||
// root store
|
||||
rootStore;
|
||||
|
||||
pageService;
|
||||
|
||||
constructor() {
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
projectPages: observable,
|
||||
projectArchivedPages: observable,
|
||||
projectPageMap: observable,
|
||||
projectArchivedPageMap: observable,
|
||||
|
||||
projectPageIds: computed,
|
||||
archivedPageIds: computed,
|
||||
favoriteProjectPageIds: computed,
|
||||
privateProjectPageIds: computed,
|
||||
publicProjectPageIds: computed,
|
||||
recentProjectPages: computed,
|
||||
|
||||
// fetch actions
|
||||
fetchProjectPages: action,
|
||||
fetchArchivedProjectPages: action,
|
||||
@ -35,19 +56,113 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
createPage: action,
|
||||
deletePage: action,
|
||||
});
|
||||
this.rootStore = _rootStore;
|
||||
|
||||
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
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
*/
|
||||
fetchProjectPages = async (workspaceSlug: string, projectId: string) => {
|
||||
const response = await this.pageService.getProjectPages(workspaceSlug, projectId);
|
||||
runInAction(() => {
|
||||
this.projectPages[projectId] = response?.map((page) => new PageStore(page as any));
|
||||
});
|
||||
try {
|
||||
await this.pageService.getProjectPages(workspaceSlug, projectId).then((response) => {
|
||||
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
|
||||
* @returns Promise<IPage[]>
|
||||
*/
|
||||
fetchArchivedProjectPages = async (workspaceSlug: string, projectId: string) =>
|
||||
await this.pageService.getArchivedPages(workspaceSlug, projectId).then((response) => {
|
||||
runInAction(() => {
|
||||
this.projectArchivedPages[projectId] = response?.map((page) => new PageStore(page as any));
|
||||
fetchArchivedProjectPages = async (workspaceSlug: string, projectId: string) => {
|
||||
try {
|
||||
await this.pageService.getArchivedPages(workspaceSlug, projectId).then((response) => {
|
||||
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
|
||||
@ -73,7 +195,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
createPage = async (workspaceSlug: string, projectId: string, data: Partial<IPage>) => {
|
||||
const response = await this.pageService.createPage(workspaceSlug, projectId, data);
|
||||
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;
|
||||
};
|
||||
@ -88,11 +210,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
deletePage = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||
const response = await this.pageService.deletePage(workspaceSlug, projectId, pageId);
|
||||
runInAction(() => {
|
||||
this.projectPages = set(
|
||||
this.projectPages,
|
||||
[projectId],
|
||||
this.projectPages[projectId].filter((page: any) => page.id !== pageId)
|
||||
);
|
||||
delete this.projectArchivedPageMap[projectId][pageId];
|
||||
});
|
||||
return response;
|
||||
};
|
||||
@ -104,14 +222,17 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
* @param pageId
|
||||
*/
|
||||
archivePage = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||
const response = await this.pageService.archivePage(workspaceSlug, projectId, pageId);
|
||||
runInAction(() => {
|
||||
set(
|
||||
this.projectPages,
|
||||
[projectId],
|
||||
this.projectPages[projectId].filter((page: any) => page.id !== pageId)
|
||||
);
|
||||
this.projectArchivedPages = set(this.projectArchivedPages, [projectId], this.projectArchivedPages[projectId]);
|
||||
set(this.projectArchivedPageMap, [projectId, pageId], this.projectPageMap[projectId][pageId]);
|
||||
set(this.projectArchivedPageMap[projectId][pageId], "archived_at", new Date().toISOString());
|
||||
delete this.projectPageMap[projectId][pageId];
|
||||
});
|
||||
const response = await this.pageService.archivePage(workspaceSlug, projectId, pageId).catch(() => {
|
||||
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;
|
||||
};
|
||||
@ -122,15 +243,19 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
* @param projectId
|
||||
* @param pageId
|
||||
*/
|
||||
restorePage = async (workspaceSlug: string, projectId: string, pageId: string) =>
|
||||
await this.pageService.restorePage(workspaceSlug, projectId, pageId).then(() => {
|
||||
restorePage = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||
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(() => {
|
||||
set(
|
||||
this.projectArchivedPages,
|
||||
[projectId],
|
||||
this.projectArchivedPages[projectId].filter((page: any) => page.id !== pageId)
|
||||
);
|
||||
set(this.projectPages, [projectId], [...this.projectPages[projectId]]);
|
||||
set(this.projectArchivedPageMap, [projectId, pageId], this.projectPageMap[projectId][pageId]);
|
||||
set(this.projectArchivedPageMap[projectId][pageId], "archived_at", pageArchivedAt);
|
||||
delete this.projectPageMap[projectId][pageId];
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import { IUserRootStore, UserRootStore } from "./user";
|
||||
import { IWorkspaceRootStore, WorkspaceRootStore } from "./workspace";
|
||||
import { IssueRootStore, IIssueRootStore } from "./issue/root.store";
|
||||
import { IStateStore, StateStore } from "./state.store";
|
||||
import { IPageStore, PageStore } from "./page.store";
|
||||
import { ILabelRootStore, LabelRootStore } from "./label";
|
||||
import { IMemberRootStore, MemberRootStore } from "./member";
|
||||
import { IInboxRootStore, InboxRootStore } from "./inbox";
|
||||
@ -33,7 +32,6 @@ export class RootStore {
|
||||
module: IModuleStore;
|
||||
projectView: IProjectViewStore;
|
||||
globalView: IGlobalViewStore;
|
||||
page: IPageStore;
|
||||
issue: IIssueRootStore;
|
||||
state: IStateStore;
|
||||
estimate: IEstimateStore;
|
||||
@ -58,8 +56,7 @@ export class RootStore {
|
||||
this.state = new StateStore(this);
|
||||
this.estimate = new EstimateStore(this);
|
||||
this.mention = new MentionStore(this);
|
||||
this.projectPages = new ProjectPageStore(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:
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-4.0.5.tgz#e2cb98f813e118917bcc463638f5bf6ea053a67b"
|
||||
integrity sha512-StfB2wxE8imKj1f6T8WWPf4lVMx3cYH9Iy60bbKXEs21+HQ4tvvfIBZfSmMXgQAefi8xYEwQIz4GN9s0d2h7dg==
|
||||
dependencies:
|
||||
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:
|
||||
version "6.12.0"
|
||||
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.12.0.tgz#72b2685ca5af031aaa49e77a4d76ed67fcbf9135"
|
||||
|
Loading…
Reference in New Issue
Block a user