From eba5ed24adac43350eae1af8ff2f5359bcda3d66 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Sun, 18 Feb 2024 15:26:50 +0530 Subject: [PATCH 001/158] fix: color pick background color on change (#3691) --- .../core/theme/custom-theme-selector.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/web/components/core/theme/custom-theme-selector.tsx b/web/components/core/theme/custom-theme-selector.tsx index bd6f43569..fdb7a6483 100644 --- a/web/components/core/theme/custom-theme-selector.tsx +++ b/web/components/core/theme/custom-theme-selector.tsx @@ -66,7 +66,6 @@ export const CustomThemeSelector: React.FC = observer(() => { const handleValueChange = (val: string | undefined, onChange: any) => { let hex = val; - // prepend a hashtag if it doesn't exist if (val && val[0] !== "#") hex = `#${val}`; @@ -94,7 +93,7 @@ export const CustomThemeSelector: React.FC = observer(() => { placeholder="#0d101b" className="w-full" style={{ - backgroundColor: value, + backgroundColor: watch("background"), color: watch("text"), }} hasError={Boolean(errors?.background)} @@ -120,8 +119,8 @@ export const CustomThemeSelector: React.FC = observer(() => { placeholder="#c5c5c5" className="w-full" style={{ - backgroundColor: watch("background"), - color: value, + backgroundColor: watch("text"), + color: watch("background"), }} hasError={Boolean(errors?.text)} /> @@ -146,7 +145,7 @@ export const CustomThemeSelector: React.FC = observer(() => { placeholder="#3f76ff" className="w-full" style={{ - backgroundColor: value, + backgroundColor: watch("primary"), color: watch("text"), }} hasError={Boolean(errors?.primary)} @@ -172,7 +171,7 @@ export const CustomThemeSelector: React.FC = observer(() => { placeholder="#0d101b" className="w-full" style={{ - backgroundColor: value, + backgroundColor: watch("sidebarBackground"), color: watch("sidebarText"), }} hasError={Boolean(errors?.sidebarBackground)} @@ -200,8 +199,8 @@ export const CustomThemeSelector: React.FC = observer(() => { placeholder="#c5c5c5" className="w-full" style={{ - backgroundColor: watch("sidebarBackground"), - color: value, + backgroundColor: watch("sidebarText"), + color: watch("sidebarBackground"), }} hasError={Boolean(errors?.sidebarText)} /> From 10057377dca40c0dc8d088dc3161821b67813635 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Sun, 18 Feb 2024 15:28:37 +0530 Subject: [PATCH 002/158] fix: improved issue description editor focus and state management (#3690) * chore: issue input and editor reload alert issue resolved * chore: issue description mutation issue in inbox * fix: reload confirmation alert and stay focused after saving * chore: updated the renderOnPropChange prop in the description-input --------- Co-authored-by: sriram veeraghanta --- web/components/issues/description-input.tsx | 30 +++- .../issue-detail/inbox/main-content.tsx | 2 + .../issues/issue-detail/main-content.tsx | 2 + .../roots/archived-issue-layout-root.tsx | 2 +- .../issues/peek-overview/header.tsx | 153 ++++++++++++++++++ web/components/issues/peek-overview/index.ts | 1 + .../issues/peek-overview/issue-detail.tsx | 20 ++- web/components/issues/peek-overview/view.tsx | 145 +++-------------- web/components/issues/title-input.tsx | 7 +- 9 files changed, 227 insertions(+), 135 deletions(-) create mode 100644 web/components/issues/peek-overview/header.tsx diff --git a/web/components/issues/description-input.tsx b/web/components/issues/description-input.tsx index 8f3dc8644..f82627fed 100644 --- a/web/components/issues/description-input.tsx +++ b/web/components/issues/description-input.tsx @@ -12,12 +12,12 @@ const fileService = new FileService(); import { TIssueOperations } from "./issue-detail"; // hooks import useDebounce from "hooks/use-debounce"; -import useReloadConfirmations from "hooks/use-reload-confirmation"; export type IssueDescriptionInputProps = { disabled?: boolean; value: string | undefined | null; workspaceSlug: string; + isSubmitting: "submitting" | "submitted" | "saved"; setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; issueOperations: TIssueOperations; projectId: string; @@ -28,21 +28,34 @@ export const IssueDescriptionInput: FC = observer((p const { disabled, value, workspaceSlug, setIsSubmitting, issueId, issueOperations, projectId } = props; // states const [descriptionHTML, setDescriptionHTML] = useState(value); + const [localIssueDescription, setLocalIssueDescription] = useState({ + id: issueId, + description_html: typeof value === "string" && value != "" ? value : "

", + }); // store hooks const { mentionHighlights, mentionSuggestions } = useMention(); - const workspaceStore = useWorkspace(); + const { getWorkspaceBySlug } = useWorkspace(); // hooks - const { setShowAlert } = useReloadConfirmations(); const debouncedValue = useDebounce(descriptionHTML, 1500); // computed values - const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug)?.id as string; + const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id as string; useEffect(() => { - setDescriptionHTML(value); + if (value) setDescriptionHTML(value); }, [value]); + useEffect(() => { + if (issueId && value) + setLocalIssueDescription({ + id: issueId, + description_html: typeof value === "string" && value != "" ? value : "

", + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [issueId, value]); + useEffect(() => { if (debouncedValue || debouncedValue === "") { + setIsSubmitting("submitted"); issueOperations .update(workspaceSlug, projectId, issueId, { description_html: debouncedValue }, false) .finally(() => { @@ -79,12 +92,13 @@ export const IssueDescriptionInput: FC = observer((p deleteFile={fileService.getDeleteImageFunction(workspaceId)} restoreFile={fileService.getRestoreImageFunction(workspaceId)} value={descriptionHTML} - setShouldShowAlert={setShowAlert} - setIsSubmitting={setIsSubmitting} + rerenderOnPropsChange={localIssueDescription} + // setShouldShowAlert={setShowAlert} + // setIsSubmitting={setIsSubmitting} dragDropEnabled customClassName="min-h-[150px] shadow-sm" onChange={(description: Object, description_html: string) => { - setShowAlert(true); + // setShowAlert(true); setIsSubmitting("submitting"); setDescriptionHTML(description_html); }} diff --git a/web/components/issues/issue-detail/inbox/main-content.tsx b/web/components/issues/issue-detail/inbox/main-content.tsx index d25fe9260..b49c0286f 100644 --- a/web/components/issues/issue-detail/inbox/main-content.tsx +++ b/web/components/issues/issue-detail/inbox/main-content.tsx @@ -63,6 +63,7 @@ export const InboxIssueMainContent: React.FC = observer((props) => { workspaceSlug={workspaceSlug} projectId={issue.project_id} issueId={issue.id} + isSubmitting={isSubmitting} setIsSubmitting={(value) => setIsSubmitting(value)} issueOperations={issueOperations} disabled={!is_editable} @@ -73,6 +74,7 @@ export const InboxIssueMainContent: React.FC = observer((props) => { workspaceSlug={workspaceSlug} projectId={issue.project_id} issueId={issue.id} + isSubmitting={isSubmitting} setIsSubmitting={(value) => setIsSubmitting(value)} issueOperations={issueOperations} disabled={!is_editable} diff --git a/web/components/issues/issue-detail/main-content.tsx b/web/components/issues/issue-detail/main-content.tsx index 14860a0cf..968b9faa5 100644 --- a/web/components/issues/issue-detail/main-content.tsx +++ b/web/components/issues/issue-detail/main-content.tsx @@ -67,6 +67,7 @@ export const IssueMainContent: React.FC = observer((props) => { workspaceSlug={workspaceSlug} projectId={issue.project_id} issueId={issue.id} + isSubmitting={isSubmitting} setIsSubmitting={(value) => setIsSubmitting(value)} issueOperations={issueOperations} disabled={!is_editable} @@ -77,6 +78,7 @@ export const IssueMainContent: React.FC = observer((props) => { workspaceSlug={workspaceSlug} projectId={issue.project_id} issueId={issue.id} + isSubmitting={isSubmitting} setIsSubmitting={(value) => setIsSubmitting(value)} issueOperations={issueOperations} disabled={!is_editable} diff --git a/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx index 2ae7ae510..5f049d4c3 100644 --- a/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx @@ -54,7 +54,7 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
- + )} diff --git a/web/components/issues/peek-overview/header.tsx b/web/components/issues/peek-overview/header.tsx new file mode 100644 index 000000000..8b51c977e --- /dev/null +++ b/web/components/issues/peek-overview/header.tsx @@ -0,0 +1,153 @@ +import { FC } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react"; +import { MoveRight, MoveDiagonal, Link2, Trash2 } from "lucide-react"; +// ui +import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon } from "@plane/ui"; +// helpers +import { copyUrlToClipboard } from "helpers/string.helper"; +// hooks +import useToast from "hooks/use-toast"; +// store hooks +import { useUser } from "hooks/store"; +// components +import { IssueSubscription, IssueUpdateStatus } from "components/issues"; + +export type TPeekModes = "side-peek" | "modal" | "full-screen"; + +const PEEK_OPTIONS: { key: TPeekModes; icon: any; title: string }[] = [ + { + key: "side-peek", + icon: SidePanelIcon, + title: "Side Peek", + }, + { + key: "modal", + icon: CenterPanelIcon, + title: "Modal", + }, + { + key: "full-screen", + icon: FullScreenPanelIcon, + title: "Full Screen", + }, +]; + +export type PeekOverviewHeaderProps = { + peekMode: TPeekModes; + setPeekMode: (value: TPeekModes) => void; + removeRoutePeekId: () => void; + workspaceSlug: string; + projectId: string; + issueId: string; + isArchived: boolean; + disabled: boolean; + toggleDeleteIssueModal: (value: boolean) => void; + isSubmitting: "submitting" | "submitted" | "saved"; +}; + +export const IssuePeekOverviewHeader: FC = observer((props) => { + const { + peekMode, + setPeekMode, + workspaceSlug, + projectId, + issueId, + isArchived, + disabled, + removeRoutePeekId, + toggleDeleteIssueModal, + isSubmitting, + } = props; + // router + const router = useRouter(); + // store hooks + const { currentUser } = useUser(); + // hooks + const { setToastAlert } = useToast(); + // derived values + const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode); + + const handleCopyText = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + copyUrlToClipboard( + `${workspaceSlug}/projects/${projectId}/${isArchived ? "archived-issues" : "issues"}/${issueId}` + ).then(() => { + setToastAlert({ + type: "success", + title: "Link Copied!", + message: "Issue link copied to clipboard.", + }); + }); + }; + + const redirectToIssueDetail = () => { + router.push({ + pathname: `/${workspaceSlug}/projects/${projectId}/${isArchived ? "archived-issues" : "issues"}/${issueId}`, + }); + removeRoutePeekId(); + }; + + return ( +
+
+ + + + {currentMode && ( +
+ setPeekMode(val)} + customButton={ + + } + > + {PEEK_OPTIONS.map((mode) => ( + +
+ + {mode.title} +
+
+ ))} +
+
+ )} +
+
+ +
+ {currentUser && !isArchived && ( + + )} + + {!disabled && ( + + )} +
+
+
+ ); +}); diff --git a/web/components/issues/peek-overview/index.ts b/web/components/issues/peek-overview/index.ts index 6d602e45b..aa341b939 100644 --- a/web/components/issues/peek-overview/index.ts +++ b/web/components/issues/peek-overview/index.ts @@ -2,3 +2,4 @@ export * from "./issue-detail"; export * from "./properties"; export * from "./root"; export * from "./view"; +export * from "./header"; diff --git a/web/components/issues/peek-overview/issue-detail.tsx b/web/components/issues/peek-overview/issue-detail.tsx index 7bc1b1b03..0134d35ee 100644 --- a/web/components/issues/peek-overview/issue-detail.tsx +++ b/web/components/issues/peek-overview/issue-detail.tsx @@ -1,4 +1,4 @@ -import { FC, useCallback, useEffect, useState } from "react"; +import { FC, useEffect } from "react"; import { observer } from "mobx-react"; // store hooks import { useIssueDetail, useProject, useUser } from "hooks/store"; @@ -9,7 +9,6 @@ import { TIssueOperations } from "components/issues"; import { IssueReaction } from "../issue-detail/reactions"; import { IssueTitleInput } from "../title-input"; import { IssueDescriptionInput } from "../description-input"; -import { debounce } from "lodash"; interface IPeekOverviewIssueDetails { workspaceSlug: string; @@ -22,13 +21,15 @@ interface IPeekOverviewIssueDetails { } export const PeekOverviewIssueDetails: FC = observer((props) => { - const { workspaceSlug, issueId, issueOperations, disabled, setIsSubmitting } = props; + const { workspaceSlug, issueId, issueOperations, disabled, isSubmitting, setIsSubmitting } = props; // store hooks const { getProjectById } = useProject(); const { currentUser } = useUser(); const { issue: { getIssueById }, } = useIssueDetail(); + // hooks + const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting"); // derived values const issue = getIssueById(issueId); @@ -36,6 +37,17 @@ export const PeekOverviewIssueDetails: FC = observer( const projectDetails = getProjectById(issue?.project_id); + useEffect(() => { + if (isSubmitting === "submitted") { + setShowAlert(false); + setTimeout(async () => { + setIsSubmitting("saved"); + }, 2000); + } else if (isSubmitting === "submitting") { + setShowAlert(true); + } + }, [isSubmitting, setShowAlert, setIsSubmitting]); + return ( <> @@ -45,6 +57,7 @@ export const PeekOverviewIssueDetails: FC = observer( workspaceSlug={workspaceSlug} projectId={issue.project_id} issueId={issue.id} + isSubmitting={isSubmitting} setIsSubmitting={(value) => setIsSubmitting(value)} issueOperations={issueOperations} disabled={disabled} @@ -54,6 +67,7 @@ export const PeekOverviewIssueDetails: FC = observer( workspaceSlug={workspaceSlug} projectId={issue.project_id} issueId={issue.id} + isSubmitting={isSubmitting} setIsSubmitting={(value) => setIsSubmitting(value)} issueOperations={issueOperations} disabled={disabled} diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index 7b6c851ff..4e80c4938 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -1,28 +1,25 @@ import { FC, useRef, useState } from "react"; -import { useRouter } from "next/router"; + import { observer } from "mobx-react-lite"; -import { MoveRight, MoveDiagonal, Link2, Trash2 } from "lucide-react"; + // hooks import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useKeypress from "hooks/use-keypress"; // store hooks -import { useIssueDetail, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +import { useIssueDetail } from "hooks/store"; // components import { DeleteArchivedIssueModal, DeleteIssueModal, - IssueSubscription, - IssueUpdateStatus, + IssuePeekOverviewHeader, + TPeekModes, PeekOverviewIssueDetails, PeekOverviewProperties, TIssueOperations, } from "components/issues"; import { IssueActivity } from "../issue-detail/issue-activity"; // ui -import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui"; -// helpers -import { copyUrlToClipboard } from "helpers/string.helper"; +import { Spinner } from "@plane/ui"; interface IIssueView { workspaceSlug: string; @@ -34,72 +31,28 @@ interface IIssueView { issueOperations: TIssueOperations; } -type TPeekModes = "side-peek" | "modal" | "full-screen"; - -const PEEK_OPTIONS: { key: TPeekModes; icon: any; title: string }[] = [ - { - key: "side-peek", - icon: SidePanelIcon, - title: "Side Peek", - }, - { - key: "modal", - icon: CenterPanelIcon, - title: "Modal", - }, - { - key: "full-screen", - icon: FullScreenPanelIcon, - title: "Full Screen", - }, -]; - export const IssueView: FC = observer((props) => { const { workspaceSlug, projectId, issueId, isLoading, is_archived, disabled = false, issueOperations } = props; - // router - const router = useRouter(); // states const [peekMode, setPeekMode] = useState("side-peek"); const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); // ref const issuePeekOverviewRef = useRef(null); // store hooks - const { setPeekIssue, isAnyModalOpen, isDeleteIssueModalOpen, toggleDeleteIssueModal } = useIssueDetail(); - const { currentUser } = useUser(); const { + setPeekIssue, + isAnyModalOpen, + isDeleteIssueModalOpen, + toggleDeleteIssueModal, issue: { getIssueById }, } = useIssueDetail(); - const { setToastAlert } = useToast(); - // derived values - const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode); const issue = getIssueById(issueId); - + // remove peek id const removeRoutePeekId = () => { setPeekIssue(undefined); }; + // hooks useOutsideClickDetector(issuePeekOverviewRef, () => !isAnyModalOpen && removeRoutePeekId()); - - const redirectToIssueDetail = () => { - router.push({ - pathname: `/${workspaceSlug}/projects/${projectId}/${is_archived ? "archived-issues" : "issues"}/${issueId}`, - }); - removeRoutePeekId(); - }; - - const handleCopyText = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - copyUrlToClipboard( - `${workspaceSlug}/projects/${projectId}/${is_archived ? "archived-issues" : "issues"}/${issueId}` - ).then(() => { - setToastAlert({ - type: "success", - title: "Link Copied!", - message: "Issue link copied to clipboard.", - }); - }); - }; - const handleKeyDown = () => !isAnyModalOpen && removeRoutePeekId(); useKeypress("Escape", handleKeyDown); @@ -141,66 +94,20 @@ export const IssueView: FC = observer((props) => { }} > {/* header */} -
-
- - - - {currentMode && ( -
- setPeekMode(val)} - customButton={ - - } - > - {PEEK_OPTIONS.map((mode) => ( - -
- - {mode.title} -
-
- ))} -
-
- )} -
-
- -
- {currentUser && !is_archived && ( - - )} - - {!disabled && ( - - )} -
-
-
- + { + setPeekMode(value); + }} + removeRoutePeekId={removeRoutePeekId} + toggleDeleteIssueModal={toggleDeleteIssueModal} + isArchived={is_archived} + issueId={issueId} + workspaceSlug={workspaceSlug} + projectId={projectId} + isSubmitting={isSubmitting} + disabled={disabled} + /> {/* content */}
{isLoading && !issue ? ( diff --git a/web/components/issues/title-input.tsx b/web/components/issues/title-input.tsx index 2cd031b4f..e18974190 100644 --- a/web/components/issues/title-input.tsx +++ b/web/components/issues/title-input.tsx @@ -6,12 +6,12 @@ import { TextArea } from "@plane/ui"; import { TIssueOperations } from "./issue-detail"; // hooks import useDebounce from "hooks/use-debounce"; -import useReloadConfirmations from "hooks/use-reload-confirmation"; export type IssueTitleInputProps = { disabled?: boolean; value: string | undefined | null; workspaceSlug: string; + isSubmitting: "submitting" | "submitted" | "saved"; setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; issueOperations: TIssueOperations; projectId: string; @@ -23,7 +23,7 @@ export const IssueTitleInput: FC = observer((props) => { // states const [title, setTitle] = useState(""); // hooks - const { setShowAlert } = useReloadConfirmations(); + const debouncedValue = useDebounce(title, 1500); useEffect(() => { @@ -42,11 +42,10 @@ export const IssueTitleInput: FC = observer((props) => { const handleTitleChange = useCallback( (e: React.ChangeEvent) => { - setShowAlert(true); setIsSubmitting("submitting"); setTitle(e.target.value); }, - [setIsSubmitting, setShowAlert] + [setIsSubmitting] ); return ( From 261013b794b39e0b39b45b36da4cb16f878c50eb Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 19 Feb 2024 00:17:31 +0530 Subject: [PATCH 003/158] fix: inbox issue initial data load (#3693) * fix: inbox issue initial data load * chore: removed unnecessary comments --- web/components/issues/description-input.tsx | 22 ++++--------------- .../issue-detail/inbox/main-content.tsx | 1 + web/components/issues/title-input.tsx | 2 +- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/web/components/issues/description-input.tsx b/web/components/issues/description-input.tsx index f82627fed..d18fad6e5 100644 --- a/web/components/issues/description-input.tsx +++ b/web/components/issues/description-input.tsx @@ -22,16 +22,13 @@ export type IssueDescriptionInputProps = { issueOperations: TIssueOperations; projectId: string; issueId: string; + initialValue?: string; }; export const IssueDescriptionInput: FC = observer((props) => { - const { disabled, value, workspaceSlug, setIsSubmitting, issueId, issueOperations, projectId } = props; + const { disabled, value, workspaceSlug, setIsSubmitting, issueId, issueOperations, projectId, initialValue } = props; // states const [descriptionHTML, setDescriptionHTML] = useState(value); - const [localIssueDescription, setLocalIssueDescription] = useState({ - id: issueId, - description_html: typeof value === "string" && value != "" ? value : "

", - }); // store hooks const { mentionHighlights, mentionSuggestions } = useMention(); const { getWorkspaceBySlug } = useWorkspace(); @@ -45,16 +42,7 @@ export const IssueDescriptionInput: FC = observer((p }, [value]); useEffect(() => { - if (issueId && value) - setLocalIssueDescription({ - id: issueId, - description_html: typeof value === "string" && value != "" ? value : "

", - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [issueId, value]); - - useEffect(() => { - if (debouncedValue || debouncedValue === "") { + if (debouncedValue && debouncedValue !== value) { setIsSubmitting("submitted"); issueOperations .update(workspaceSlug, projectId, issueId, { description_html: debouncedValue }, false) @@ -92,9 +80,7 @@ export const IssueDescriptionInput: FC = observer((p deleteFile={fileService.getDeleteImageFunction(workspaceId)} restoreFile={fileService.getRestoreImageFunction(workspaceId)} value={descriptionHTML} - rerenderOnPropsChange={localIssueDescription} - // setShouldShowAlert={setShowAlert} - // setIsSubmitting={setIsSubmitting} + initialValue={initialValue} dragDropEnabled customClassName="min-h-[150px] shadow-sm" onChange={(description: Object, description_html: string) => { diff --git a/web/components/issues/issue-detail/inbox/main-content.tsx b/web/components/issues/issue-detail/inbox/main-content.tsx index b49c0286f..a576f6034 100644 --- a/web/components/issues/issue-detail/inbox/main-content.tsx +++ b/web/components/issues/issue-detail/inbox/main-content.tsx @@ -79,6 +79,7 @@ export const InboxIssueMainContent: React.FC = observer((props) => { issueOperations={issueOperations} disabled={!is_editable} value={issue.description_html} + initialValue={issue.description_html} /> {currentUser && ( diff --git a/web/components/issues/title-input.tsx b/web/components/issues/title-input.tsx index e18974190..55dd80b87 100644 --- a/web/components/issues/title-input.tsx +++ b/web/components/issues/title-input.tsx @@ -31,7 +31,7 @@ export const IssueTitleInput: FC = observer((props) => { }, [value]); useEffect(() => { - if (debouncedValue) { + if (debouncedValue && debouncedValue !== value) { issueOperations.update(workspaceSlug, projectId, issueId, { name: debouncedValue }, false).finally(() => { setIsSubmitting("saved"); }); From 7381a818a9e1ec52c8e14dac6859895f80fc4be6 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 19 Feb 2024 00:34:54 +0530 Subject: [PATCH 004/158] fix: description editor fixes --- web/components/issues/description-input.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/components/issues/description-input.tsx b/web/components/issues/description-input.tsx index d18fad6e5..8e0e47ae9 100644 --- a/web/components/issues/description-input.tsx +++ b/web/components/issues/description-input.tsx @@ -84,7 +84,6 @@ export const IssueDescriptionInput: FC = observer((p dragDropEnabled customClassName="min-h-[150px] shadow-sm" onChange={(description: Object, description_html: string) => { - // setShowAlert(true); setIsSubmitting("submitting"); setDescriptionHTML(description_html); }} From 170f30c7dde872758d7ec1fc033d7a34ccca0269 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 19 Feb 2024 00:35:21 +0530 Subject: [PATCH 005/158] fix: editor fixes --- packages/editor/rich-text-editor/src/ui/index.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/editor/rich-text-editor/src/ui/index.tsx b/packages/editor/rich-text-editor/src/ui/index.tsx index 43c3f8f34..fe5752849 100644 --- a/packages/editor/rich-text-editor/src/ui/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/index.tsx @@ -15,6 +15,7 @@ import { EditorBubbleMenu } from "src/ui/menus/bubble-menu"; export type IRichTextEditor = { value: string; + initialValue?: string; dragDropEnabled?: boolean; uploadFile: UploadImage; restoreFile: RestoreImage; @@ -54,6 +55,7 @@ const RichTextEditor = ({ setShouldShowAlert, editorContentCustomClassNames, value, + initialValue, uploadFile, deleteFile, noBorder, @@ -97,6 +99,10 @@ const RichTextEditor = ({ customClassName, }); + React.useEffect(() => { + if (editor && initialValue) editor.commands.setContent(initialValue); + }, [editor, initialValue]); + if (!editor) return null; return ( From 17e5663e81a88606b27bd9731c65574b09d45d7e Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 19 Feb 2024 12:54:45 +0530 Subject: [PATCH 006/158] fix: description autosave fixes --- web/components/issues/description-input.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/web/components/issues/description-input.tsx b/web/components/issues/description-input.tsx index 8e0e47ae9..86ef898c0 100644 --- a/web/components/issues/description-input.tsx +++ b/web/components/issues/description-input.tsx @@ -38,23 +38,22 @@ export const IssueDescriptionInput: FC = observer((p const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id as string; useEffect(() => { - if (value) setDescriptionHTML(value); + setDescriptionHTML(value); }, [value]); useEffect(() => { if (debouncedValue && debouncedValue !== value) { - setIsSubmitting("submitted"); issueOperations .update(workspaceSlug, projectId, issueId, { description_html: debouncedValue }, false) .finally(() => { - setIsSubmitting("saved"); + setIsSubmitting("submitted"); }); } // DO NOT Add more dependencies here. It will cause multiple requests to be sent. // eslint-disable-next-line react-hooks/exhaustive-deps }, [debouncedValue]); - if (!descriptionHTML && descriptionHTML !== "") { + if (!descriptionHTML) { return ( @@ -79,13 +78,13 @@ export const IssueDescriptionInput: FC = observer((p uploadFile={fileService.getUploadFileFunction(workspaceSlug)} deleteFile={fileService.getDeleteImageFunction(workspaceId)} restoreFile={fileService.getRestoreImageFunction(workspaceId)} - value={descriptionHTML} + value={descriptionHTML === "" ? "

" : descriptionHTML} initialValue={initialValue} dragDropEnabled customClassName="min-h-[150px] shadow-sm" onChange={(description: Object, description_html: string) => { setIsSubmitting("submitting"); - setDescriptionHTML(description_html); + setDescriptionHTML(description_html === "" ? "

" : description_html); }} mentionSuggestions={mentionSuggestions} mentionHighlights={mentionHighlights} From bbbd7047d33d9f6f223ff7d5c3a1f79892afcbb1 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Mon, 19 Feb 2024 15:43:57 +0530 Subject: [PATCH 007/158] fix: issue description empty state initial load in inbox and issue detail page (#3696) * fix: updated description init loading and added loading confirmation alert in inbox issues, issue peek overview, and issue detail * fix: updated the space issue in the editor and removed unwanted props in the description-input for issues --- .../editor/rich-text-editor/src/ui/index.tsx | 2 +- web/components/issues/description-input.tsx | 20 +++++------ .../issue-detail/inbox/main-content.tsx | 33 +++++++++++++++---- .../issues/issue-detail/main-content.tsx | 32 ++++++++++++++---- .../issues/peek-overview/issue-detail.tsx | 28 ++++++++++------ 5 files changed, 80 insertions(+), 35 deletions(-) diff --git a/packages/editor/rich-text-editor/src/ui/index.tsx b/packages/editor/rich-text-editor/src/ui/index.tsx index fe5752849..4bcb340fd 100644 --- a/packages/editor/rich-text-editor/src/ui/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/index.tsx @@ -100,7 +100,7 @@ const RichTextEditor = ({ }); React.useEffect(() => { - if (editor && initialValue) editor.commands.setContent(initialValue); + if (editor && initialValue && editor.getHTML() != initialValue) editor.commands.setContent(initialValue); }, [editor, initialValue]); if (!editor) return null; diff --git a/web/components/issues/description-input.tsx b/web/components/issues/description-input.tsx index 86ef898c0..79634fa84 100644 --- a/web/components/issues/description-input.tsx +++ b/web/components/issues/description-input.tsx @@ -1,5 +1,4 @@ import { FC, useState, useEffect } from "react"; -import { observer } from "mobx-react"; // components import { Loader } from "@plane/ui"; import { RichReadOnlyEditor, RichTextEditor } from "@plane/rich-text-editor"; @@ -14,19 +13,18 @@ import { TIssueOperations } from "./issue-detail"; import useDebounce from "hooks/use-debounce"; export type IssueDescriptionInputProps = { - disabled?: boolean; - value: string | undefined | null; workspaceSlug: string; - isSubmitting: "submitting" | "submitted" | "saved"; - setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; - issueOperations: TIssueOperations; projectId: string; issueId: string; - initialValue?: string; + value: string | undefined; + initialValue: string | undefined; + disabled?: boolean; + issueOperations: TIssueOperations; + setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; }; -export const IssueDescriptionInput: FC = observer((props) => { - const { disabled, value, workspaceSlug, setIsSubmitting, issueId, issueOperations, projectId, initialValue } = props; +export const IssueDescriptionInput: FC = (props) => { + const { workspaceSlug, projectId, issueId, value, initialValue, disabled, issueOperations, setIsSubmitting } = props; // states const [descriptionHTML, setDescriptionHTML] = useState(value); // store hooks @@ -78,7 +76,7 @@ export const IssueDescriptionInput: FC = observer((p uploadFile={fileService.getUploadFileFunction(workspaceSlug)} deleteFile={fileService.getDeleteImageFunction(workspaceId)} restoreFile={fileService.getRestoreImageFunction(workspaceId)} - value={descriptionHTML === "" ? "

" : descriptionHTML} + value={descriptionHTML} initialValue={initialValue} dragDropEnabled customClassName="min-h-[150px] shadow-sm" @@ -90,4 +88,4 @@ export const IssueDescriptionInput: FC = observer((p mentionHighlights={mentionHighlights} /> ); -}); +}; diff --git a/web/components/issues/issue-detail/inbox/main-content.tsx b/web/components/issues/issue-detail/inbox/main-content.tsx index a576f6034..d753be02f 100644 --- a/web/components/issues/issue-detail/inbox/main-content.tsx +++ b/web/components/issues/issue-detail/inbox/main-content.tsx @@ -1,7 +1,8 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; // hooks import { useIssueDetail, useProjectState, useUser } from "hooks/store"; +import useReloadConfirmations from "hooks/use-reload-confirmation"; // components import { IssueUpdateStatus, TIssueOperations } from "components/issues"; import { IssueTitleInput } from "../../title-input"; @@ -31,12 +32,31 @@ export const InboxIssueMainContent: React.FC = observer((props) => { const { issue: { getIssueById }, } = useIssueDetail(); + const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting"); - const issue = getIssueById(issueId); + useEffect(() => { + if (isSubmitting === "submitted") { + setShowAlert(false); + setTimeout(async () => { + setIsSubmitting("saved"); + }, 3000); + } else if (isSubmitting === "submitting") { + setShowAlert(true); + } + }, [isSubmitting, setShowAlert, setIsSubmitting]); + + const issue = issueId ? getIssueById(issueId) : undefined; if (!issue) return <>; const currentIssueState = projectStates?.find((s) => s.id === issue.state_id); + const issueDescription = + issue.description_html !== undefined || issue.description_html !== null + ? issue.description_html != "" + ? issue.description_html + : "

" + : undefined; + return ( <>
@@ -74,12 +94,11 @@ export const InboxIssueMainContent: React.FC = observer((props) => { workspaceSlug={workspaceSlug} projectId={issue.project_id} issueId={issue.id} - isSubmitting={isSubmitting} - setIsSubmitting={(value) => setIsSubmitting(value)} - issueOperations={issueOperations} + value={issueDescription} + initialValue={issueDescription} disabled={!is_editable} - value={issue.description_html} - initialValue={issue.description_html} + issueOperations={issueOperations} + setIsSubmitting={(value) => setIsSubmitting(value)} /> {currentUser && ( diff --git a/web/components/issues/issue-detail/main-content.tsx b/web/components/issues/issue-detail/main-content.tsx index 968b9faa5..719129d98 100644 --- a/web/components/issues/issue-detail/main-content.tsx +++ b/web/components/issues/issue-detail/main-content.tsx @@ -1,7 +1,8 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; // hooks import { useIssueDetail, useProjectState, useUser } from "hooks/store"; +import useReloadConfirmations from "hooks/use-reload-confirmation"; // components import { IssueAttachmentRoot, IssueUpdateStatus } from "components/issues"; import { IssueTitleInput } from "../title-input"; @@ -33,12 +34,31 @@ export const IssueMainContent: React.FC = observer((props) => { const { issue: { getIssueById }, } = useIssueDetail(); + const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting"); - const issue = getIssueById(issueId); + useEffect(() => { + if (isSubmitting === "submitted") { + setShowAlert(false); + setTimeout(async () => { + setIsSubmitting("saved"); + }, 2000); + } else if (isSubmitting === "submitting") { + setShowAlert(true); + } + }, [isSubmitting, setShowAlert, setIsSubmitting]); + + const issue = issueId ? getIssueById(issueId) : undefined; if (!issue) return <>; const currentIssueState = projectStates?.find((s) => s.id === issue.state_id); + const issueDescription = + issue.description_html !== undefined || issue.description_html !== null + ? issue.description_html != "" + ? issue.description_html + : "

" + : undefined; + return ( <>
@@ -78,11 +98,11 @@ export const IssueMainContent: React.FC = observer((props) => { workspaceSlug={workspaceSlug} projectId={issue.project_id} issueId={issue.id} - isSubmitting={isSubmitting} - setIsSubmitting={(value) => setIsSubmitting(value)} - issueOperations={issueOperations} + value={issueDescription} + initialValue={issueDescription} disabled={!is_editable} - value={issue.description_html} + issueOperations={issueOperations} + setIsSubmitting={(value) => setIsSubmitting(value)} /> {currentUser && ( diff --git a/web/components/issues/peek-overview/issue-detail.tsx b/web/components/issues/peek-overview/issue-detail.tsx index 0134d35ee..7f540874c 100644 --- a/web/components/issues/peek-overview/issue-detail.tsx +++ b/web/components/issues/peek-overview/issue-detail.tsx @@ -30,12 +30,6 @@ export const PeekOverviewIssueDetails: FC = observer( } = useIssueDetail(); // hooks const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting"); - // derived values - const issue = getIssueById(issueId); - - if (!issue) return <>; - - const projectDetails = getProjectById(issue?.project_id); useEffect(() => { if (isSubmitting === "submitted") { @@ -48,6 +42,18 @@ export const PeekOverviewIssueDetails: FC = observer( } }, [isSubmitting, setShowAlert, setIsSubmitting]); + const issue = issueId ? getIssueById(issueId) : undefined; + if (!issue) return <>; + + const projectDetails = getProjectById(issue?.project_id); + + const issueDescription = + issue.description_html !== undefined || issue.description_html !== null + ? issue.description_html != "" + ? issue.description_html + : "

" + : undefined; + return ( <> @@ -63,16 +69,18 @@ export const PeekOverviewIssueDetails: FC = observer( disabled={disabled} value={issue.name} /> + setIsSubmitting(value)} - issueOperations={issueOperations} + value={issueDescription} + initialValue={issueDescription} disabled={disabled} - value={issue.description_html} + issueOperations={issueOperations} + setIsSubmitting={(value) => setIsSubmitting(value)} /> + {currentUser && ( Date: Mon, 19 Feb 2024 21:07:43 +0530 Subject: [PATCH 008/158] chore : project inbox improvement (#3695) * chore: project inbox layout loader added * chore: project inbox layout loader added * chore: project inbox added in sidebar project items * chore: inbox loader improvement * chore: project inbox improvement --- web/components/headers/project-issues.tsx | 35 +++----- web/components/inbox/sidebar/root.tsx | 22 ++--- web/components/project/sidebar-list-item.tsx | 88 ++++++++++++++----- web/components/ui/loader/layouts/index.ts | 1 + .../project-inbox/inbox-layout-loader.tsx | 19 ++++ .../project-inbox/inbox-sidebar-loader.tsx | 24 +++++ .../ui/loader/layouts/project-inbox/index.ts | 2 + .../projects/[projectId]/inbox/index.tsx | 4 +- 8 files changed, 136 insertions(+), 59 deletions(-) create mode 100644 web/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx create mode 100644 web/components/ui/loader/layouts/project-inbox/inbox-sidebar-loader.tsx create mode 100644 web/components/ui/loader/layouts/project-inbox/index.ts diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 5c44a84d6..033196758 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -1,8 +1,7 @@ import { useCallback, useState } from "react"; -import Link from "next/link"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { Briefcase, Circle, ExternalLink, Plus, Inbox } from "lucide-react"; +import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react"; // hooks import { useApplication, @@ -11,7 +10,6 @@ import { useProject, useProjectState, useUser, - useInbox, useMember, } from "hooks/store"; // components @@ -54,7 +52,6 @@ export const ProjectIssuesHeader: React.FC = observer(() => { const { currentProjectDetails } = useProject(); const { projectStates } = useProjectState(); const { projectLabels } = useLabel(); - const { getInboxesByProjectId, getInboxById } = useInbox(); const activeLayout = issueFilters?.displayFilters?.layout; @@ -101,9 +98,6 @@ export const ProjectIssuesHeader: React.FC = observer(() => { [workspaceSlug, projectId, updateFilters] ); - const inboxesMap = currentProjectDetails?.inbox_view ? getInboxesByProjectId(currentProjectDetails.id) : undefined; - const inboxDetails = inboxesMap && inboxesMap.length > 0 ? getInboxById(inboxesMap[0]) : undefined; - const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL; const canUserCreateIssue = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); @@ -154,7 +148,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => { } />} + link={ + } /> + } />
@@ -201,24 +197,15 @@ export const ProjectIssuesHeader: React.FC = observer(() => { />
- {currentProjectDetails?.inbox_view && inboxDetails && ( - - - - - - - )} + {canUserCreateIssue && ( <> - + <> + +
+
+ +
+
- -
+ ); -}; +}); ProjectArchivedIssuesPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index f24fc5597..58431bd38 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -1,12 +1,14 @@ import { ReactElement } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; +import { observer } from "mobx-react"; // hooks -import { useCycle } from "hooks/store"; +import { useCycle, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; // components +import { PageHead } from "components/core"; import { CycleIssuesHeader } from "components/headers"; import { CycleDetailsSidebar } from "components/cycles"; import { CycleLayoutRoot } from "components/issues/issue-layouts"; @@ -17,27 +19,36 @@ import emptyCycle from "public/empty-state/cycle.svg"; // types import { NextPageWithLayout } from "lib/types"; -const CycleDetailPage: NextPageWithLayout = () => { +const CycleDetailPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; // store hooks - const { fetchCycleDetails } = useCycle(); - + const { fetchCycleDetails, getCycleById } = useCycle(); + const { getProjectById } = useProject(); + // hooks const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false"); - const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; - + // fetching cycle details const { error } = useSWR( workspaceSlug && projectId && cycleId ? `CYCLE_DETAILS_${cycleId.toString()}` : null, workspaceSlug && projectId && cycleId ? () => fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString()) : null ); + // derived values + const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; + const cycle = cycleId ? getCycleById(cycleId.toString()) : undefined; + const project = projectId ? getProjectById(projectId.toString()) : undefined; + const pageTitle = project?.name && cycle?.name ? `${project?.name} - ${cycle?.name}` : undefined; + /** + * Toggles the sidebar + */ const toggleSidebar = () => setValue(`${!isSidebarCollapsed}`); return ( <> + {error ? ( { )} ); -}; +}); CycleDetailPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 0b9af62fd..0f86089aa 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -4,11 +4,12 @@ import { observer } from "mobx-react-lite"; import { Tab } from "@headlessui/react"; import { useTheme } from "next-themes"; // hooks -import { useEventTracker, useCycle, useUser } from "hooks/store"; +import { useEventTracker, useCycle, useUser, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; // components +import { PageHead } from "components/core"; import { CyclesHeader } from "components/headers"; import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; @@ -34,12 +35,20 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { currentUser, } = useUser(); const { currentProjectCycleIds, loader } = useCycle(); + const { getProjectById } = useProject(); // router const router = useRouter(); const { workspaceSlug, projectId, peekCycle } = router.query; // local storage const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); + // derived values + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode); + const totalCycles = currentProjectCycleIds?.length ?? 0; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + const project = projectId ? getProjectById(projectId?.toString()) : undefined; + const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined; const handleCurrentLayout = useCallback( (_layout: TCycleLayout) => { @@ -56,13 +65,6 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { [handleCurrentLayout, setCycleTab] ); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode); - - const totalCycles = currentProjectCycleIds?.length ?? 0; - - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; - if (!workspaceSlug || !projectId) return null; if (loader) @@ -75,143 +77,146 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { ); return ( -
- setCreateModal(false)} - /> - {totalCycles === 0 ? ( -
- { - setTrackElement("Cycle empty state"); - setCreateModal(true); - }, - }} - size="lg" - disabled={!isEditingAllowed} - /> -
- ) : ( - i.key == cycleTab)} - selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)} - onChange={(i) => handleCurrentView(CYCLE_TAB_LIST[i]?.key ?? "active")} - > -
- - {CYCLE_TAB_LIST.map((tab) => ( - - `border-b-2 p-4 text-sm font-medium outline-none ${ - selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent" - }` - } - > - {tab.name} - - ))} - -
- {cycleTab !== "active" && ( -
- {CYCLE_VIEW_LAYOUTS.map((layout) => { - if (layout.key === "gantt" && cycleTab === "draft") return null; - - return ( - - - - ); - })} -
- )} -
+ <> + +
+ setCreateModal(false)} + /> + {totalCycles === 0 ? ( +
+ { + setTrackElement("Cycle empty state"); + setCreateModal(true); + }, + }} + size="lg" + disabled={!isEditingAllowed} + />
+ ) : ( + i.key == cycleTab)} + selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)} + onChange={(i) => handleCurrentView(CYCLE_TAB_LIST[i]?.key ?? "active")} + > +
+ + {CYCLE_TAB_LIST.map((tab) => ( + + `border-b-2 p-4 text-sm font-medium outline-none ${ + selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent" + }` + } + > + {tab.name} + + ))} + +
+ {cycleTab !== "active" && ( +
+ {CYCLE_VIEW_LAYOUTS.map((layout) => { + if (layout.key === "gantt" && cycleTab === "draft") return null; - - - {cycleTab && cycleLayout && ( - - )} - + return ( + + + + ); + })} +
+ )} +
+
- - - + + + {cycleTab && cycleLayout && ( + + )} + - - {cycleTab && cycleLayout && ( - - )} - + + + - - {cycleTab && cycleLayout && workspaceSlug && projectId && ( - - )} - + + {cycleTab && cycleLayout && ( + + )} + - - {cycleTab && cycleLayout && workspaceSlug && projectId && ( - - )} - - -
- )} -
+ + {cycleTab && cycleLayout && workspaceSlug && projectId && ( + + )} + + + + {cycleTab && cycleLayout && workspaceSlug && projectId && ( + + )} + + + + )} +
+ ); }); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/draft-issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/draft-issues/index.tsx index eaf7ce3d3..e2ad25214 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/draft-issues/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/draft-issues/index.tsx @@ -5,31 +5,43 @@ import { X, PenSquare } from "lucide-react"; import { AppLayout } from "layouts/app-layout"; // components import { DraftIssueLayoutRoot } from "components/issues/issue-layouts/roots/draft-issue-layout-root"; +import { PageHead } from "components/core"; import { ProjectDraftIssueHeader } from "components/headers"; // types import { NextPageWithLayout } from "lib/types"; +// hooks +import { useProject } from "hooks/store"; +import { observer } from "mobx-react"; -const ProjectDraftIssuesPage: NextPageWithLayout = () => { +const ProjectDraftIssuesPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + // store + const { getProjectById } = useProject(); + // derived values + const project = projectId ? getProjectById(projectId.toString()) : undefined; + const pageTitle = project?.name ? `${project?.name} - Draft Issues` : undefined; return ( -
-
- - +
+
- -
+ ); -}; +}); ProjectDraftIssuesPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx index 5cd1e6c2c..125ee4245 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -7,9 +7,9 @@ import { useProject, useInboxIssues } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // components +import { PageHead } from "components/core"; import { ProjectInboxHeader } from "components/headers"; import { InboxSidebarRoot, InboxContentRoot } from "components/inbox"; - // types import { NextPageWithLayout } from "lib/types"; @@ -22,7 +22,7 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => { filters: { fetchInboxFilters }, issues: { fetchInboxIssues }, } = useInboxIssues(); - + // fetching the Inbox filters and issues useSWR( workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view ? `INBOX_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` @@ -34,26 +34,32 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => { } } ); + // derived values + const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : undefined; if (!workspaceSlug || !projectId || !inboxId || !currentProjectDetails?.inbox_view) return <>; + return ( -
-
- + <> + +
+
+ +
+
+ +
-
- -
-
+ ); }); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 64f43939e..6ff7d5aa5 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -5,14 +5,15 @@ import { observer } from "mobx-react-lite"; // layouts import { AppLayout } from "layouts/app-layout"; // components +import { PageHead } from "components/core"; import { ProjectIssueDetailsHeader } from "components/headers"; import { IssueDetailRoot } from "components/issues"; // ui import { Loader } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; -// fetch-keys -import { useApplication, useIssueDetail } from "hooks/store"; +// store hooks +import { useApplication, useIssueDetail, useProject } from "hooks/store"; const IssueDetailsPage: NextPageWithLayout = observer(() => { // router @@ -23,17 +24,20 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => { fetchIssue, issue: { getIssueById }, } = useIssueDetail(); + const { getProjectById } = useProject(); const { theme: themeStore } = useApplication(); - + // fetching issue details const { isLoading } = useSWR( workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null, workspaceSlug && projectId && issueId ? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString()) : null ); - + // derived values const issue = getIssueById(issueId?.toString() || "") || undefined; + const project = (issue?.project_id && getProjectById(issue?.project_id)) || undefined; const issueLoader = !issue || isLoading ? true : false; + const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined; useEffect(() => { const handleToggleIssueDetailSidebar = () => { @@ -52,6 +56,7 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => { return ( <> + {issueLoader ? (
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx index 3193fe64e..2aa9ab2e6 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx @@ -1,4 +1,7 @@ import { ReactElement } from "react"; +import Head from "next/head"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react"; // components import { ProjectLayoutRoot } from "components/issues"; import { ProjectIssuesHeader } from "components/headers"; @@ -6,12 +9,36 @@ import { ProjectIssuesHeader } from "components/headers"; import { NextPageWithLayout } from "lib/types"; // layouts import { AppLayout } from "layouts/app-layout"; +// hooks +import { useProject } from "hooks/store"; +import { PageHead } from "components/core"; -const ProjectIssuesPage: NextPageWithLayout = () => ( -
- -
-); +const ProjectIssuesPage: NextPageWithLayout = observer(() => { + const router = useRouter(); + const { projectId } = router.query; + // store + const { getProjectById } = useProject(); + + if (!projectId) { + return <>; + } + + // derived values + const project = getProjectById(projectId.toString()); + const pageTitle = project?.name ? `${project?.name} - Issues` : undefined; + + return ( + <> + + + {project?.name} - Issues + +
+ +
+ + ); +}); ProjectIssuesPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx index 946041176..37f7e6b0e 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx @@ -1,8 +1,9 @@ import { ReactElement } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; +import { observer } from "mobx-react"; // hooks -import { useModule } from "hooks/store"; +import { useModule, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; @@ -10,37 +11,44 @@ import { AppLayout } from "layouts/app-layout"; import { ModuleDetailsSidebar } from "components/modules"; import { ModuleLayoutRoot } from "components/issues"; import { ModuleIssuesHeader } from "components/headers"; -// ui +import { PageHead } from "components/core"; import { EmptyState } from "components/common"; // assets import emptyModule from "public/empty-state/module.svg"; // types import { NextPageWithLayout } from "lib/types"; -const ModuleIssuesPage: NextPageWithLayout = () => { +const ModuleIssuesPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; // store hooks - const { fetchModuleDetails } = useModule(); + const { fetchModuleDetails, getModuleById } = useModule(); + const { getProjectById } = useProject(); // local storage const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false"); const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; - + // fetching module details const { error } = useSWR( workspaceSlug && projectId && moduleId ? `CURRENT_MODULE_DETAILS_${moduleId.toString()}` : null, workspaceSlug && projectId && moduleId ? () => fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString()) : null ); + // derived values + const projectModule = moduleId ? getModuleById(moduleId.toString()) : undefined; + const project = projectId ? getProjectById(projectId.toString()) : undefined; + const pageTitle = project?.name && projectModule?.name ? `${project?.name} - ${projectModule?.name}` : undefined; const toggleSidebar = () => { setValue(`${!isSidebarCollapsed}`); }; if (!workspaceSlug || !projectId || !moduleId) return <>; + return ( <> + {error ? ( { )} ); -}; +}); ModuleIssuesPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx index 8e63c2899..085f1e3c3 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx @@ -1,13 +1,33 @@ import { ReactElement } from "react"; +import { useRouter } from "next/router"; // layouts import { AppLayout } from "layouts/app-layout"; // components +import { PageHead } from "components/core"; import { ModulesListView } from "components/modules"; import { ModulesListHeader } from "components/headers"; // types import { NextPageWithLayout } from "lib/types"; +// hooks +import { useProject } from "hooks/store"; +import { observer } from "mobx-react"; -const ProjectModulesPage: NextPageWithLayout = () => ; +const ProjectModulesPage: NextPageWithLayout = observer(() => { + const router = useRouter(); + const { projectId } = router.query; + // store + const { getProjectById } = useProject(); + // derived values + const project = projectId ? getProjectById(projectId.toString()) : undefined; + const pageTitle = project?.name ? `${project?.name} - Modules` : undefined; + + return ( + <> + + + + ); +}); ProjectModulesPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index 93a814d57..bee4fc9c7 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -14,7 +14,7 @@ import { FileService } from "services/file.service"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { GptAssistantPopover } from "components/core"; +import { GptAssistantPopover, PageHead } from "components/core"; import { PageDetailsHeader } from "components/headers/page-details"; // ui import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor"; @@ -256,113 +256,116 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); return pageIdMobx ? ( -
-
- {isPageReadOnly ? ( - - ) : ( -
- ( - { - 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} - /> - )} + <> + +
+
+ {isPageReadOnly ? ( + - {projectId && envConfig?.has_openai_configured && ( -
- { - 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={ - - } - className="!min-w-[38rem]" - /> -
- )} -
- )} - + ) : ( +
+ ( + { + 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 && ( +
+ { + 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={ + + } + className="!min-w-[38rem]" + /> +
+ )} +
+ )} + +
-
+ ) : (
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index 5dad4ede0..fd2da0258 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -6,7 +6,7 @@ import useSWR from "swr"; import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; // hooks -import { useApplication, useEventTracker, useUser } from "hooks/store"; +import { useApplication, useEventTracker, useUser, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; import useUserAuth from "hooks/use-user-auth"; import useSize from "hooks/use-window-size"; @@ -24,6 +24,7 @@ import { PAGE_TABS_LIST } from "constants/page"; import { useProjectPages } from "hooks/store/use-project-page"; import { EUserWorkspaceRoles } from "constants/workspace"; import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { PageHead } from "components/core"; const AllPagesList = dynamic(() => import("components/pages").then((a) => a.AllPagesList), { ssr: false, @@ -63,7 +64,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { commandPalette: { toggleCreatePageModal }, } = useApplication(); const { setTrackElement } = useEventTracker(); - + const { getProjectById } = useProject(); const { fetchProjectPages, fetchArchivedProjectPages, loader, archivedPageLoader, projectPageIds, archivedPageIds } = useProjectPages(); // hooks @@ -101,10 +102,12 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { } }; + // derived values const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", isLightMode); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + const project = projectId ? getProjectById(projectId.toString()) : undefined; + const pageTitle = project?.name ? `${project?.name} - Pages` : undefined; const MobileTabList = () => ( @@ -129,6 +132,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { return ( <> + {projectPageIds && archivedPageIds && projectPageIds.length + archivedPageIds.length > 0 ? ( <> {workspaceSlug && projectId && ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx index f17b7cc8a..8c4780cba 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx @@ -10,6 +10,7 @@ import { ProjectSettingLayout } from "layouts/settings-layout"; import useToast from "hooks/use-toast"; // components import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation"; +import { PageHead } from "components/core"; import { ProjectSettingHeader } from "components/headers"; // types import { NextPageWithLayout } from "lib/types"; @@ -41,16 +42,21 @@ const AutomationSettingsPage: NextPageWithLayout = observer(() => { }); }; + // derived values const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; + const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Automations` : undefined; return ( -
-
-

Automations

-
- - -
+ <> + +
+
+

Automations

+
+ + +
+ ); }); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx index bc90c110f..3aea45adb 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx @@ -1,11 +1,12 @@ import { ReactElement } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useUser } from "hooks/store"; +import { useUser, useProject } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components +import { PageHead } from "components/core"; import { ProjectSettingHeader } from "components/headers"; import { EstimatesList } from "components/estimates"; // types @@ -17,13 +18,18 @@ const EstimatesSettingsPage: NextPageWithLayout = observer(() => { const { membership: { currentProjectRole }, } = useUser(); - + const { currentProjectDetails } = useProject(); + // derived values const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; + const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Estimates` : undefined; return ( -
- -
+ <> + +
+ +
+ ); }); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx index a6f66d963..b618437ab 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx @@ -1,41 +1,48 @@ import { ReactElement } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; +import { observer } from "mobx-react"; // hooks -import { useUser } from "hooks/store"; +import { useProject, useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components +import { PageHead } from "components/core"; import { ProjectSettingHeader } from "components/headers"; import { ProjectFeaturesList } from "components/project"; // types import { NextPageWithLayout } from "lib/types"; -const FeaturesSettingsPage: NextPageWithLayout = () => { +const FeaturesSettingsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; // store const { membership: { fetchUserProjectInfo }, } = useUser(); - + const { currentProjectDetails } = useProject(); + // fetch the project details const { data: memberDetails } = useSWR( workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null, workspaceSlug && projectId ? () => fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null ); - + // derived values const isAdmin = memberDetails?.role === 20; + const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Features` : undefined; return ( -
-
-

