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
32 changed files with 960 additions and 1100 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue