From a94c60703195379ef91f37d3c87b5c4343adc0bc Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Fri, 16 Feb 2024 20:07:04 +0530 Subject: [PATCH] fix: updated issue title and description components (#3687) * fix: issue description fixes * chore: description html in the archive issue * chore: changed retrieve viewset * chore: implemented new issue title description components in inbox, issue-detail and fixed issue in archived store * chore: removed consoles and empty description update in issue detail * fix: draft issue empty state image --------- Co-authored-by: sriram veeraghanta Co-authored-by: NarayanBavisetti Co-authored-by: Anmol Singh Bhatia --- apiserver/plane/app/views/issue.py | 12 +-- web/components/issues/description-input.tsx | 95 +++++++++++++++++++ .../issue-detail/inbox/main-content.tsx | 23 +++-- .../comments/comment-create.tsx | 1 - .../issues/issue-detail/main-content.tsx | 23 +++-- .../empty-states/draft-issues.tsx | 2 +- .../issues/issue-layouts/list/block.tsx | 5 +- .../roots/archived-issue-layout-root.tsx | 2 +- .../issues/peek-overview/issue-detail.tsx | 42 +++++--- web/components/issues/peek-overview/root.tsx | 22 +++-- web/components/issues/title-input.tsx | 70 ++++++++++++++ .../pages/create-update-page-modal.tsx | 1 - web/components/project/form.tsx | 2 +- .../profile/preferences/layout.tsx | 77 +++++++-------- 14 files changed, 289 insertions(+), 88 deletions(-) create mode 100644 web/components/issues/description-input.tsx create mode 100644 web/components/issues/title-input.tsx diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index c8845150a..edefade16 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -1209,13 +1209,13 @@ class IssueArchiveViewSet(BaseViewSet): return Response(issues, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, pk=None): - issue = Issue.objects.get( - workspace__slug=slug, - project_id=project_id, - archived_at__isnull=False, - pk=pk, + issue = self.get_queryset().filter(pk=pk).first() + return Response( + IssueDetailSerializer( + issue, fields=self.fields, expand=self.expand + ).data, + status=status.HTTP_200_OK, ) - return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) def unarchive(self, request, slug, project_id, pk=None): issue = Issue.objects.get( diff --git a/web/components/issues/description-input.tsx b/web/components/issues/description-input.tsx new file mode 100644 index 000000000..8f3dc8644 --- /dev/null +++ b/web/components/issues/description-input.tsx @@ -0,0 +1,95 @@ +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"; +// store hooks +import { useMention, useWorkspace } from "hooks/store"; +// services +import { FileService } from "services/file.service"; +const fileService = new FileService(); +// types +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; + setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; + issueOperations: TIssueOperations; + projectId: string; + issueId: string; +}; + +export const IssueDescriptionInput: FC = observer((props) => { + const { disabled, value, workspaceSlug, setIsSubmitting, issueId, issueOperations, projectId } = props; + // states + const [descriptionHTML, setDescriptionHTML] = useState(value); + // store hooks + const { mentionHighlights, mentionSuggestions } = useMention(); + const workspaceStore = useWorkspace(); + // hooks + const { setShowAlert } = useReloadConfirmations(); + const debouncedValue = useDebounce(descriptionHTML, 1500); + // computed values + const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug)?.id as string; + + useEffect(() => { + setDescriptionHTML(value); + }, [value]); + + useEffect(() => { + if (debouncedValue || debouncedValue === "") { + issueOperations + .update(workspaceSlug, projectId, issueId, { description_html: debouncedValue }, false) + .finally(() => { + setIsSubmitting("saved"); + }); + } + // 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 !== "") { + return ( + + + + ); + } + + if (disabled) { + return ( + + ); + } + + return ( + { + setShowAlert(true); + setIsSubmitting("submitting"); + setDescriptionHTML(description_html); + }} + mentionSuggestions={mentionSuggestions} + 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 4a1f79bee..d25fe9260 100644 --- a/web/components/issues/issue-detail/inbox/main-content.tsx +++ b/web/components/issues/issue-detail/inbox/main-content.tsx @@ -3,7 +3,9 @@ import { observer } from "mobx-react-lite"; // hooks import { useIssueDetail, useProjectState, useUser } from "hooks/store"; // components -import { IssueDescriptionForm, IssueUpdateStatus, TIssueOperations } from "components/issues"; +import { IssueUpdateStatus, TIssueOperations } from "components/issues"; +import { IssueTitleInput } from "../../title-input"; +import { IssueDescriptionInput } from "../../description-input"; import { IssueReaction } from "../reactions"; import { IssueActivity } from "../issue-activity"; import { InboxIssueStatus } from "../../../inbox/inbox-issue-status"; @@ -57,15 +59,24 @@ export const InboxIssueMainContent: React.FC = observer((props) => { - setIsSubmitting(value)} - isSubmitting={isSubmitting} - issue={issue} issueOperations={issueOperations} disabled={!is_editable} + value={issue.name} + /> + + setIsSubmitting(value)} + issueOperations={issueOperations} + disabled={!is_editable} + value={issue.description_html} /> {currentUser && ( diff --git a/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx b/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx index bb79c9817..bf5b15266 100644 --- a/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx +++ b/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx @@ -81,7 +81,6 @@ export const IssueCommentCreate: FC = (props) => { render={({ field: { value, onChange } }) => ( { - console.log("yo"); handleSubmit(onSubmit)(e); }} cancelUploadImage={fileService.cancelUpload} diff --git a/web/components/issues/issue-detail/main-content.tsx b/web/components/issues/issue-detail/main-content.tsx index 075525801..14860a0cf 100644 --- a/web/components/issues/issue-detail/main-content.tsx +++ b/web/components/issues/issue-detail/main-content.tsx @@ -3,7 +3,9 @@ import { observer } from "mobx-react-lite"; // hooks import { useIssueDetail, useProjectState, useUser } from "hooks/store"; // components -import { IssueDescriptionForm, IssueAttachmentRoot, IssueUpdateStatus } from "components/issues"; +import { IssueAttachmentRoot, IssueUpdateStatus } from "components/issues"; +import { IssueTitleInput } from "../title-input"; +import { IssueDescriptionInput } from "../description-input"; import { IssueParentDetail } from "./parent"; import { IssueReaction } from "./reactions"; import { SubIssuesRoot } from "../sub-issues"; @@ -61,15 +63,24 @@ export const IssueMainContent: React.FC = observer((props) => { - setIsSubmitting(value)} - isSubmitting={isSubmitting} - issue={issue} issueOperations={issueOperations} disabled={!is_editable} + value={issue.name} + /> + + setIsSubmitting(value)} + issueOperations={issueOperations} + disabled={!is_editable} + value={issue.description_html} /> {currentUser && ( diff --git a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx index 347778d8f..c496cc5fe 100644 --- a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx @@ -42,7 +42,7 @@ export const ProjectDraftEmptyState: React.FC = observer(() => { const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const EmptyStateImagePath = getEmptyStateImagePath("draft", "empty-issues", isLightMode); + const EmptyStateImagePath = getEmptyStateImagePath("draft", "draft-issues-empty", isLightMode); const issueFilterCount = size( Object.fromEntries( diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index ceec7b219..2e48e1f1c 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -50,9 +50,8 @@ export const IssueBlock: React.FC = observer((props: IssueBlock return (
{displayProperties && displayProperties?.key && ( 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 5f049d4c3..2ae7ae510 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/issue-detail.tsx b/web/components/issues/peek-overview/issue-detail.tsx index 8c5101938..7bc1b1b03 100644 --- a/web/components/issues/peek-overview/issue-detail.tsx +++ b/web/components/issues/peek-overview/issue-detail.tsx @@ -1,10 +1,15 @@ -import { FC } from "react"; -// hooks -import { useIssueDetail, useProject, useUser } from "hooks/store"; -// components -import { IssueDescriptionForm, TIssueOperations } from "components/issues"; -import { IssueReaction } from "../issue-detail/reactions"; +import { FC, useCallback, useEffect, useState } from "react"; import { observer } from "mobx-react"; +// store hooks +import { useIssueDetail, useProject, useUser } from "hooks/store"; +// hooks +import useReloadConfirmations from "hooks/use-reload-confirmation"; +// components +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; @@ -17,17 +22,18 @@ interface IPeekOverviewIssueDetails { } export const PeekOverviewIssueDetails: FC = observer((props) => { - const { workspaceSlug, projectId, issueId, issueOperations, disabled, isSubmitting, setIsSubmitting } = props; + const { workspaceSlug, issueId, issueOperations, disabled, setIsSubmitting } = props; // store hooks const { getProjectById } = useProject(); const { currentUser } = useUser(); const { issue: { getIssueById }, } = useIssueDetail(); - // derived values const issue = getIssueById(issueId); + if (!issue) return <>; + const projectDetails = getProjectById(issue?.project_id); return ( @@ -35,20 +41,28 @@ export const PeekOverviewIssueDetails: FC = observer( {projectDetails?.identifier}-{issue?.sequence_id} - setIsSubmitting(value)} - isSubmitting={isSubmitting} - issue={issue} issueOperations={issueOperations} disabled={disabled} + value={issue.name} + /> + setIsSubmitting(value)} + issueOperations={issueOperations} + disabled={disabled} + value={issue.description_html} /> {currentUser && ( diff --git a/web/components/issues/peek-overview/root.tsx b/web/components/issues/peek-overview/root.tsx index b491ebe36..c49c0a503 100644 --- a/web/components/issues/peek-overview/root.tsx +++ b/web/components/issues/peek-overview/root.tsx @@ -69,20 +69,11 @@ export const IssuePeekOverview: FC = observer((props) => { // state const [loader, setLoader] = useState(false); - useEffect(() => { - if (peekIssue) { - setLoader(true); - fetchIssue(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId).finally(() => { - setLoader(false); - }); - } - }, [peekIssue, fetchIssue]); - const issueOperations: TIssuePeekOperations = useMemo( () => ({ fetch: async (workspaceSlug: string, projectId: string, issueId: string) => { try { - await fetchIssue(workspaceSlug, projectId, issueId); + await fetchIssue(workspaceSlug, projectId, issueId, is_archived); } catch (error) { console.error("Error fetching the parent issue"); } @@ -324,9 +315,20 @@ export const IssuePeekOverview: FC = observer((props) => { removeModulesFromIssue, setToastAlert, onIssueUpdate, + captureIssueEvent, + router.asPath, ] ); + useEffect(() => { + if (peekIssue) { + setLoader(true); + issueOperations.fetch(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId).finally(() => { + setLoader(false); + }); + } + }, [peekIssue, issueOperations]); + if (!peekIssue?.workspaceSlug || !peekIssue?.projectId || !peekIssue?.issueId) return <>; const issue = getIssueById(peekIssue.issueId) || undefined; diff --git a/web/components/issues/title-input.tsx b/web/components/issues/title-input.tsx new file mode 100644 index 000000000..2cd031b4f --- /dev/null +++ b/web/components/issues/title-input.tsx @@ -0,0 +1,70 @@ +import { FC, useState, useEffect, useCallback } from "react"; +import { observer } from "mobx-react"; +// components +import { TextArea } from "@plane/ui"; +// types +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; + setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; + issueOperations: TIssueOperations; + projectId: string; + issueId: string; +}; + +export const IssueTitleInput: FC = observer((props) => { + const { disabled, value, workspaceSlug, setIsSubmitting, issueId, issueOperations, projectId } = props; + // states + const [title, setTitle] = useState(""); + // hooks + const { setShowAlert } = useReloadConfirmations(); + const debouncedValue = useDebounce(title, 1500); + + useEffect(() => { + if (value) setTitle(value); + }, [value]); + + useEffect(() => { + if (debouncedValue) { + issueOperations.update(workspaceSlug, projectId, issueId, { name: debouncedValue }, false).finally(() => { + setIsSubmitting("saved"); + }); + } + // DO NOT Add more dependencies here. It will cause multiple requests to be sent. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debouncedValue]); + + const handleTitleChange = useCallback( + (e: React.ChangeEvent) => { + setShowAlert(true); + setIsSubmitting("submitting"); + setTitle(e.target.value); + }, + [setIsSubmitting, setShowAlert] + ); + + return ( +
+