Features

-
- -
+ <> + +
+
+

Features

+
+ +
+ ); -}; +}); FeaturesSettingsPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx index 72b8ce6f5..347d64f84 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx @@ -8,6 +8,7 @@ import { useProject } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components +import { PageHead } from "components/core"; import { ProjectSettingHeader } from "components/headers"; import { DeleteProjectModal, @@ -32,14 +33,15 @@ const GeneralSettingsPage: NextPageWithLayout = observer(() => { workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId}` : null, workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) : null ); - + // derived values + const isAdmin = currentProjectDetails?.member_role === 20; + const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - General Settings` : undefined; // const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network); // const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network")); - const isAdmin = currentProjectDetails?.member_role === 20; - return ( <> + {currentProjectDetails && ( { +const ProjectIntegrationsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; // theme const { resolvedTheme } = useTheme(); // store hooks const { currentUser } = useUser(); - + // fetch project details const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null ); - + // fetch Integrations list const { data: workspaceIntegrations } = useSWR( workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null, () => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null) ); - + // derived values const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["integrations"]; const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; const emptyStateImage = getEmptyStateImagePath("project-settings", "integrations", isLightMode); - const isAdmin = projectDetails?.member_role === 20; + const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Integrations` : undefined; return ( -
-
-

Integrations

-
- {workspaceIntegrations ? ( - workspaceIntegrations.length > 0 ? ( -
- {workspaceIntegrations.map((integration) => ( - - ))} -
+ <> + +
+
+

Integrations

+
+ {workspaceIntegrations ? ( + workspaceIntegrations.length > 0 ? ( +
+ {workspaceIntegrations.map((integration) => ( + + ))} +
+ ) : ( +
+ router.push(`/${workspaceSlug}/settings/integrations`), + }} + size="lg" + disabled={!isAdmin} + /> +
+ ) ) : ( -
- router.push(`/${workspaceSlug}/settings/integrations`), - }} - size="lg" - disabled={!isAdmin} - /> -
- ) - ) : ( - - )} -
+ + )} +
+ ); -}; +}); ProjectIntegrationsPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx index 02a700bbe..3bb1c8c04 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx @@ -1,18 +1,30 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components +import { PageHead } from "components/core"; import { ProjectSettingsLabelList } from "components/labels"; import { ProjectSettingHeader } from "components/headers"; // types import { NextPageWithLayout } from "lib/types"; +// hooks +import { useProject } from "hooks/store"; -const LabelsSettingsPage: NextPageWithLayout = () => ( -
- -
-); +const LabelsSettingsPage: NextPageWithLayout = observer(() => { + const { currentProjectDetails } = useProject(); + const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Labels` : undefined; + + return ( + <> + +
+ +
+ + ); +}); LabelsSettingsPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx index 3b66f40bb..f74d464d5 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx @@ -1,19 +1,33 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components +import { PageHead } from "components/core"; import { ProjectSettingHeader } from "components/headers"; import { ProjectMemberList, ProjectSettingsMemberDefaults } from "components/project"; // types import { NextPageWithLayout } from "lib/types"; +// hooks +import { useProject } from "hooks/store"; -const MembersSettingsPage: NextPageWithLayout = () => ( -
- - -
-); +const MembersSettingsPage: NextPageWithLayout = observer(() => { + // store + const { currentProjectDetails } = useProject(); + // derived values + const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined; + + return ( + <> + +
+ + +
+ + ); +}); MembersSettingsPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx index 51c811195..2ac6b2e00 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx @@ -1,13 +1,15 @@ import { ReactElement } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; +import { observer } from "mobx-react"; // hooks -import { useProjectView } from "hooks/store"; +import { useProject, useProjectView } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // components import { ProjectViewLayoutRoot } from "components/issues"; import { ProjectViewIssuesHeader } from "components/headers"; +import { PageHead } from "components/core"; // ui import { EmptyState } from "components/common"; // assets @@ -15,12 +17,17 @@ import emptyView from "public/empty-state/view.svg"; // types import { NextPageWithLayout } from "lib/types"; -const ProjectViewIssuesPage: NextPageWithLayout = () => { +const ProjectViewIssuesPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId, viewId } = router.query; // store hooks - const { fetchViewDetails } = useProjectView(); + const { fetchViewDetails, getViewById } = useProjectView(); + const { getProjectById } = useProject(); + // derived values + const projectView = viewId ? getViewById(viewId.toString()) : undefined; + const project = projectId ? getProjectById(projectId.toString()) : undefined; + const pageTitle = project?.name && projectView?.name ? `${project?.name} - ${projectView?.name}` : undefined; const { error } = useSWR( workspaceSlug && projectId && viewId ? `VIEW_DETAILS_${viewId.toString()}` : null, @@ -42,11 +49,14 @@ const ProjectViewIssuesPage: NextPageWithLayout = () => { }} /> ) : ( - + <> + + + )} ); -}; +}); ProjectViewIssuesPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx index 239318763..33be5d102 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx @@ -1,13 +1,34 @@ import { ReactElement } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react"; // components import { ProjectViewsHeader } from "components/headers"; import { ProjectViewsList } from "components/views"; +import { PageHead } from "components/core"; +// hooks +import { useProject } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // types import { NextPageWithLayout } from "lib/types"; -const ProjectViewsPage: NextPageWithLayout = () => ; +const ProjectViewsPage: NextPageWithLayout = observer(() => { + // router + const router = useRouter(); + const { projectId } = router.query; + // store + const { getProjectById } = useProject(); + // derived values + const project = projectId ? getProjectById(projectId.toString()) : undefined; + const pageTitle = project?.name ? `${project?.name} - Views` : undefined; + + return ( + <> + + + + ); +}); ProjectViewsPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/[workspaceSlug]/projects/index.tsx b/web/pages/[workspaceSlug]/projects/index.tsx index 734098280..1a145a2d1 100644 --- a/web/pages/[workspaceSlug]/projects/index.tsx +++ b/web/pages/[workspaceSlug]/projects/index.tsx @@ -1,13 +1,28 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; // components +import { PageHead } from "components/core"; import { ProjectCardList } from "components/project"; import { ProjectsHeader } from "components/headers"; // layouts import { AppLayout } from "layouts/app-layout"; // type import { NextPageWithLayout } from "lib/types"; +import { useWorkspace } from "hooks/store"; -const ProjectsPage: NextPageWithLayout = () => ; +const ProjectsPage: NextPageWithLayout = observer(() => { + // store + const { currentWorkspace } = useWorkspace(); + // derived values + const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Projects` : undefined; + + return ( + <> + + + + ); +}); ProjectsPage.getLayout = function getLayout(page: ReactElement) { return }>{page}; diff --git a/web/pages/[workspaceSlug]/settings/api-tokens.tsx b/web/pages/[workspaceSlug]/settings/api-tokens.tsx index 3d65c2d7b..1f203ff04 100644 --- a/web/pages/[workspaceSlug]/settings/api-tokens.tsx +++ b/web/pages/[workspaceSlug]/settings/api-tokens.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; import useSWR from "swr"; import { useTheme } from "next-themes"; // store hooks -import { useUser } from "hooks/store"; +import { useUser, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; @@ -23,6 +23,7 @@ import { NextPageWithLayout } from "lib/types"; import { API_TOKENS_LIST } from "constants/fetch-keys"; import { EUserWorkspaceRoles } from "constants/workspace"; import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { PageHead } from "components/core"; const apiTokenService = new APITokenService(); @@ -39,6 +40,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => { membership: { currentWorkspaceRole }, currentUser, } = useUser(); + const { currentWorkspace } = useWorkspace(); const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; @@ -49,12 +51,16 @@ const ApiTokensPage: NextPageWithLayout = observer(() => { const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["api-tokens"]; const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; const emptyStateImage = getEmptyStateImagePath("workspace-settings", "api-tokens", isLightMode); + const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined; if (!isAdmin) return ( -
-

