[WEB-3482] refactor: platform components and mobx stores (#6713)
* improvement: platform componenents and mobx stores * minor improvements
This commit is contained in:
parent
4958be7898
commit
6d216f2607
50 changed files with 375 additions and 102 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue