diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 0d72f9192..28e881060 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -68,6 +68,7 @@ from .issue import ( IssueRelationSerializer, RelatedIssueSerializer, IssuePublicSerializer, + IssueDetailSerializer, ) from .module import ( diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index be98bc312..90069bd41 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -586,7 +586,6 @@ class IssueSerializer(DynamicBaseSerializer): "id", "name", "state_id", - "description_html", "sort_order", "completed_at", "estimate_point", @@ -618,6 +617,13 @@ class IssueSerializer(DynamicBaseSerializer): return [module for module in obj.issue_module.values_list("module_id", flat=True)] +class IssueDetailSerializer(IssueSerializer): + description_html = serializers.CharField() + + class Meta(IssueSerializer.Meta): + fields = IssueSerializer.Meta.fields + ['description_html'] + + class IssueLiteSerializer(DynamicBaseSerializer): workspace_detail = WorkspaceLiteSerializer( read_only=True, source="workspace" diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index 34bce8a0a..c8845150a 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -50,6 +50,7 @@ from plane.app.serializers import ( CommentReactionSerializer, IssueRelationSerializer, RelatedIssueSerializer, + IssueDetailSerializer, ) from plane.app.permissions import ( ProjectEntityPermission, @@ -267,7 +268,7 @@ class IssueViewSet(WebhookMixin, BaseViewSet): def retrieve(self, request, slug, project_id, pk=None): issue = self.get_queryset().filter(pk=pk).first() return Response( - IssueSerializer( + IssueDetailSerializer( issue, fields=self.fields, expand=self.expand ).data, status=status.HTTP_200_OK, diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index ca6d7e0e7..b7601ef52 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -4,7 +4,7 @@ import { Controller, useForm } from "react-hook-form"; import useReloadConfirmations from "hooks/use-reload-confirmation"; import debounce from "lodash/debounce"; // components -import { TextArea } from "@plane/ui"; +import { Loader, TextArea } from "@plane/ui"; import { RichReadOnlyEditor, RichTextEditor } from "@plane/rich-text-editor"; // types import { TIssue } from "@plane/types"; @@ -12,6 +12,8 @@ import { TIssueOperations } from "./issue-detail"; // services import { FileService } from "services/file.service"; import { useMention, useWorkspace } from "hooks/store"; +import { observer } from "mobx-react"; +import { isNil } from "lodash"; export interface IssueDescriptionFormValues { name: string; @@ -36,7 +38,7 @@ export interface IssueDetailsProps { const fileService = new FileService(); -export const IssueDescriptionForm: FC = (props) => { +export const IssueDescriptionForm: FC = observer((props) => { const { workspaceSlug, projectId, issueId, issue, issueOperations, disabled, isSubmitting, setIsSubmitting } = props; const workspaceStore = useWorkspace(); const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug)?.id as string; @@ -71,12 +73,20 @@ export const IssueDescriptionForm: FC = (props) => { // editor rerendering on every save useEffect(() => { if (issue.id) { - setLocalIssueDescription({ id: issue.id, description_html: issue.description_html }); setLocalTitleValue(issue.name); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [issue.id]); // TODO: verify the exhaustive-deps warning + useEffect(() => { + if (issue.description_html) { + setLocalIssueDescription((state) => { + if (!isNil(state.description_html)) return state; + return { id: issue.id, description_html: issue.description_html }; + }); + } + }, [issue.description_html]); + const handleDescriptionFormSubmit = useCallback( async (formData: Partial) => { if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return; @@ -167,42 +177,48 @@ export const IssueDescriptionForm: FC = (props) => { {errors.name ? errors.name.message : null}
- - !disabled ? ( - { - setShowAlert(true); - setIsSubmitting("submitting"); - onChange(description_html); - debouncedFormSave(); - }} - mentionSuggestions={mentionSuggestions} - mentionHighlights={mentionHighlights} - /> - ) : ( - - ) - } - /> + {issue.description_html ? ( + + !disabled ? ( + { + setShowAlert(true); + setIsSubmitting("submitting"); + onChange(description_html); + debouncedFormSave(); + }} + mentionSuggestions={mentionSuggestions} + mentionHighlights={mentionHighlights} + /> + ) : ( + + ) + } + /> + ) : ( + + + + )}
); -}; +}); diff --git a/web/components/issues/peek-overview/issue-detail.tsx b/web/components/issues/peek-overview/issue-detail.tsx index fefba1713..8c5101938 100644 --- a/web/components/issues/peek-overview/issue-detail.tsx +++ b/web/components/issues/peek-overview/issue-detail.tsx @@ -4,6 +4,7 @@ import { useIssueDetail, useProject, useUser } from "hooks/store"; // components import { IssueDescriptionForm, TIssueOperations } from "components/issues"; import { IssueReaction } from "../issue-detail/reactions"; +import { observer } from "mobx-react"; interface IPeekOverviewIssueDetails { workspaceSlug: string; @@ -15,7 +16,7 @@ interface IPeekOverviewIssueDetails { setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; } -export const PeekOverviewIssueDetails: FC = (props) => { +export const PeekOverviewIssueDetails: FC = observer((props) => { const { workspaceSlug, projectId, issueId, issueOperations, disabled, isSubmitting, setIsSubmitting } = props; // store hooks const { getProjectById } = useProject(); @@ -23,6 +24,7 @@ export const PeekOverviewIssueDetails: FC = (props) = const { issue: { getIssueById }, } = useIssueDetail(); + // derived values const issue = getIssueById(issueId); if (!issue) return <>; @@ -53,4 +55,4 @@ export const PeekOverviewIssueDetails: FC = (props) = )} ); -}; +}); diff --git a/web/services/issue/issue_draft.service.tsx b/web/services/issue/issue_draft.service.ts similarity index 100% rename from web/services/issue/issue_draft.service.tsx rename to web/services/issue/issue_draft.service.ts diff --git a/web/store/issue/issue-details/issue.store.ts b/web/store/issue/issue-details/issue.store.ts index 46605c771..43a7ca093 100644 --- a/web/store/issue/issue-details/issue.store.ts +++ b/web/store/issue/issue-details/issue.store.ts @@ -4,6 +4,7 @@ import { IssueArchiveService, IssueService } from "services/issue"; // types import { IIssueDetail } from "./root.store"; import { TIssue } from "@plane/types"; +import { computedFn } from "mobx-utils"; export interface IIssueStoreActions { // actions @@ -44,10 +45,10 @@ export class IssueStore implements IIssueStore { } // helper methods - getIssueById = (issueId: string) => { + getIssueById = computedFn((issueId: string) => { if (!issueId) return undefined; return this.rootIssueDetailStore.rootIssueStore.issues.getIssueById(issueId) ?? undefined; - }; + }); // actions fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => { @@ -63,12 +64,12 @@ export class IssueStore implements IIssueStore { if (!issue) throw new Error("Issue not found"); - this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue]); + this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue], true); // store handlers from issue detail // parent if (issue && issue?.parent && issue?.parent?.id) - this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue?.parent]); + this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue.parent]); // assignees // labels // state diff --git a/web/store/issue/issue.store.ts b/web/store/issue/issue.store.ts index 8ee689daf..36b2d8741 100644 --- a/web/store/issue/issue.store.ts +++ b/web/store/issue/issue.store.ts @@ -10,7 +10,7 @@ export type IIssueStore = { // observables issuesMap: Record; // Record defines issue_id as key and TIssue as value // actions - addIssue(issues: TIssue[]): void; + addIssue(issues: TIssue[], shouldReplace?: boolean): void; updateIssue(issueId: string, issue: Partial): void; removeIssue(issueId: string): void; // helper methods @@ -39,11 +39,11 @@ export class IssueStore implements IIssueStore { * @param {TIssue[]} issues * @returns {void} */ - addIssue = (issues: TIssue[]) => { + addIssue = (issues: TIssue[], shouldReplace = false) => { if (issues && issues.length <= 0) return; runInAction(() => { issues.forEach((issue) => { - if (!this.issuesMap[issue.id]) set(this.issuesMap, issue.id, issue); + if (!this.issuesMap[issue.id] || shouldReplace) set(this.issuesMap, issue.id, issue); }); }); };