You are not authorized to access this page.

-
+ <> + +
+

You are not authorized to access this page.

+
+ ); if (!tokens) { @@ -63,6 +69,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => { return ( <> + setIsCreateTokenModalOpen(false)} />
{tokens.length > 0 ? ( diff --git a/web/pages/[workspaceSlug]/settings/billing.tsx b/web/pages/[workspaceSlug]/settings/billing.tsx index 7cc9c7a28..f4f5d5397 100644 --- a/web/pages/[workspaceSlug]/settings/billing.tsx +++ b/web/pages/[workspaceSlug]/settings/billing.tsx @@ -1,11 +1,12 @@ import { observer } from "mobx-react-lite"; // hooks -import { useUser } from "hooks/store"; +import { useUser, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // component import { WorkspaceSettingHeader } from "components/headers"; +import { PageHead } from "components/core"; // ui import { Button } from "@plane/ui"; // types @@ -18,33 +19,41 @@ const BillingSettingsPage: NextPageWithLayout = observer(() => { const { membership: { currentWorkspaceRole }, } = useUser(); - + const { currentWorkspace } = useWorkspace(); + // derived values const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined; if (!isAdmin) return ( -
-

You are not authorized to access this page.

-
+ <> + +
+

You are not authorized to access this page.

+
+ ); return ( -
-
-
-

Billing & Plans

-
-
-
+ <> + +
-

Current plan

-

You are currently using the free plan

- - - +
+

Billing & Plans

+
-
-
+
+
+

Current plan

+

You are currently using the free plan

+ + + +
+
+
+ ); }); diff --git a/web/pages/[workspaceSlug]/settings/exports.tsx b/web/pages/[workspaceSlug]/settings/exports.tsx index f491581fd..c124a6423 100644 --- a/web/pages/[workspaceSlug]/settings/exports.tsx +++ b/web/pages/[workspaceSlug]/settings/exports.tsx @@ -1,12 +1,13 @@ import { observer } from "mobx-react-lite"; // hooks -import { useUser } from "hooks/store"; +import { useUser, useWorkspace } from "hooks/store"; // layout import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components import { WorkspaceSettingHeader } from "components/headers"; import ExportGuide from "components/exporter/guide"; +import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // constants @@ -17,24 +18,33 @@ const ExportsPage: NextPageWithLayout = observer(() => { const { membership: { currentWorkspaceRole }, } = useUser(); + const { currentWorkspace } = useWorkspace(); + // derived values const hasPageAccess = currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole); + const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Exports` : undefined; if (!hasPageAccess) return ( -
-

You are not authorized to access this page.

-
+ <> + +
+

You are not authorized to access this page.

+
+ ); return ( -
-
-

Exports

+ <> + +
+
+

Exports

+
+
- -
+ ); }); diff --git a/web/pages/[workspaceSlug]/settings/imports.tsx b/web/pages/[workspaceSlug]/settings/imports.tsx index 2e4be4cc1..5178209d2 100644 --- a/web/pages/[workspaceSlug]/settings/imports.tsx +++ b/web/pages/[workspaceSlug]/settings/imports.tsx @@ -1,12 +1,13 @@ import { observer } from "mobx-react-lite"; // hooks -import { useUser } from "hooks/store"; +import { useUser, useWorkspace } from "hooks/store"; // layouts import { WorkspaceSettingLayout } from "layouts/settings-layout"; import { AppLayout } from "layouts/app-layout"; // components import IntegrationGuide from "components/integration/guide"; import { WorkspaceSettingHeader } from "components/headers"; +import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // constants @@ -17,23 +18,32 @@ const ImportsPage: NextPageWithLayout = observer(() => { const { membership: { currentWorkspaceRole }, } = useUser(); + const { currentWorkspace } = useWorkspace(); + // derived values const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Imports` : undefined; if (!isAdmin) return ( -
-

