[WEB-460] refactor: editors, chore: pages list improvement (#4090)
* fix: stroing the transactions in page * fix: page details changes * chore: page response change * chore: removed duplicated endpoints * chore: optimised the urls * chore: removed archived and favorite pages * chore: revamping pages store and components * mentions loading state part done * fixed mentions not showing in modals * removed comments and cleaned up types * removed unused types * reset: head * chore: pages store and component updates * style: pages list item UI * fix: improved colors and drag handle width * fix: slash commands are no more shown in the code blocks * fix: cleanup/hide drag handles post drop * fix: hide/cleanup drag handles post drag start * fix: aligning the drag handles better with the node post css changes of the length * fix: juggling back and forth of drag handles in ordered and unordered lists * chore: fix imports, ts errors and other things * fix: clearing nodes to default node i.e paragraph before converting it to other types of nodes For more reference on what this does, please refer https://tiptap.dev/docs/editor/api/commands/clear-nodes * chore: clearNodes after delete in case of selections being present * fix: hiding link selector in the bubble menu if inline code block is selected * chore: filtering, ordering and searching implemented * chore: updated pages store and updated UI * chore: new core editor just for document editor created * chore: removed setIsSubmitting prop in doc editor * fix: fixed submitting state for image uploads * refactor: setShouldShowAlert removed * refactor: rerenderOnPropsChange prop removed * chore: type inference magic in ref to expose an api for controlling editor menu items from outside * fix: naming imports * chore: change names of the exposed functions and removing old types * refactor: remove debouncedUpdatesEnabled prop; * refactor: editor heading markings now parsed using html * chore: removed unrelated components from the document editor * refactor: page details granular components * fix: remove onActionCompleteHandler * refactor: removed rerenderOnProps change prop * feat: added getMarkDown function * chore: update dropdown option actions * fix: sidebar markings update logic * chore: add image and to-do list actions to the toolbar * fix: handling refs and populating them via callbacks * feat: scroll to node api exposed * cleaning up editor refs when the editor is destroyed * feat: scrolling added to read only instance of the editor * fix: markings logic * fix: build errors with types * fix: build erros * fix: subscribing to transactions of editor via ref * chore: remove debug statements * fix: type errors * fix: temporary different slash commands for document editor * chore: inline code extension style * chore: remove border from readOnly editor * fix: editor bottom padding * chore: pages improvements * chore: handle Enter key on the page title * feat: added loading indicator logic in mentions * fix: mentions and slash commands now work well with multiple editors in one place * refactor: page store structure, filtering logic * feat: added better seperation in inline code blocks * feat: list autojoining added * fix: pages folder structure * fix: image refocus from external parts * working lists somewhat * chore: implement page reactions * fix: build errors * fix: build errors * fixed drag handles stuff * task list item fixed * working * fix: working on multiple nested lists * chore: remove debug statements * fix: Tab key on first list item handled to not go out of editor focus * feat: threshold auto scroll support added and multi nested list selection fixed * fix: caret color bug with improved inline code blocks * fix: node range error when bulk deleting with list * fix: removed slash commands from working in code blocks * chore: update typography margins * chore: new field added in page model * fix: better type inference in slash commands * chore: code block UI * feat: image insertion at correct position using ref added * feat: added improved mentions support for space * fix: type errors in mentions for comments in web app * sync: core with document-core * fix: build errors * fix: fallback for appendTo not being able to find active container instantly * fix: page store * fix: page description * fix: css quality issues * chore: code cleanup * chore: removed placeholder text in codeblocks * chore: archived pages response change * chore: archived pages response change * fix: initial pages list fetch * fix: pages list filters and ordering * chore: add access change option in the quick actions dropdown * fix: inline code block caret fixed * regression: removing extra text * chore: caret color removed * feat: copy code button added in code blocks * fix: initial load of page details * fix: initial load of page details * fix: image resizing weird behavior on click/expanding it too much fixed now * chore: copy page response * fix: todo list spacing * chore: description html in the copy page * chore: handle latest description on refetch * fix: saner scroll behaviours * fix: block menu positioning * fix: updated empty string description * feat: tab change sync support added * fix: infinite rerendering with markings * fix: block menu finally * fix: intial load on reload bug fixed * fix: nested lists alignment * fix: editor padding * fix: first level list items copyable * chore: list spacing * fix: title change * fix: pages list block items interaction * fix: saving chip position * fix: delete action from block menu to focus properly * fix: margin-bottom as 0 to avoid weird spacing when a paragraph node follows a list node * style: table, chore: lite text editor toolbar * fix: page description tab sync * fix: lists spacing and alignment * refactor: document editor props * feat: rich text editor wrapper created and migrated core * feat: created wrapper around lite text editor and merged core * chore: add lite text editor toolbar * fix: build errors * fix: type errors and addead live updation of toolbar * chore: pages migration * fix: inbox issue * refactor: remove redundant package * refactor: unused files * fix: add dompurify to space app * fix: inline code margin * fix: editor className props * fix: build errors * fix: traversing up the tree before assuming the parent is not a list item * fix: drag handle positions for list items fixed * fix: removed focus at end logic after deleting block * fix: image wrapper overflow scroll fix with block menu's position * fix: selection and deletion logic for nested lists fixed!! * fix: hiding the block menu while scrolling in the document/app * fix: merge conflicts resolved from develop * fix: inbox issue description * chore: move page title to the web app * fix: handling edge cases for table selection * chore: lint issues * refactor: list item functions moved to same file * refactor: use mention hook * fix: added try catch blocks for mention suggestions * chore: remove unused code * fix: remove console logs * fix: remove console logs --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> Co-authored-by: gurusainath <gurusainath007@gmail.com> Co-authored-by: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com>
This commit is contained in:
parent
8b6035d315
commit
3e2355e223
248 changed files with 7602 additions and 5619 deletions
|
|
@ -39,7 +39,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
|
|||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
|
||||
const { isLoading } = useSWR(
|
||||
const { isLoading, data: swrArchivedIssueDetails } = useSWR(
|
||||
workspaceSlug && projectId && archivedIssueId
|
||||
? `ARCHIVED_ISSUE_DETAIL_${workspaceSlug}_${projectId}_${archivedIssueId}`
|
||||
: null,
|
||||
|
|
@ -123,6 +123,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
|
|||
)}
|
||||
{workspaceSlug && projectId && archivedIssueId && (
|
||||
<IssueDetailRoot
|
||||
swrIssueDetails={swrArchivedIssueDetails}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
issueId={archivedIssueId.toString()}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
|
|||
const { getProjectById } = useProject();
|
||||
const { theme: themeStore } = useApplication();
|
||||
// fetching issue details
|
||||
const { isLoading } = useSWR(
|
||||
const { isLoading, data: swrIssueDetails } = useSWR(
|
||||
workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null,
|
||||
workspaceSlug && projectId && issueId
|
||||
? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
||||
|
|
@ -77,6 +77,7 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
|
|||
projectId &&
|
||||
issueId && (
|
||||
<IssueDetailRoot
|
||||
swrIssueDetails={swrIssueDetails}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
issueId={issueId.toString()}
|
||||
|
|
|
|||
|
|
@ -1,370 +1,149 @@
|
|||
import { ReactElement, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import useSWR from "swr";
|
||||
import { Sparkle } from "lucide-react";
|
||||
import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor";
|
||||
import { IPage } from "@plane/types";
|
||||
// hooks
|
||||
|
||||
import { Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { GptAssistantPopover, PageHead } from "@/components/core";
|
||||
import { PageDetailsHeader } from "@/components/headers/page-details";
|
||||
import { IssuePeekOverview } from "@/components/issues";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
import { useApplication, usePage, useUser, useWorkspace } from "@/hooks/store";
|
||||
import { useProjectPages } from "@/hooks/store/use-project-specific-pages";
|
||||
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
||||
// services
|
||||
import { AppLayout } from "@/layouts/app-layout";
|
||||
import { NextPageWithLayout } from "@/lib/types";
|
||||
import { FileService } from "@/services/file.service";
|
||||
// layouts
|
||||
// components
|
||||
// ui
|
||||
// assets
|
||||
// helpers
|
||||
// document-editor
|
||||
import { EditorRefApi, useEditorMarkings } from "@plane/document-editor";
|
||||
// types
|
||||
// fetch-keys
|
||||
// constants
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
import { TPage } from "@plane/types";
|
||||
// ui
|
||||
import { Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
import { PageDetailsHeader } from "@/components/headers";
|
||||
import { IssuePeekOverview } from "@/components/issues";
|
||||
import { PageEditorBody, PageEditorHeaderRoot } from "@/components/pages";
|
||||
// hooks
|
||||
import { usePage, useProjectPages } from "@/hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "@/layouts/app-layout";
|
||||
// lib
|
||||
import { NextPageWithLayout } from "@/lib/types";
|
||||
|
||||
const PageDetailsPage: NextPageWithLayout = observer(() => {
|
||||
// states
|
||||
const [gptModalOpen, setGptModal] = useState(false);
|
||||
const [sidePeekVisible, setSidePeekVisible] = useState(true);
|
||||
const [editorReady, setEditorReady] = useState(false);
|
||||
const [readOnlyEditorReady, setReadOnlyEditorReady] = useState(false);
|
||||
// refs
|
||||
const editorRef = useRef<any>(null);
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
const readOnlyEditorRef = useRef<EditorRefApi>(null);
|
||||
// router
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaceSlug, projectId, pageId } = router.query;
|
||||
const workspaceStore = useWorkspace();
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string;
|
||||
|
||||
// store hooks
|
||||
const {
|
||||
config: { envConfig },
|
||||
} = useApplication();
|
||||
const {
|
||||
currentUser,
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
|
||||
const { handleSubmit, getValues, control, reset } = useForm<IPage>({
|
||||
defaultValues: { name: "", description_html: "" },
|
||||
const { createPage, getPageById } = useProjectPages(projectId?.toString() ?? "");
|
||||
const pageStore = usePage(pageId?.toString() ?? "");
|
||||
// editor markings hook
|
||||
const { markings, updateMarkings } = useEditorMarkings();
|
||||
// form info
|
||||
const { handleSubmit, getValues, control } = useForm<TPage>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description_html: "",
|
||||
},
|
||||
});
|
||||
|
||||
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 page details
|
||||
const { data: swrPageDetails } = useSWR(
|
||||
pageId ? `PAGE_DETAILS_${pageId}` : null,
|
||||
pageId ? () => getPageById(pageId.toString()) : null,
|
||||
{
|
||||
revalidateIfStale: false,
|
||||
revalidateOnFocus: true,
|
||||
revalidateOnReconnect: true,
|
||||
}
|
||||
);
|
||||
// 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 pageStore = usePage(pageId as string);
|
||||
|
||||
const { setShowAlert } = useReloadConfirmations(pageStore?.isSubmitting === "submitting");
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (pageStore) {
|
||||
pageStore.cleanup();
|
||||
}
|
||||
if (pageStore.cleanup) pageStore.cleanup();
|
||||
},
|
||||
[pageStore]
|
||||
);
|
||||
|
||||
if (!pageStore) {
|
||||
const handleEditorReady = (value: boolean) => setEditorReady(value);
|
||||
|
||||
const handleReadOnlyEditorReady = () => setReadOnlyEditorReady(true);
|
||||
|
||||
if (!pageStore || !pageStore.id)
|
||||
return (
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
<div className="h-full w-full grid 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
|
||||
// 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;
|
||||
updateDescriptionAction(formData.description_html);
|
||||
const handleCreatePage = async (payload: Partial<TPage>) => await createPage(payload);
|
||||
|
||||
const handleUpdatePage = async (formData: TPage) => {
|
||||
let updatedDescription = formData.description_html;
|
||||
if (!updatedDescription || updatedDescription.trim() === "") updatedDescription = "<p></p>";
|
||||
pageStore.updateDescription(updatedDescription);
|
||||
};
|
||||
|
||||
const handleAiAssistance = async (response: string) => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
|
||||
editorRef.current?.setEditorValueAtCursorPosition(response);
|
||||
};
|
||||
|
||||
const actionCompleteAlert = ({
|
||||
title,
|
||||
message,
|
||||
type,
|
||||
}: {
|
||||
title: string;
|
||||
message: string;
|
||||
type: "success" | "error" | "warning" | "info";
|
||||
}) => {
|
||||
setToast({
|
||||
title,
|
||||
message,
|
||||
type: type as TOAST_TYPE,
|
||||
});
|
||||
};
|
||||
|
||||
const updatePageTitle = (title: string) => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
updateNameAction(title);
|
||||
};
|
||||
|
||||
const createPage = async (payload: Partial<IPage>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
await createPageAction(workspaceSlug as string, projectId as string, payload);
|
||||
};
|
||||
|
||||
// ================ Page Menu Actions ==================
|
||||
const duplicate_page = async () => {
|
||||
const handleDuplicatePage = async () => {
|
||||
const currentPageValues = getValues();
|
||||
|
||||
if (!currentPageValues?.description_html) {
|
||||
// TODO: We need to get latest data the above variable will give us stale data
|
||||
currentPageValues.description_html = pageDescription as string;
|
||||
currentPageValues.description_html = pageStore.description_html;
|
||||
}
|
||||
|
||||
const formData: Partial<IPage> = {
|
||||
name: "Copy of " + pageTitle,
|
||||
const formData: Partial<TPage> = {
|
||||
name: "Copy of " + pageStore.name,
|
||||
description_html: currentPageValues.description_html,
|
||||
};
|
||||
|
||||
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",
|
||||
});
|
||||
}
|
||||
await handleCreatePage(formData)
|
||||
.then((res) => router.push(`/${workspaceSlug}/projects/${projectId}/pages/${res?.id}`))
|
||||
.catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Page could not be duplicated. Please try again later.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const archivePage = async () => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
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;
|
||||
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",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const lockPage = async () => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
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;
|
||||
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 isPageReadOnly =
|
||||
is_locked ||
|
||||
archived_at ||
|
||||
(currentProjectRole && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(currentProjectRole));
|
||||
|
||||
const isCurrentUserOwner = owned_by === currentUser?.id;
|
||||
|
||||
const userCanDuplicate =
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
const userCanArchive = isCurrentUserOwner || currentProjectRole === EUserProjectRoles.ADMIN;
|
||||
const userCanLock =
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return pageIdMobx ? (
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<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: getDate(created_at) ?? new Date(created_at ?? ""),
|
||||
last_updated_at: getDate(updated_at) ?? new Date(created_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: getDate(archived_at),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
<div className="h-full w-full flex-shrink-0 flex flex-col overflow-hidden">
|
||||
{projectId && (
|
||||
<PageEditorHeaderRoot
|
||||
editorRef={editorRef}
|
||||
readOnlyEditorRef={readOnlyEditorRef}
|
||||
editorReady={editorReady}
|
||||
readOnlyEditorReady={readOnlyEditorReady}
|
||||
handleDuplicatePage={handleDuplicatePage}
|
||||
markings={markings}
|
||||
pageStore={pageStore}
|
||||
projectId={projectId.toString()}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
setSidePeekVisible={(state) => setSidePeekVisible(state)}
|
||||
/>
|
||||
) : (
|
||||
<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: getDate(created_at) ?? new Date(created_at ?? ""),
|
||||
last_updated_at: getDate(updated_at) ?? new Date(created_at ?? ""),
|
||||
last_updated_by: updated_by,
|
||||
}}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
||||
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
||||
value={pageDescription}
|
||||
setShouldShowAlert={setShowAlert}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
ref={editorRef}
|
||||
debouncedUpdatesEnabled={false}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
updatePageTitle={updatePageTitle}
|
||||
onActionCompleteHandler={actionCompleteAlert}
|
||||
customClassName="tracking-tight self-center h-full w-full right-[0.675rem]"
|
||||
onChange={(_description_json: any, 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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{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>
|
||||
)}
|
||||
<PageEditorBody
|
||||
swrPageDetails={swrPageDetails}
|
||||
control={control}
|
||||
editorRef={editorRef}
|
||||
handleEditorReady={handleEditorReady}
|
||||
readOnlyEditorRef={readOnlyEditorRef}
|
||||
handleReadOnlyEditorReady={handleReadOnlyEditorReady}
|
||||
handleSubmit={() => handleSubmit(handleUpdatePage)()}
|
||||
markings={markings}
|
||||
pageStore={pageStore}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
updateMarkings={updateMarkings}
|
||||
/>
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,231 +1,43 @@
|
|||
import { useState, Fragment, ReactElement } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import dynamic from "next/dynamic";
|
||||
import { ReactElement } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { Tab } from "@headlessui/react";
|
||||
// hooks
|
||||
import { PageHead } from "@/components/core";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
// types
|
||||
import { TPageNavigationTabs } from "@plane/types";
|
||||
// components
|
||||
import { PagesHeader } from "@/components/headers";
|
||||
import { RecentPagesList, CreateUpdatePageModal } from "@/components/pages";
|
||||
import { PagesLoader } from "@/components/ui";
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { PAGE_TABS_LIST } from "@/constants/page";
|
||||
import { useApplication, useEventTracker, useUser, useProject } from "@/hooks/store";
|
||||
import { useProjectPages } from "@/hooks/store/use-project-page";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
import useUserAuth from "@/hooks/use-user-auth";
|
||||
import useSize from "@/hooks/use-window-size";
|
||||
import { PagesListRoot, PagesListView } from "@/components/pages";
|
||||
// hooks
|
||||
import { useApplication } from "@/hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "@/layouts/app-layout";
|
||||
// components
|
||||
// types
|
||||
// lib
|
||||
import { NextPageWithLayout } from "@/lib/types";
|
||||
// constants
|
||||
|
||||
const AllPagesList = dynamic<any>(() => import("@/components/pages").then((a) => a.AllPagesList), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FavoritePagesList = dynamic<any>(() => import("@/components/pages").then((a) => a.FavoritePagesList), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const PrivatePagesList = dynamic<any>(() => import("@/components/pages").then((a) => a.PrivatePagesList), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ArchivedPagesList = dynamic<any>(() => import("@/components/pages").then((a) => a.ArchivedPagesList), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const SharedPagesList = dynamic<any>(() => import("@/components/pages").then((a) => a.SharedPagesList), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// states
|
||||
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
|
||||
const { type } = router.query;
|
||||
// store hooks
|
||||
const { currentUser, currentUserLoader } = useUser();
|
||||
const {
|
||||
commandPalette: { toggleCreatePageModal },
|
||||
router: { workspaceSlug, projectId },
|
||||
} = useApplication();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { getProjectById, currentProjectDetails } = useProject();
|
||||
const { fetchProjectPages, fetchArchivedProjectPages, loader, archivedPageLoader, projectPageIds, archivedPageIds } =
|
||||
useProjectPages();
|
||||
// hooks
|
||||
const {} = useUserAuth({ user: currentUser, isLoading: currentUserLoader });
|
||||
const [windowWidth] = useSize();
|
||||
// local storage
|
||||
const { storedValue: pageTab, setValue: setPageTab } = useLocalStorage("pageTab", "Recent");
|
||||
// fetching pages from API
|
||||
useSWR(
|
||||
workspaceSlug && projectId ? `ALL_PAGES_LIST_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchProjectPages(workspaceSlug.toString(), projectId.toString()) : null
|
||||
);
|
||||
// fetching archived pages from API
|
||||
useSWR(
|
||||
workspaceSlug && projectId ? `ALL_ARCHIVED_PAGES_LIST_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchArchivedProjectPages(workspaceSlug.toString(), projectId.toString()) : null
|
||||
);
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
// No access to
|
||||
if (currentProjectDetails?.page_view === false)
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState
|
||||
type={EmptyStateType.DISABLED_PROJECT_PAGE}
|
||||
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const currentTabValue = (tab: string | null) => {
|
||||
switch (tab) {
|
||||
case "Recent":
|
||||
return 0;
|
||||
case "All":
|
||||
return 1;
|
||||
case "Favorites":
|
||||
return 2;
|
||||
case "Private":
|
||||
return 3;
|
||||
case "Shared":
|
||||
return 4;
|
||||
case "Archived":
|
||||
return 5;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
const currentPageType = (): TPageNavigationTabs => {
|
||||
const pageType = type?.toString();
|
||||
if (pageType === "private") return "private";
|
||||
if (pageType === "archived") return "archived";
|
||||
return "public";
|
||||
};
|
||||
|
||||
// derived values
|
||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - Pages` : undefined;
|
||||
|
||||
const MobileTabList = () => (
|
||||
<Tab.List as="div" className="flex items-center justify-between border-b border-custom-border-200 px-3 pt-3 mb-4">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{PAGE_TABS_LIST.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={({ selected }) =>
|
||||
`text-sm outline-none pb-3 ${
|
||||
selected ? "border-custom-primary-100 text-custom-primary-100 border-b" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{tab.title}
|
||||
</Tab>
|
||||
))}
|
||||
</div>
|
||||
</Tab.List>
|
||||
);
|
||||
|
||||
if (loader || archivedPageLoader) return <PagesLoader />;
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
{projectPageIds && archivedPageIds && projectPageIds.length + archivedPageIds.length > 0 ? (
|
||||
<>
|
||||
{workspaceSlug && projectId && (
|
||||
<CreateUpdatePageModal
|
||||
isOpen={createUpdatePageModal}
|
||||
handleClose={() => setCreateUpdatePageModal(false)}
|
||||
projectId={projectId.toString()}
|
||||
/>
|
||||
)}
|
||||
<div className="flex h-full flex-col md:space-y-5 overflow-hidden md:py-6">
|
||||
<div className="justify-between gap-4 hidden md:flex px-6">
|
||||
<h3 className="text-2xl font-semibold text-custom-text-100">Pages</h3>
|
||||
</div>
|
||||
<Tab.Group
|
||||
as={Fragment}
|
||||
defaultIndex={currentTabValue(pageTab)}
|
||||
onChange={(i) => {
|
||||
switch (i) {
|
||||
case 0:
|
||||
return setPageTab("Recent");
|
||||
case 1:
|
||||
return setPageTab("All");
|
||||
case 2:
|
||||
return setPageTab("Favorites");
|
||||
case 3:
|
||||
return setPageTab("Private");
|
||||
case 4:
|
||||
return setPageTab("Shared");
|
||||
case 5:
|
||||
return setPageTab("Archived");
|
||||
default:
|
||||
return setPageTab("All");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{windowWidth < 768 ? (
|
||||
<MobileTabList />
|
||||
) : (
|
||||
<Tab.List as="div" className="mb-6 items-center justify-between hidden md:flex px-6">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{PAGE_TABS_LIST.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={({ selected }) =>
|
||||
`rounded-full border px-5 py-1.5 text-sm outline-none ${
|
||||
selected
|
||||
? "border-custom-primary bg-custom-primary text-white"
|
||||
: "border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{tab.title}
|
||||
</Tab>
|
||||
))}
|
||||
</div>
|
||||
</Tab.List>
|
||||
)}
|
||||
|
||||
<Tab.Panels as={Fragment}>
|
||||
<Tab.Panel as="div" className="h-full space-y-5 overflow-y-auto vertical-scrollbar scrollbar-lg pl-6">
|
||||
<RecentPagesList />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="h-full overflow-hidden pl-6">
|
||||
<AllPagesList />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="h-full overflow-hidden pl-6">
|
||||
<FavoritePagesList />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="h-full overflow-hidden pl-6">
|
||||
<PrivatePagesList />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="h-full overflow-hidden pl-6">
|
||||
<SharedPagesList />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="h-full overflow-hidden pl-6">
|
||||
<ArchivedPagesList />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
type={EmptyStateType.PROJECT_PAGE}
|
||||
primaryButtonOnClick={() => {
|
||||
setTrackElement("Pages empty state");
|
||||
toggleCreatePageModal(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<PagesListView
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
pageType={currentPageType()}
|
||||
>
|
||||
<PagesListRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||
</PagesListView>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue