[WEB-3482] refactor: platform components and mobx stores (#6713)

* improvement: platform componenents and mobx stores

* minor improvements
This commit is contained in:
Prateek Shourya 2025-03-06 15:47:46 +05:30 committed by GitHub
parent 4958be7898
commit 6d216f2607
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 375 additions and 102 deletions

View file

@ -2,7 +2,7 @@
import React from "react";
import { observer } from "mobx-react";
import { Control, Controller, FieldErrors } from "react-hook-form";
import { Control, Controller, FormState } from "react-hook-form";
// plane imports
import { ETabIndices } from "@plane/constants";
// types
@ -18,12 +18,17 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
type TIssueTitleInputProps = {
control: Control<TIssue>;
issueTitleRef: React.MutableRefObject<HTMLInputElement | null>;
errors: FieldErrors<TIssue>;
formState: FormState<TIssue>;
handleFormChange: () => void;
};
export const IssueTitleInput: React.FC<TIssueTitleInputProps> = observer((props) => {
const { control, issueTitleRef, errors, handleFormChange } = props;
const {
control,
issueTitleRef,
formState: { errors },
handleFormChange,
} = props;
// store hooks
const { isMobile } = usePlatformOS();
const { t } = useTranslation();

View file

@ -1,20 +1,23 @@
import React, { createContext } from "react";
import { UseFormWatch } from "react-hook-form";
// types
import { TIssue } from "@plane/types";
// plane web types
import { TIssuePropertyValueErrors, TIssuePropertyValues } from "@/plane-web/types";
import { createContext } from "react";
// ce imports
import { TIssueFields } from "ce/components/issues";
// react-hook-form
import { UseFormReset, UseFormWatch } from "react-hook-form";
// plane imports
import { EditorRefApi } from "@plane/editor";
import { ISearchIssueResponse, TIssue } from "@plane/types";
import { TIssuePropertyValues, TIssuePropertyValueErrors } from "@/plane-web/types/issue-types";
export type TPropertyValuesValidationProps = {
projectId: string | null;
workspaceSlug: string;
watch: UseFormWatch<TIssue>;
watch: UseFormWatch<TIssueFields>;
};
export type TActiveAdditionalPropertiesProps = {
projectId: string | null;
workspaceSlug: string;
watch: UseFormWatch<TIssue>;
watch: UseFormWatch<TIssueFields>;
};
export type TCreateUpdatePropertyValuesProps = {
@ -25,7 +28,31 @@ export type TCreateUpdatePropertyValuesProps = {
isDraft?: boolean;
};
export type THandleTemplateChangeProps = {
workspaceSlug: string;
reset: UseFormReset<TIssue>;
editorRef: React.MutableRefObject<EditorRefApi | null>;
};
export type THandleProjectEntitiesFetchProps = {
workspaceSlug: string;
templateId: string;
};
export type THandleParentWorkItemDetailsProps = {
workspaceSlug: string;
parentId: string | undefined;
parentProjectId: string | undefined;
isParentEpic: boolean;
};
export type TIssueModalContext = {
workItemTemplateId: string | null;
setWorkItemTemplateId: React.Dispatch<React.SetStateAction<string | null>>;
isApplyingTemplate: boolean;
setIsApplyingTemplate: React.Dispatch<React.SetStateAction<boolean>>;
selectedParentIssue: ISearchIssueResponse | null;
setSelectedParentIssue: React.Dispatch<React.SetStateAction<ISearchIssueResponse | null>>;
issuePropertyValues: TIssuePropertyValues;
setIssuePropertyValues: React.Dispatch<React.SetStateAction<TIssuePropertyValues>>;
issuePropertyValueErrors: TIssuePropertyValueErrors;
@ -34,6 +61,9 @@ export type TIssueModalContext = {
getActiveAdditionalPropertiesLength: (props: TActiveAdditionalPropertiesProps) => number;
handlePropertyValuesValidation: (props: TPropertyValuesValidationProps) => boolean;
handleCreateUpdatePropertyValues: (props: TCreateUpdatePropertyValuesProps) => Promise<void>;
handleParentWorkItemDetails: (props: THandleParentWorkItemDetailsProps) => Promise<ISearchIssueResponse | undefined>;
handleProjectEntitiesFetch: (props: THandleProjectEntitiesFetchProps) => Promise<void>;
handleTemplateChange: (props: THandleTemplateChangeProps) => Promise<void>;
};
export const IssueModalContext = createContext<TIssueModalContext | undefined>(undefined);

View file

@ -3,17 +3,18 @@
import React, { FC, useState, useRef, useEffect } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { useForm } from "react-hook-form";
import { FormProvider, useForm } from "react-hook-form";
// editor
import { ETabIndices, EIssuesStoreType } from "@plane/constants";
import { ETabIndices, EIssuesStoreType, DEFAULT_WORK_ITEM_FORM_VALUES } from "@plane/constants";
import { EditorRefApi } from "@plane/editor";
// i18n
import { useTranslation } from "@plane/i18n";
// types
import type { TIssue, ISearchIssueResponse, TWorkspaceDraftIssue } from "@plane/types";
import type { TIssue, TWorkspaceDraftIssue } from "@plane/types";
// hooks
import { Button, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { convertWorkItemDataToSearchResponse, getUpdateFormDataForReset } from "@plane/utils";
import {
IssueDefaultProperties,
IssueDescriptionEditor,
@ -25,35 +26,22 @@ import { CreateLabelModal } from "@/components/labels";
// helpers
import { cn } from "@/helpers/common.helper";
import { getTextContent } from "@/helpers/editor.helper";
import { getChangedIssuefields } from "@/helpers/issue.helper";
import { getChangedIssuefields } from "@/helpers/issue-modal.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { useIssueModal } from "@/hooks/context/use-issue-modal";
import { useIssueDetail, useProject, useProjectState, useWorkspaceDraftIssues } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { useProjectIssueProperties } from "@/hooks/use-project-issue-properties";
// plane web components
// plane web imports
import { DeDupeButtonRoot, DuplicateModalRoot } from "@/plane-web/components/de-dupe";
import { IssueAdditionalProperties, IssueTypeSelect } from "@/plane-web/components/issues/issue-modal";
import {
IssueAdditionalProperties,
IssueTypeSelect,
WorkItemTemplateSelect,
} from "@/plane-web/components/issues/issue-modal";
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
const defaultValues: Partial<TIssue> = {
project_id: "",
type_id: null,
name: "",
description_html: "",
estimate_point: null,
state_id: "",
parent_id: null,
priority: "none",
assignee_ids: [],
label_ids: [],
cycle_id: null,
module_ids: null,
start_date: null,
target_date: null,
};
export interface IssueFormProps {
data?: Partial<TIssue>;
issueTitleRef: React.MutableRefObject<HTMLInputElement | null>;
@ -104,7 +92,6 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
// states
const [labelModal, setLabelModal] = useState(false);
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
const [gptAssistantModal, setGptAssistantModal] = useState(false);
const [isMoving, setIsMoving] = useState<boolean>(false);
@ -120,10 +107,16 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
// store hooks
const { getProjectById } = useProject();
const {
workItemTemplateId,
isApplyingTemplate,
selectedParentIssue,
setWorkItemTemplateId,
setSelectedParentIssue,
getIssueTypeIdOnProjectChange,
getActiveAdditionalPropertiesLength,
handlePropertyValuesValidation,
handleCreateUpdatePropertyValues,
handleTemplateChange,
} = useIssueModal();
const { isMobile } = usePlatformOS();
const { moveIssue } = useWorkspaceDraftIssues();
@ -135,18 +128,20 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
const { getStateById } = useProjectState();
// form info
const methods = useForm<TIssue>({
defaultValues: { ...DEFAULT_WORK_ITEM_FORM_VALUES, project_id: defaultProjectId, ...data },
reValidateMode: "onChange",
});
const {
formState: { errors, isDirty, isSubmitting, dirtyFields },
formState,
formState: { isDirty, isSubmitting, dirtyFields },
handleSubmit,
reset,
watch,
control,
getValues,
setValue,
} = useForm<TIssue>({
defaultValues: { ...defaultValues, project_id: defaultProjectId, ...data },
reValidateMode: "onChange",
});
} = methods;
const projectId = watch("project_id");
const activeAdditionalPropertiesLength = getActiveAdditionalPropertiesLength({
@ -157,24 +152,21 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
// derived values
const projectDetails = projectId ? getProjectById(projectId) : undefined;
const isDisabled = isSubmitting || isApplyingTemplate;
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
//reset few fields on projectId change
useEffect(() => {
if (isDirty) {
const formData = getValues();
reset({
...defaultValues,
project_id: projectId,
name: formData.name,
description_html: formData.description_html,
priority: formData.priority,
start_date: formData.start_date,
target_date: formData.target_date,
parent_id: formData.parent_id,
});
if (workItemTemplateId) {
// reset work item template id
setWorkItemTemplateId(null);
reset({ ...DEFAULT_WORK_ITEM_FORM_VALUES, project_id: projectId });
editorRef.current?.clearEditor();
} else {
reset(getUpdateFormDataForReset(projectId, getValues()));
}
}
if (projectId && routeProjectId !== projectId) fetchCycles(workspaceSlug?.toString(), projectId);
@ -195,6 +187,17 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, projectId]);
useEffect(() => {
if (workItemTemplateId && editorRef.current) {
handleTemplateChange({
workspaceSlug: workspaceSlug?.toString(),
reset,
editorRef,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [workItemTemplateId]);
const handleFormSubmit = async (formData: Partial<TIssue>, is_draft_issue = false) => {
// Check if the editor is ready to discard
if (!editorRef.current?.isEditorReadyToDiscard()) {
@ -233,7 +236,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
.then(() => {
setGptAssistantModal(false);
reset({
...defaultValues,
...DEFAULT_WORK_ITEM_FORM_VALUES,
...(isCreateMoreToggleEnabled ? { ...data } : {}),
project_id: getValues<"project_id">("project_id"),
type_id: getValues<"type_id">("type_id"),
@ -262,7 +265,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
...data,
...getValues(),
} as TWorkspaceDraftIssue);
} catch (error) {
} catch {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
@ -308,16 +311,9 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
const stateDetails = getStateById(issue.state_id);
setSelectedParentIssue({
id: issue.id,
name: issue.name,
project_id: issue.project_id,
project__identifier: projectDetails.identifier,
project__name: projectDetails.name,
sequence_id: issue.sequence_id,
type_id: issue.type_id,
state__color: stateDetails?.color,
} as ISearchIssueResponse);
setSelectedParentIssue(
convertWorkItemDataToSearchResponse(workspaceSlug?.toString(), issue, projectDetails, stateDetails)
);
}, [watch, getIssueById, getProjectById, selectedParentIssue, getStateById]);
// executing this useEffect when isDirty changes
@ -351,7 +347,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
const shouldRenderDuplicateModal = isDuplicateModalOpen && duplicateIssues?.length > 0;
return (
<>
<FormProvider {...methods}>
{projectId && (
<CreateLabelModal
isOpen={labelModal}
@ -383,11 +379,20 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
<IssueTypeSelect
control={control}
projectId={projectId}
editorRef={editorRef}
disabled={!!data?.sourceIssueId}
handleFormChange={handleFormChange}
renderChevron
/>
)}
{projectId && !data?.id && !data?.sourceIssueId && (
<WorkItemTemplateSelect
projectId={projectId}
typeId={watch("type_id")}
handleFormChange={handleFormChange}
renderChevron
/>
)}
</div>
{duplicateIssues.length > 0 && (
<DeDupeButtonRoot
@ -416,7 +421,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
<IssueTitleInput
control={control}
issueTitleRef={issueTitleRef}
errors={errors}
formState={formState}
handleFormChange={handleFormChange}
/>
</div>
@ -526,6 +531,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
size="sm"
ref={submitBtnRef}
loading={isSubmitting}
disabled={isDisabled}
>
{isSubmitting ? primaryButtonText.loading : primaryButtonText.default}
</Button>
@ -562,6 +568,6 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
</div>
)}
</div>
</>
</FormProvider>
);
});

View file

@ -2,13 +2,12 @@
import React from "react";
import { observer } from "mobx-react";
// types
// plane imports
import { EIssuesStoreType } from "@plane/constants";
import type { TIssue } from "@plane/types";
// components
import { CreateUpdateIssueModalBase } from "@/components/issues";
// constants
// plane web providers
// plane web imports
import { IssueModalProvider } from "@/plane-web/components/issues";
export interface IssuesModalProps {
@ -28,12 +27,13 @@ export interface IssuesModalProps {
loading: string;
};
isProjectSelectionDisabled?: boolean;
templateId?: string;
}
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer(
(props) =>
props.isOpen && (
<IssueModalProvider>
<IssueModalProvider templateId={props.templateId}>
<CreateUpdateIssueModalBase {...props} />
</IssueModalProvider>
)

View file

@ -1,4 +1,5 @@
import React, { Fragment, useEffect, useRef, useState } from "react";
import { Placement } from "@popperjs/core";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { usePopper } from "react-popper";
@ -24,7 +25,9 @@ type Props = {
disabled?: boolean;
tabIndex?: number;
createLabelEnabled?: boolean;
buttonContainerClassName?: string;
buttonClassName?: string;
placement?: Placement;
};
export const IssueLabelSelect: React.FC<Props> = observer((props) => {
@ -37,7 +40,9 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
disabled = false,
tabIndex,
createLabelEnabled = false,
buttonContainerClassName,
buttonClassName,
placement,
} = props;
const { t } = useTranslation();
// router
@ -55,7 +60,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
const inputRef = useRef<HTMLInputElement | null>(null);
// popper
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "bottom-start",
placement: placement ?? "bottom-start",
});
const projectLabels = getProjectLabels(projectId);
@ -115,13 +120,16 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
<button
type="button"
ref={setReferenceElement}
className={cn("h-full flex cursor-pointer items-center gap-2 text-xs text-custom-text-200", buttonClassName)}
className={cn(
"h-full flex cursor-pointer items-center gap-2 text-xs text-custom-text-200",
buttonContainerClassName
)}
onClick={handleOnClick}
>
{label ? (
label
) : value && value.length > 0 ? (
<span className="flex items-center justify-center gap-2 text-xs h-full">
<span className={cn("flex items-center justify-center gap-2 text-xs h-full", buttonClassName)}>
<IssueLabelsList
labels={value.map((v) => projectLabels?.find((l) => l.id === v)) ?? []}
length={3}
@ -129,7 +137,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
/>
</span>
) : (
<div className="h-full flex items-center justify-center gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1 text-xs hover:bg-custom-background-80">
<div className={cn("h-full flex items-center justify-center gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1 text-xs hover:bg-custom-background-80", buttonClassName)}>
<Tag className="h-3 w-3 flex-shrink-0" />
<span>{t("labels")}</span>
</div>