You are not authorized to access this page.

-
+ <> + +
+

You are not authorized to access this page.

+
+ ); return ( -
-
-

Imports

-
- -
+ <> + +
+
+

Imports

+
+ +
+ ); }); diff --git a/web/pages/[workspaceSlug]/settings/index.tsx b/web/pages/[workspaceSlug]/settings/index.tsx index b2920aade..2924b13c4 100644 --- a/web/pages/[workspaceSlug]/settings/index.tsx +++ b/web/pages/[workspaceSlug]/settings/index.tsx @@ -1,14 +1,30 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; +// hooks +import { useWorkspace } from "hooks/store"; // components import { WorkspaceSettingHeader } from "components/headers"; import { WorkspaceDetails } from "components/workspace"; +import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; -const WorkspaceSettingsPage: NextPageWithLayout = () => ; +const WorkspaceSettingsPage: NextPageWithLayout = observer(() => { + // store hooks + const { currentWorkspace } = useWorkspace(); + // derived values + const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - General Settings` : undefined; + + return ( + <> + + + + ); +}); WorkspaceSettingsPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/[workspaceSlug]/settings/integrations.tsx b/web/pages/[workspaceSlug]/settings/integrations.tsx index 940c90f3a..500533877 100644 --- a/web/pages/[workspaceSlug]/settings/integrations.tsx +++ b/web/pages/[workspaceSlug]/settings/integrations.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; // hooks -import { useUser } from "hooks/store"; +import { useUser, useWorkspace } from "hooks/store"; // services import { IntegrationService } from "services/integrations"; // layouts @@ -12,6 +12,7 @@ import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components import { SingleIntegrationCard } from "components/integration"; import { WorkspaceSettingHeader } from "components/headers"; +import { PageHead } from "components/core"; // ui import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "components/ui"; // types @@ -31,14 +32,20 @@ const WorkspaceIntegrationsPage: NextPageWithLayout = observer(() => { const { membership: { currentWorkspaceRole }, } = useUser(); + const { currentWorkspace } = useWorkspace(); + // derived values const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Integrations` : undefined; if (!isAdmin) return ( -
-

You are not authorized to access this page.

-
+ <> + +
+

You are not authorized to access this page.

+
+ ); const { data: appIntegrations } = useSWR(workspaceSlug && isAdmin ? APP_INTEGRATIONS : null, () => @@ -46,16 +53,21 @@ const WorkspaceIntegrationsPage: NextPageWithLayout = observer(() => { ); return ( -
- -
- {appIntegrations ? ( - appIntegrations.map((integration) => ) - ) : ( - - )} -
-
+ <> + +
+ +
+ {appIntegrations ? ( + appIntegrations.map((integration) => ( + + )) + ) : ( + + )} +
+
+ ); }); diff --git a/web/pages/[workspaceSlug]/settings/members.tsx b/web/pages/[workspaceSlug]/settings/members.tsx index 6e9d8d924..b8739ae77 100644 --- a/web/pages/[workspaceSlug]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/settings/members.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Search } from "lucide-react"; // hooks -import { useEventTracker, useMember, useUser } from "hooks/store"; +import { useEventTracker, useMember, useUser, useWorkspace } from "hooks/store"; import useToast from "hooks/use-toast"; // layouts import { AppLayout } from "layouts/app-layout"; @@ -11,6 +11,7 @@ import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components import { WorkspaceSettingHeader } from "components/headers"; import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace"; +import { PageHead } from "components/core"; // ui import { Button } from "@plane/ui"; // types @@ -37,6 +38,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { const { workspace: { inviteMembersToWorkspace }, } = useMember(); + const { currentWorkspace } = useWorkspace(); // toast alert const { setToastAlert } = useToast(); @@ -83,11 +85,14 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { }); }; + // derived values const hasAddMemberPermission = currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole); + const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined; return ( <> + setInviteModal(false)} diff --git a/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx b/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx index 562578b66..60e65e905 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; // hooks -import { useUser, useWebhook } from "hooks/store"; +import { useUser, useWebhook, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; @@ -12,6 +12,7 @@ import useToast from "hooks/use-toast"; // components import { WorkspaceSettingHeader } from "components/headers"; import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks"; +import { PageHead } from "components/core"; // ui import { Spinner } from "@plane/ui"; // types @@ -29,6 +30,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => { membership: { currentWorkspaceRole }, } = useUser(); const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook(); + const { currentWorkspace } = useWorkspace(); // toast const { setToastAlert } = useToast(); @@ -38,6 +40,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => { // }, [clearSecretKey, isCreated]); const isAdmin = currentWorkspaceRole === 20; + const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhook` : undefined; useSWR( workspaceSlug && webhookId && isAdmin ? `WEBHOOK_DETAILS_${workspaceSlug}_${webhookId}` : null, @@ -76,9 +79,12 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => { if (!isAdmin) return ( -
-

You are not authorized to access this page.

-
+ <> + +
+

You are not authorized to access this page.

+
+ ); if (!currentWebhook) @@ -90,6 +96,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => { return ( <> + setDeleteWebhookModal(false)} />
await handleUpdateWebhook(data)} data={currentWebhook} /> diff --git a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx index cafac485e..46c7e99cb 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx @@ -19,6 +19,7 @@ import { WebhookSettingsLoader } from "components/ui"; import { NextPageWithLayout } from "lib/types"; // constants import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { PageHead } from "components/core"; const WebhooksListPage: NextPageWithLayout = observer(() => { // states @@ -47,6 +48,7 @@ const WebhooksListPage: NextPageWithLayout = observer(() => { const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; const emptyStateImage = getEmptyStateImagePath("workspace-settings", "webhooks", isLightMode); + const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhooks` : undefined; // clear secret key when modal is closed. useEffect(() => { @@ -55,53 +57,59 @@ const WebhooksListPage: NextPageWithLayout = observer(() => { if (!isAdmin) return ( -
-

You are not authorized to access this page.

-
+ <> + +
+

You are not authorized to access this page.

+
+ ); if (!webhooks) return ; return ( -
- { - setShowCreateWebhookModal(false); - }} - /> - {Object.keys(webhooks).length > 0 ? ( -
-
-
Webhooks
- + <> + +
+ { + setShowCreateWebhookModal(false); + }} + /> + {Object.keys(webhooks).length > 0 ? ( +
+
+
Webhooks
+ +
+
- -
- ) : ( -
-
-
Webhooks
- + ) : ( +
+
+
Webhooks
+ +
+
+ +
-
- -
-
- )} -
+ )} +
+ ); }); diff --git a/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx b/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx index e89e2c70f..43a0ad494 100644 --- a/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx +++ b/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx @@ -7,15 +7,26 @@ import { AllIssueLayoutRoot } from "components/issues"; import { GlobalIssuesHeader } from "components/headers"; // types import { NextPageWithLayout } from "lib/types"; +import { observer } from "mobx-react"; +import { useWorkspace } from "hooks/store"; +import { PageHead } from "components/core"; -const GlobalViewIssuesPage: NextPageWithLayout = () => ( -
-
- - -
-
-); +const GlobalViewIssuesPage: NextPageWithLayout = observer(() => { + const { currentWorkspace } = useWorkspace(); + // derived values + const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Views` : undefined; + return ( + <> + +
+
+ + +
+
+ + ); +}); GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) { return }>{page}; diff --git a/web/pages/[workspaceSlug]/workspace-views/index.tsx b/web/pages/[workspaceSlug]/workspace-views/index.tsx index db1a6fca7..656120a3e 100644 --- a/web/pages/[workspaceSlug]/workspace-views/index.tsx +++ b/web/pages/[workspaceSlug]/workspace-views/index.tsx @@ -1,7 +1,9 @@ import React, { useState, ReactElement } from "react"; +import { observer } from "mobx-react"; // layouts import { AppLayout } from "layouts/app-layout"; // components +import { PageHead } from "components/core"; import { GlobalDefaultViewListItem, GlobalViewsList } from "components/workspace"; import { GlobalIssuesHeader } from "components/headers"; // ui @@ -12,31 +14,40 @@ import { Search } from "lucide-react"; import { NextPageWithLayout } from "lib/types"; // constants import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace"; +// hooks +import { useWorkspace } from "hooks/store"; -const WorkspaceViewsPage: NextPageWithLayout = () => { +const WorkspaceViewsPage: NextPageWithLayout = observer(() => { const [query, setQuery] = useState(""); + // store + const { currentWorkspace } = useWorkspace(); + // derived values + const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - All Views` : undefined; return ( -
-
-
- - setQuery(e.target.value)} - placeholder="Search" - mode="true-transparent" - /> + <> + +
+
+
+ + setQuery(e.target.value)} + placeholder="Search" + mode="true-transparent" + /> +
+ {DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => v.label.toLowerCase().includes(query.toLowerCase())).map((option) => ( + + ))} +
- {DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => v.label.toLowerCase().includes(query.toLowerCase())).map((option) => ( - - ))} - -
+ ); -}; +}); WorkspaceViewsPage.getLayout = function getLayout(page: ReactElement) { return }>{page}; diff --git a/web/pages/accounts/forgot-password.tsx b/web/pages/accounts/forgot-password.tsx index 07fa86045..0eef16009 100644 --- a/web/pages/accounts/forgot-password.tsx +++ b/web/pages/accounts/forgot-password.tsx @@ -12,6 +12,7 @@ import { useEventTracker } from "hooks/store"; import DefaultLayout from "layouts/default-layout"; // components import { LatestFeatureBlock } from "components/common"; +import { PageHead } from "components/core"; // ui import { Button, Input } from "@plane/ui"; // images @@ -85,59 +86,62 @@ const ForgotPasswordPage: NextPageWithLayout = () => { }; return ( -
-
-
- Plane Logo - Plane -
-
- -
-
-
-

- Get on your flight deck -

-

Get a link to reset your password

- - checkEmailValidity(value) || "Email is invalid", - }} - render={({ field: { value, onChange, ref } }) => ( - - )} - /> - - + <> + +
+
+
+ Plane Logo + Plane +
+
+ +
+
+
+

+ Get on your flight deck +

+

Get a link to reset your password

+
+ checkEmailValidity(value) || "Email is invalid", + }} + render={({ field: { value, onChange, ref } }) => ( + + )} + /> + + +
+
-
-
+ ); }; diff --git a/web/pages/accounts/reset-password.tsx b/web/pages/accounts/reset-password.tsx index c4258f39e..c848245ac 100644 --- a/web/pages/accounts/reset-password.tsx +++ b/web/pages/accounts/reset-password.tsx @@ -12,6 +12,7 @@ import { useEventTracker } from "hooks/store"; import DefaultLayout from "layouts/default-layout"; // components import { LatestFeatureBlock } from "components/common"; +import { PageHead } from "components/core"; // ui import { Button, Input } from "@plane/ui"; // images @@ -90,90 +91,93 @@ const ResetPasswordPage: NextPageWithLayout = () => { }; return ( -
-
-
- Plane Logo - Plane + <> + +
+
+
+ Plane Logo + Plane +
-
-
-
-
-

- Let{"'"}s get a new password -

-
- checkEmailValidity(value) || "Email is invalid", - }} - render={({ field: { value, onChange, ref } }) => ( - - )} - /> - ( -
+
+
+
+

+ Let{"'"}s get a new password +

+ + checkEmailValidity(value) || "Email is invalid", + }} + render={({ field: { value, onChange, ref } }) => ( - {showPassword ? ( - setShowPassword(false)} + )} + /> + ( +
+ - ) : ( - setShowPassword(true)} - /> - )} -
- )} - /> - - + {showPassword ? ( + setShowPassword(false)} + /> + ) : ( + setShowPassword(true)} + /> + )} +
+ )} + /> + + +
+
-
-
+ ); }; diff --git a/web/pages/accounts/sign-up.tsx b/web/pages/accounts/sign-up.tsx index 5b5648439..cba9c0166 100644 --- a/web/pages/accounts/sign-up.tsx +++ b/web/pages/accounts/sign-up.tsx @@ -7,6 +7,7 @@ import { useApplication, useUser } from "hooks/store"; import DefaultLayout from "layouts/default-layout"; // components import { SignUpRoot } from "components/account"; +import { PageHead } from "components/core"; // ui import { Spinner } from "@plane/ui"; // assets @@ -29,20 +30,23 @@ const SignUpPage: NextPageWithLayout = observer(() => { ); return ( -
-
-
- Plane Logo - Plane + <> + +
+
+
+ Plane Logo + Plane +
-
-
-
- +
+
+ +
-
+ ); }); diff --git a/web/pages/create-workspace.tsx b/web/pages/create-workspace.tsx index 10eb11f55..952ed0b68 100644 --- a/web/pages/create-workspace.tsx +++ b/web/pages/create-workspace.tsx @@ -11,6 +11,7 @@ import DefaultLayout from "layouts/default-layout"; import { UserAuthWrapper } from "layouts/auth-layout"; // components import { CreateWorkspaceForm } from "components/workspace"; +import { PageHead } from "components/core"; // images import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; @@ -37,38 +38,41 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => { }; return ( -
-
-
- -
- {theme === "light" ? ( - Plane black logo - ) : ( - Plane white logo - )} + <> + +
+
+
+ +
+ {theme === "light" ? ( + Plane black logo + ) : ( + Plane white logo + )} +
+ +
+ {currentUser?.email}
- -
- {currentUser?.email}
-
-
-
-

Create your workspace

-
- +
+
+

Create your workspace

+
+ +
-
+ ); }); diff --git a/web/pages/god-mode/ai.tsx b/web/pages/god-mode/ai.tsx index 3ceb902c9..b84e98098 100644 --- a/web/pages/god-mode/ai.tsx +++ b/web/pages/god-mode/ai.tsx @@ -13,6 +13,7 @@ import { Loader } from "@plane/ui"; import { Lightbulb } from "lucide-react"; // components import { InstanceAIForm } from "components/instance"; +import { PageHead } from "components/core"; const InstanceAdminAIPage: NextPageWithLayout = observer(() => { // store @@ -23,37 +24,40 @@ const InstanceAdminAIPage: NextPageWithLayout = observer(() => { useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); return ( -
-
-
AI features for all your workspaces
-
- Configure your AI API credentials so Plane AI features are turned on for all your workspaces. + <> + +
+
+
AI features for all your workspaces
+
+ Configure your AI API credentials so Plane AI features are turned on for all your workspaces. +
-
- {formattedConfig ? ( - <> -
-
OpenAI
-
If you use ChatGPT, this is for you.
-
- -
-
- -
If you have a preferred AI models vendor, please get in touch with us.
+ {formattedConfig ? ( + <> +
+
OpenAI
+
If you use ChatGPT, this is for you.
+
+ +
+
+ +
If you have a preferred AI models vendor, please get in touch with us.
+
+
+ + ) : ( + +
+ +
-
- - ) : ( - -
- -
- -
- )} -
+ + )} +
+ ); }); diff --git a/web/pages/god-mode/authorization.tsx b/web/pages/god-mode/authorization.tsx index 5085ff61a..e36a1a455 100644 --- a/web/pages/god-mode/authorization.tsx +++ b/web/pages/god-mode/authorization.tsx @@ -14,6 +14,7 @@ import useToast from "hooks/use-toast"; import { Loader, ToggleSwitch } from "@plane/ui"; // components import { InstanceGithubConfigForm, InstanceGoogleConfigForm } from "components/instance"; +import { PageHead } from "components/core"; const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => { // store @@ -64,69 +65,71 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => { }; return ( -
-
-
Single sign-on and OAuth
-
- Make your teams life easy by letting them sign-up with their Google and GitHub accounts, and below are the - settings. + <> + +
+
+
Single sign-on and OAuth
+
+ Make your teams life easy by letting them sign-up with their Google and GitHub accounts, and below are the + settings. +
-
- {formattedConfig ? ( - <> -
-
-
-
- Turn Magic Links {Boolean(parseInt(enableMagicLogin)) ? "off" : "on"} + {formattedConfig ? ( + <> +
+
+
+
+ Turn Magic Links {Boolean(parseInt(enableMagicLogin)) ? "off" : "on"} +
+
+

Slack-like emails for authentication.

+ You need to have set up email{" "} + + here + {" "} + to enable this. +
-
-

Slack-like emails for authentication.

- You need to have set up email{" "} - - here - {" "} - to enable this. +
+ { + // Boolean(parseInt(enableMagicLogin)) === true + // ? updateConfig("ENABLE_MAGIC_LINK_LOGIN", "0") + // : updateConfig("ENABLE_MAGIC_LINK_LOGIN", "1"); + // }} + onChange={() => {}} + size="sm" + disabled={isSubmitting} + />
-
- { - // Boolean(parseInt(enableMagicLogin)) === true - // ? updateConfig("ENABLE_MAGIC_LINK_LOGIN", "0") - // : updateConfig("ENABLE_MAGIC_LINK_LOGIN", "1"); - // }} - onChange={() => {}} - size="sm" - disabled={isSubmitting} - /> -
-
-
-
-
- Let your users log in via the methods below +
+
+
+ Let your users log in via the methods below +
+
+ Toggling this off will disable all previous configs. Users will only be able to login with an e-mail + and password combo. +
-
- Toggling this off will disable all previous configs. Users will only be able to login with an e-mail - and password combo. +
+ { + Boolean(parseInt(enableSignup)) === true + ? updateConfig("ENABLE_SIGNUP", "0") + : updateConfig("ENABLE_SIGNUP", "1"); + }} + size="sm" + disabled={isSubmitting} + />
-
- { - Boolean(parseInt(enableSignup)) === true - ? updateConfig("ENABLE_SIGNUP", "0") - : updateConfig("ENABLE_SIGNUP", "1"); - }} - size="sm" - disabled={isSubmitting} - /> -
-
- {/*
+ {/*
Turn Email Password {Boolean(parseInt(enableEmailPassword)) ? "off" : "on"} @@ -146,36 +149,37 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => { />
*/} -
-
-
-
- Google +
+
+
+
+ Google +
+
+ +
-
- +
+
+ Github +
+
+ +
-
-
- Github -
-
- -
+ + ) : ( + +
+ +
-
- - ) : ( - -
- -
- -
- )} -
+ + )} +
+ ); }); diff --git a/web/pages/god-mode/email.tsx b/web/pages/god-mode/email.tsx index bea14a357..65889607f 100644 --- a/web/pages/god-mode/email.tsx +++ b/web/pages/god-mode/email.tsx @@ -11,6 +11,7 @@ import { useApplication } from "hooks/store"; import { Loader } from "@plane/ui"; // components import { InstanceEmailForm } from "components/instance"; +import { PageHead } from "components/core"; const InstanceAdminEmailPage: NextPageWithLayout = observer(() => { // store @@ -21,29 +22,32 @@ const InstanceAdminEmailPage: NextPageWithLayout = observer(() => { useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); return ( -
-
-
Secure emails from your own instance
-
- Plane can send useful emails to you and your users from your own instance without talking to the Internet. -
-
- Set it up below and please test your settings before you save them.{" "} - Misconfigs can lead to email bounces and errors. -
-
- {formattedConfig ? ( - - ) : ( - -
- - + <> + +
+
+
Secure emails from your own instance
+
+ Plane can send useful emails to you and your users from your own instance without talking to the Internet.
- - - )} -
+
+ Set it up below and please test your settings before you save them.{" "} + Misconfigs can lead to email bounces and errors. +
+
+ {formattedConfig ? ( + + ) : ( + +
+ + +
+ +
+ )} +
+ ); }); diff --git a/web/pages/god-mode/image.tsx b/web/pages/god-mode/image.tsx index f7cf21c2f..349dccf4b 100644 --- a/web/pages/god-mode/image.tsx +++ b/web/pages/god-mode/image.tsx @@ -11,6 +11,7 @@ import { useApplication } from "hooks/store"; import { Loader } from "@plane/ui"; // components import { InstanceImageConfigForm } from "components/instance"; +import { PageHead } from "components/core"; const InstanceAdminImagePage: NextPageWithLayout = observer(() => { // store @@ -21,25 +22,28 @@ const InstanceAdminImagePage: NextPageWithLayout = observer(() => { useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); return ( -
-
-
Third-party image libraries
-
- Let your users search and choose images from third-party libraries -
-
- {formattedConfig ? ( - - ) : ( - -
- - + <> + +
+
+
Third-party image libraries
+
+ Let your users search and choose images from third-party libraries
- - - )} -
+
+ {formattedConfig ? ( + + ) : ( + +
+ + +
+ +
+ )} +
+ ); }); diff --git a/web/pages/god-mode/index.tsx b/web/pages/god-mode/index.tsx index 35a8ed0c4..a93abad31 100644 --- a/web/pages/god-mode/index.tsx +++ b/web/pages/god-mode/index.tsx @@ -11,6 +11,7 @@ import { useApplication } from "hooks/store"; import { Loader } from "@plane/ui"; // components import { InstanceGeneralForm } from "components/instance"; +import { PageHead } from "components/core"; const InstanceAdminPage: NextPageWithLayout = observer(() => { // store hooks @@ -22,26 +23,29 @@ const InstanceAdminPage: NextPageWithLayout = observer(() => { useSWR("INSTANCE_ADMINS", () => fetchInstanceAdmins()); return ( -
-
-
ID your instance easily
-
- Change the name of your instance and instance admin e-mail addresses. If you have a paid subscription, you - will find your license key here. -
-
- {instance && instanceAdmins ? ( - - ) : ( - -
- - + <> + +
+
+
ID your instance easily
+
+ Change the name of your instance and instance admin e-mail addresses. If you have a paid subscription, you + will find your license key here.
- - - )} -
+
+ {instance && instanceAdmins ? ( + + ) : ( + +
+ + +
+ +
+ )} +
+ ); }); diff --git a/web/pages/invitations/index.tsx b/web/pages/invitations/index.tsx index 26ced2010..b5acec196 100644 --- a/web/pages/invitations/index.tsx +++ b/web/pages/invitations/index.tsx @@ -32,7 +32,7 @@ import { ROLE } from "constants/workspace"; import { MEMBER_ACCEPTED } from "constants/event-tracker"; // components import { EmptyState } from "components/common"; - +import { PageHead } from "components/core"; // services const workspaceService = new WorkspaceService(); const userService = new UserService(); @@ -126,108 +126,111 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { }; return ( -
-
-
-
-
- {theme === "light" ? ( - Plane black logo - ) : ( - Plane white logo - )} -
-
-
- {currentUser?.email} -
-
- {invitations ? ( - invitations.length > 0 ? ( -
-
-
We see that someone has invited you to
-

Join a workspace

-
- {invitations.map((invitation) => { - const isSelected = invitationsRespond.includes(invitation.id); - - return ( -
handleInvitation(invitation, isSelected ? "withdraw" : "accepted")} - > -
-
- {invitation.workspace.logo && invitation.workspace.logo.trim() !== "" ? ( - {invitation.workspace.name} - ) : ( - - {invitation.workspace.name[0]} - - )} -
-
-
-
{truncateText(invitation.workspace.name, 30)}
-

{ROLE[invitation.role]}

-
- - - -
- ); - })} -
-
- - - - - - -
+ <> + +
+
+
+
+
+ {theme === "light" ? ( + Plane black logo + ) : ( + Plane white logo + )}
- ) : ( -
- router.push("/"), - }} - /> +
+ {currentUser?.email}
- ) - ) : null} -
+
+ {invitations ? ( + invitations.length > 0 ? ( +
+
+
We see that someone has invited you to
+

Join a workspace

+
+ {invitations.map((invitation) => { + const isSelected = invitationsRespond.includes(invitation.id); + + return ( +
handleInvitation(invitation, isSelected ? "withdraw" : "accepted")} + > +
+
+ {invitation.workspace.logo && invitation.workspace.logo.trim() !== "" ? ( + {invitation.workspace.name} + ) : ( + + {invitation.workspace.name[0]} + + )} +
+
+
+
{truncateText(invitation.workspace.name, 30)}
+

{ROLE[invitation.role]}

+
+ + + +
+ ); + })} +
+
+ + + + + + +
+
+
+ ) : ( +
+ router.push("/"), + }} + /> +
+ ) + ) : null} +
+ ); }); diff --git a/web/pages/onboarding/index.tsx b/web/pages/onboarding/index.tsx index 99886156d..5b5b91280 100644 --- a/web/pages/onboarding/index.tsx +++ b/web/pages/onboarding/index.tsx @@ -17,6 +17,7 @@ import DefaultLayout from "layouts/default-layout"; import { UserAuthWrapper } from "layouts/auth-layout"; // components import { InviteMembers, JoinWorkspaces, UserDetails, SwitchOrDeleteAccountModal } from "components/onboarding"; +import { PageHead } from "components/core"; // ui import { Avatar, Spinner } from "@plane/ui"; // images @@ -142,6 +143,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => { return ( <> + setShowDeleteAccountModal(false)} /> {user && step !== null ? (
diff --git a/web/pages/profile/activity.tsx b/web/pages/profile/activity.tsx index 051127dd3..4460f2ec5 100644 --- a/web/pages/profile/activity.tsx +++ b/web/pages/profile/activity.tsx @@ -9,7 +9,7 @@ import { UserService } from "services/user.service"; // layouts import { ProfileSettingsLayout } from "layouts/settings-layout"; // components -import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; +import { ActivityIcon, ActivityMessage, IssueLink, PageHead } from "components/core"; import { RichReadOnlyEditor } from "@plane/rich-text-editor"; // icons import { History, MessageSquare } from "lucide-react"; @@ -32,159 +32,162 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => { const { theme: themeStore } = useApplication(); return ( -
-
- themeStore.toggleSidebar()} /> -

Activity

-
- {userActivity ? ( -
-
    - {userActivity.results.map((activityItem: any) => { - if (activityItem.field === "comment") { - return ( -
    -
    -
    - {activityItem.field ? ( - activityItem.new_value === "restore" && ( - - ) - ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( - {activityItem.actor_detail.display_name} - ) : ( -
    - {activityItem.actor_detail.display_name?.charAt(0)} -
    - )} + <> + +
    +
    + themeStore.toggleSidebar()} /> +

    Activity

    +
    + {userActivity ? ( +
    +
      + {userActivity.results.map((activityItem: any) => { + if (activityItem.field === "comment") { + return ( +
      +
      +
      + {activityItem.field ? ( + activityItem.new_value === "restore" && ( + + ) + ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( + {activityItem.actor_detail.display_name} + ) : ( +
      + {activityItem.actor_detail.display_name?.charAt(0)} +
      + )} - - -
      -
      -
      -
      - {activityItem.actor_detail.is_bot - ? activityItem.actor_detail.first_name + " Bot" - : activityItem.actor_detail.display_name} -
      -

      - Commented {calculateTimeAgo(activityItem.created_at)} -

      + +
      -
      - +
      +
      +
      + {activityItem.actor_detail.is_bot + ? activityItem.actor_detail.first_name + " Bot" + : activityItem.actor_detail.display_name} +
      +

      + Commented {calculateTimeAgo(activityItem.created_at)} +

      +
      +
      + +
      -
      - ); - } + ); + } - const message = - activityItem.verb === "created" && + const message = + activityItem.verb === "created" && activityItem.field !== "cycles" && activityItem.field !== "modules" && activityItem.field !== "attachment" && activityItem.field !== "link" && activityItem.field !== "estimate" && !activityItem.field ? ( - - created - - ) : ( - - ); + + created + + ) : ( + + ); - if ("field" in activityItem && activityItem.field !== "updated_by") { - return ( -
    • -
      -
      - <> -
      -
      -
      -
      - {activityItem.field ? ( - activityItem.new_value === "restore" ? ( - + if ("field" in activityItem && activityItem.field !== "updated_by") { + return ( +
    • +
      +
      + <> +
      +
      +
      +
      + {activityItem.field ? ( + activityItem.new_value === "restore" ? ( + + ) : ( + + ) + ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( + {activityItem.actor_detail.display_name} ) : ( - - ) - ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( - {activityItem.actor_detail.display_name} - ) : ( -
      - {activityItem.actor_detail.display_name?.charAt(0)} -
      - )} +
      + {activityItem.actor_detail.display_name?.charAt(0)} +
      + )} +
      -
      -
      -
      - {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( - Plane - ) : activityItem.actor_detail.is_bot ? ( - - {activityItem.actor_detail.first_name} Bot - - ) : ( - +
      +
      + {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( + Plane + ) : activityItem.actor_detail.is_bot ? ( - {currentUser?.id === activityItem.actor_detail.id - ? "You" - : activityItem.actor_detail.display_name} + {activityItem.actor_detail.first_name} Bot - - )}{" "} -
      - {message}{" "} - - {calculateTimeAgo(activityItem.created_at)} - + ) : ( + + + {currentUser?.id === activityItem.actor_detail.id + ? "You" + : activityItem.actor_detail.display_name} + + + )}{" "} +
      + {message}{" "} + + {calculateTimeAgo(activityItem.created_at)} + +
      -
      - + +
      -
      -
    • - ); - } - })} -
    -
    - ) : ( - - )} -
    + + ); + } + })} +
+
+ ) : ( + + )} +
+ ); }); diff --git a/web/pages/profile/change-password.tsx b/web/pages/profile/change-password.tsx index 15cb946ed..80e2965d6 100644 --- a/web/pages/profile/change-password.tsx +++ b/web/pages/profile/change-password.tsx @@ -6,6 +6,8 @@ import { Controller, useForm } from "react-hook-form"; import { useApplication, useUser } from "hooks/store"; // services import { UserService } from "services/user.service"; +// components +import { PageHead } from "components/core"; // hooks import useToast from "hooks/use-toast"; // layout @@ -88,93 +90,98 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => { ); return ( -
-
- themeStore.toggleSidebar()} /> + <> + +
+
+ themeStore.toggleSidebar()} /> +
+
+

Change password

+
+
+

Current password

+ ( + + )} + /> + {errors.old_password && {errors.old_password.message}} +
+ +
+

New password

+ ( + + )} + /> + {errors.new_password && {errors.new_password.message}} +
+ +
+

Confirm password

+ ( + + )} + /> + {errors.confirm_password && ( + {errors.confirm_password.message} + )} +
+
+ +
+ +
+
-
-

Change password

-
-
-

Current password

- ( - - )} - /> - {errors.old_password && {errors.old_password.message}} -
- -
-

New password

- ( - - )} - /> - {errors.new_password && {errors.new_password.message}} -
- -
-

Confirm password

- ( - - )} - /> - {errors.confirm_password && {errors.confirm_password.message}} -
-
- -
- -
-
-
+ ); }); diff --git a/web/pages/profile/index.tsx b/web/pages/profile/index.tsx index da8626dd1..e967df828 100644 --- a/web/pages/profile/index.tsx +++ b/web/pages/profile/index.tsx @@ -11,7 +11,7 @@ import useToast from "hooks/use-toast"; // layouts import { ProfileSettingsLayout } from "layouts/settings-layout"; // components -import { ImagePickerPopover, UserImageUploadModal } from "components/core"; +import { ImagePickerPopover, UserImageUploadModal, PageHead } from "components/core"; import { DeactivateAccountModal } from "components/account"; // ui import { Button, CustomSelect, CustomSearchSelect, Input, Spinner } from "@plane/ui"; @@ -57,7 +57,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { // store hooks const { currentUser: myProfile, updateCurrentUser, currentUserLoader } = useUser(); // custom hooks - const { } = useUserAuth({ user: myProfile, isLoading: currentUserLoader }); + const {} = useUserAuth({ user: myProfile, isLoading: currentUserLoader }); const { theme: themeStore } = useApplication(); useEffect(() => { @@ -138,6 +138,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { return ( <> +
themeStore.toggleSidebar()} /> @@ -296,8 +297,9 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { ref={ref} hasError={Boolean(errors.email)} placeholder="Enter your email" - className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${errors.email ? "border-red-500" : "" - }`} + className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${ + errors.email ? "border-red-500" : "" + }`} disabled /> )} @@ -385,7 +387,9 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { render={({ field: { value, onChange } }) => ( t.value === value)?.label ?? value : "Select a timezone"} + label={ + value ? TIME_ZONES.find((t) => t.value === value)?.label ?? value : "Select a timezone" + } options={timeZoneOptions} onChange={onChange} optionsClassName="w-full" @@ -409,7 +413,11 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { {({ open }) => ( <> - + Deactivate account @@ -426,8 +434,8 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
The danger zone of the profile page is a critical area that requires careful consideration and - attention. When deactivating an account, all of the data and resources within that account will be - permanently removed and cannot be recovered. + attention. When deactivating an account, all of the data and resources within that account + will be permanently removed and cannot be recovered.
- {storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && ( + {!disabled && storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && ( <>
Date: Tue, 20 Feb 2024 13:41:38 +0530 Subject: [PATCH 013/158] fix: kanban layout empty group toggle fix (#3708) --- .../issue-layouts/kanban/base-kanban-root.tsx | 2 +- .../issues/issue-layouts/kanban/default.tsx | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 83f72d8ea..8bf31a28f 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -288,7 +288,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas handleKanbanFilters={handleKanbanFilters} kanbanFilters={kanbanFilters} enableQuickIssueCreate={enableQuickAdd} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + showEmptyGroup={userDisplayFilters?.show_empty_groups ?? true} quickAddCallback={issues?.quickAddIssue} viewId={viewId} disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle} diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index f11321944..b645afe30 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -48,6 +48,7 @@ export interface IGroupByKanBan { canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; isDragStarted?: boolean; + showEmptyGroup?: boolean; } const GroupByKanBan: React.FC = observer((props) => { @@ -72,6 +73,7 @@ const GroupByKanBan: React.FC = observer((props) => { canEditProperties, scrollableContainerRef, isDragStarted, + showEmptyGroup = true, } = props; const member = useMember(); @@ -84,6 +86,10 @@ const GroupByKanBan: React.FC = observer((props) => { if (!list) return null; + const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)[_list.id]?.length > 0); + + const groupList = showEmptyGroup ? list : groupWithIssues; + const visibilityGroupBy = (_list: IGroupByColumn) => sub_group_by ? false : kanbanFilters?.group_by.includes(_list.id) ? true : false; @@ -91,9 +97,9 @@ const GroupByKanBan: React.FC = observer((props) => { return (
- {list && - list.length > 0 && - list.map((_list: IGroupByColumn) => { + {groupList && + groupList.length > 0 && + groupList.map((_list: IGroupByColumn) => { const groupByVisibilityToggle = visibilityGroupBy(_list); return ( @@ -196,6 +202,7 @@ export const KanBan: React.FC = observer((props) => { canEditProperties, scrollableContainerRef, isDragStarted, + showEmptyGroup, } = props; const issueKanBanView = useKanbanView(); @@ -222,6 +229,7 @@ export const KanBan: React.FC = observer((props) => { canEditProperties={canEditProperties} scrollableContainerRef={scrollableContainerRef} isDragStarted={isDragStarted} + showEmptyGroup={showEmptyGroup} /> ); }); From d16a0b61d00aeefa0cbc7efdf89a3a91936002e6 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 20 Feb 2024 13:42:04 +0530 Subject: [PATCH 014/158] chore: dashboard widget heading updated (#3709) --- web/components/dashboard/widgets/recent-projects.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/dashboard/widgets/recent-projects.tsx b/web/components/dashboard/widgets/recent-projects.tsx index eb7a5e4e5..79be92333 100644 --- a/web/components/dashboard/widgets/recent-projects.tsx +++ b/web/components/dashboard/widgets/recent-projects.tsx @@ -96,7 +96,7 @@ export const RecentProjectsWidget: React.FC = observer((props) => { href={`/${workspaceSlug}/projects`} className="text-lg font-semibold text-custom-text-300 mx-7 hover:underline" > - Your projects + Recent projects
{canCreateProject && ( From 6bf9d84bea17b97921ba8b3309f6ee8eff4f0ae4 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:03:58 +0530 Subject: [PATCH 015/158] chore : calendar layout improvement (#3705) * chore: current date indicator added in calendar layout * style: calendar layout ui improvement --- .../issues/issue-layouts/calendar/calendar.tsx | 2 +- .../issues/issue-layouts/calendar/day-tile.tsx | 15 ++++++++++++--- .../issues/issue-layouts/calendar/week-days.tsx | 2 +- .../issues/issue-layouts/calendar/week-header.tsx | 4 ++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index c48f23068..733b3a097 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -76,7 +76,7 @@ export const CalendarChart: React.FC = observer((props) => {
{layout === "month" && ( -
+
{allWeeksOfActiveMonth && Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => ( = observer((props) => { const issueIdList = groupedIssueIds ? groupedIssueIds[formattedDatePayload] : null; const totalIssues = issueIdList?.length ?? 0; + + const isToday = date.date.toDateString() === new Date().toDateString(); + return ( <>
{/* header */}
= observer((props) => { date.date.getDay() === 0 || date.date.getDay() === 6 ? "bg-custom-background-90" : "bg-custom-background-100" - }`} + } `} > {date.date.getDate() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "} - {date.date.getDate()} + {isToday ? ( + + {date.date.getDate()} + + ) : ( + <>{date.date.getDate()} + )}
{/* content */} diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index 5a640a566..f65b69491 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -50,7 +50,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { return (
diff --git a/web/components/issues/issue-layouts/calendar/week-header.tsx b/web/components/issues/issue-layouts/calendar/week-header.tsx index 5e7b0fcb7..ca6b05568 100644 --- a/web/components/issues/issue-layouts/calendar/week-header.tsx +++ b/web/components/issues/issue-layouts/calendar/week-header.tsx @@ -13,7 +13,7 @@ export const CalendarWeekHeader: React.FC = observer((props) => { return (
@@ -24,7 +24,7 @@ export const CalendarWeekHeader: React.FC = observer((props) => { if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null; return ( -
+
{day.shortTitle}
); From 952eb871df73a999a9055c4c577bb0261261c889 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 20 Feb 2024 20:11:59 +0530 Subject: [PATCH 016/158] [WEB-462] fix: inbox mutation (#3720) * fix: inbox issue status mutation * fix: sidebar inbox issue count fix * fix: inbox issue mutation fix --- web/components/project/sidebar-list-item.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index b8e4b9eda..f899a9b31 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -105,7 +105,7 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { const actionSectionRef = useRef(null); - const inboxesMap = currentProjectDetails?.inbox_view ? getInboxesByProjectId(currentProjectDetails.id) : undefined; + const inboxesMap = project?.inbox_view ? getInboxesByProjectId(projectId) : undefined; const inboxDetails = inboxesMap && inboxesMap.length > 0 ? getInboxById(inboxesMap[0]) : undefined; const handleAddToFavorites = () => { From 95871b0049f77572239ea9a17967941a1f71f28a Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Tue, 20 Feb 2024 20:16:45 +0530 Subject: [PATCH 017/158] [WEB-472] fix: Page title for global view (#3723) * fix: Page title for global view * fix: global-view-id type --- .../workspace-views/[globalViewId].tsx | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx b/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx index 43a0ad494..85e907481 100644 --- a/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx +++ b/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx @@ -1,20 +1,37 @@ import { ReactElement } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react"; // layouts import { AppLayout } from "layouts/app-layout"; +// hooks +import { useGlobalView, useWorkspace } from "hooks/store"; // components import { GlobalViewsHeader } from "components/workspace"; import { AllIssueLayoutRoot } from "components/issues"; import { GlobalIssuesHeader } from "components/headers"; +import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; -import { observer } from "mobx-react"; -import { useWorkspace } from "hooks/store"; -import { PageHead } from "components/core"; +// constants +import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace"; const GlobalViewIssuesPage: NextPageWithLayout = observer(() => { + // router + const router = useRouter(); + const { globalViewId } = router.query; + // store hooks const { currentWorkspace } = useWorkspace(); + const { getViewDetailsById } = useGlobalView(); // derived values - const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Views` : undefined; + const globalViewDetails = globalViewId ? getViewDetailsById(globalViewId.toString()) : undefined; + const defaultView = DEFAULT_GLOBAL_VIEWS_LIST.find((view) => view.key === globalViewId); + const pageTitle = + currentWorkspace?.name && defaultView?.label + ? `${currentWorkspace?.name} - ${defaultView?.label}` + : currentWorkspace?.name && globalViewDetails?.name + ? `${currentWorkspace?.name} - ${globalViewDetails?.name}` + : undefined; + return ( <> From 3efb7fc0701e7dd0b2e2885fd063f59ca38328c3 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:40:52 +0530 Subject: [PATCH 018/158] fix: layout change not working in cycles and modules (#3729) --- .../issues/issue-layouts/calendar/calendar.tsx | 2 +- .../calendar/dropdowns/options-dropdown.tsx | 16 +++++++++++----- .../issues/issue-layouts/calendar/week-days.tsx | 2 +- .../issue-layouts/calendar/week-header.tsx | 2 +- web/store/issue/issue_calendar_view.store.ts | 2 +- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index 733b3a097..c48f23068 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -76,7 +76,7 @@ export const CalendarChart: React.FC = observer((props) => {
{layout === "month" && ( -
+
{allWeeksOfActiveMonth && Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => ( = observer((prop const handleLayoutChange = (layout: TCalendarLayouts) => { if (!workspaceSlug || !projectId) return; - issuesFilterStore.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { - calendar: { - ...issuesFilterStore.issueFilters?.displayFilters?.calendar, - layout, + issuesFilterStore.updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + { + calendar: { + ...issuesFilterStore.issueFilters?.displayFilters?.calendar, + layout, + }, }, - }); + viewId + ); issueCalendarView.updateCalendarPayload( layout === "month" diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index f65b69491..5a640a566 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -50,7 +50,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { return (
diff --git a/web/components/issues/issue-layouts/calendar/week-header.tsx b/web/components/issues/issue-layouts/calendar/week-header.tsx index ca6b05568..f5ec41e96 100644 --- a/web/components/issues/issue-layouts/calendar/week-header.tsx +++ b/web/components/issues/issue-layouts/calendar/week-header.tsx @@ -13,7 +13,7 @@ export const CalendarWeekHeader: React.FC = observer((props) => { return (
diff --git a/web/store/issue/issue_calendar_view.store.ts b/web/store/issue/issue_calendar_view.store.ts index 61c2216a2..ac4a60809 100644 --- a/web/store/issue/issue_calendar_view.store.ts +++ b/web/store/issue/issue_calendar_view.store.ts @@ -78,7 +78,7 @@ export class CalendarStore implements ICalendarStore { const { activeWeekDate } = this.calendarFilters; return this.calendarPayload[`y-${activeWeekDate.getFullYear()}`][`m-${activeWeekDate.getMonth()}`][ - `w-${this.activeWeekNumber}` + `w-${this.activeWeekNumber - 1}` ]; } From fb4f4260fa27c650845d97b89bddd7a366f26fdd Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Wed, 21 Feb 2024 16:42:04 +0530 Subject: [PATCH 019/158] [WEB-479]: rendering issue title without overflow problem in issue peekoverview (#3728) * chore: rendering issue title without overflow problem * chore: optimised calssName * fix: className update --- web/components/issues/title-input.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/components/issues/title-input.tsx b/web/components/issues/title-input.tsx index 55dd80b87..dbd18aaaa 100644 --- a/web/components/issues/title-input.tsx +++ b/web/components/issues/title-input.tsx @@ -48,6 +48,8 @@ export const IssueTitleInput: FC = observer((props) => { [setIsSubmitting] ); + if (disabled) return
{title}
; + return (