diff --git a/packages/constants/src/issue/index.ts b/packages/constants/src/issue/index.ts index 63320322340..c5cf7908153 100644 --- a/packages/constants/src/issue/index.ts +++ b/packages/constants/src/issue/index.ts @@ -1,3 +1,4 @@ export * from "./common"; export * from "./filter"; export * from "./layout"; +export * from "./modal"; diff --git a/packages/constants/src/issue/modal.ts b/packages/constants/src/issue/modal.ts new file mode 100644 index 00000000000..c2697ca9ddc --- /dev/null +++ b/packages/constants/src/issue/modal.ts @@ -0,0 +1,19 @@ +// plane imports +import { TIssue } from "@plane/types"; + +export const DEFAULT_WORK_ITEM_FORM_VALUES: Partial = { + 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, +}; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 9ec3846b7c5..bd4e593cc26 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -40,3 +40,4 @@ export * from "./epics"; export * from "./charts"; export * from "./home"; export * from "./stickies"; +export * from "./utils"; diff --git a/packages/types/src/utils.d.ts b/packages/types/src/utils.d.ts new file mode 100644 index 00000000000..aae5fd90ced --- /dev/null +++ b/packages/types/src/utils.d.ts @@ -0,0 +1,5 @@ +export type PartialDeep = { + [attr in keyof K]?: K[attr] extends object ? PartialDeep : K[attr]; +}; + +export type CompleteOrEmpty = T | Record; diff --git a/packages/utils/src/common.ts b/packages/utils/src/common.ts index fb47656d3b2..fff5d9d8ef9 100644 --- a/packages/utils/src/common.ts +++ b/packages/utils/src/common.ts @@ -5,3 +5,37 @@ import { twMerge } from "tailwind-merge"; export const getSupportEmail = (defaultEmail: string = ""): string => defaultEmail; export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs)); + +/** + * Extracts IDs from an array of objects with ID property + */ +export const extractIds = (items: T[]): string[] => items.map((item) => item.id); + +/** + * Checks if an ID exists and is valid within the provided list + */ +export const isValidId = (id: string | null | undefined, validIds: string[]): boolean => !!id && validIds.includes(id); + +/** + * Filters an array to only include valid IDs + */ +export const filterValidIds = (ids: string[], validIds: string[]): string[] => + ids.filter((id) => validIds.includes(id)); + +/** + * Filters an array to include only valid IDs, returning both valid and invalid IDs + */ +export const partitionValidIds = (ids: string[], validIds: string[]): { valid: string[]; invalid: string[] } => { + const valid: string[] = []; + const invalid: string[] = []; + + ids.forEach((id) => { + if (validIds.includes(id)) { + valid.push(id); + } else { + invalid.push(id); + } + }); + + return { valid, invalid }; +}; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 7ae26931877..57f10c5d421 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -11,3 +11,4 @@ export * from "./state"; export * from "./string"; export * from "./theme"; export * from "./workspace"; +export * from "./work-item"; diff --git a/packages/utils/src/work-item/index.ts b/packages/utils/src/work-item/index.ts new file mode 100644 index 00000000000..031608e25ff --- /dev/null +++ b/packages/utils/src/work-item/index.ts @@ -0,0 +1 @@ +export * from "./modal"; diff --git a/packages/utils/src/work-item/modal.ts b/packages/utils/src/work-item/modal.ts new file mode 100644 index 00000000000..c70d7591fb2 --- /dev/null +++ b/packages/utils/src/work-item/modal.ts @@ -0,0 +1,33 @@ +// plane imports +import { DEFAULT_WORK_ITEM_FORM_VALUES } from "@plane/constants"; +import { IPartialProject, ISearchIssueResponse, IState, TIssue } from "@plane/types"; + +export const getUpdateFormDataForReset = (projectId: string | null | undefined, formData: Partial) => ({ + ...DEFAULT_WORK_ITEM_FORM_VALUES, + project_id: projectId, + name: formData.name, + description_html: formData.description_html, + priority: formData.priority, + start_date: formData.start_date, + target_date: formData.target_date, +}); + +export const convertWorkItemDataToSearchResponse = ( + workspaceSlug: string, + workItem: TIssue, + project: IPartialProject | undefined, + state: IState | undefined +): ISearchIssueResponse => ({ + id: workItem.id, + name: workItem.name, + project_id: workItem.project_id ?? "", + project__identifier: project?.identifier ?? "", + project__name: project?.name ?? "", + sequence_id: workItem.sequence_id, + type_id: workItem.type_id ?? "", + state__color: state?.color ?? "", + start_date: workItem.start_date, + state__group: state?.group ?? "backlog", + state__name: state?.name ?? "", + workspace__slug: workspaceSlug, +}); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/automations/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/automations/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/automations/page.tsx rename to web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/automations/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/estimates/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/estimates/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/estimates/page.tsx rename to web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/estimates/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/features/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/features/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/features/page.tsx rename to web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/features/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/labels/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/labels/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/labels/page.tsx rename to web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/labels/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/layout.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/layout.tsx similarity index 95% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/layout.tsx rename to web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/layout.tsx index e532e743af3..221ecf44288 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/layout.tsx @@ -4,7 +4,7 @@ import { FC, ReactNode } from "react"; // components import { AppHeader } from "@/components/core"; // local components -import { ProjectSettingHeader } from "./header"; +import { ProjectSettingHeader } from "../header"; import { ProjectSettingsSidebar } from "./sidebar"; export interface IProjectSettingLayout { diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/members/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx rename to web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/members/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/page.tsx rename to web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/sidebar.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/sidebar.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/sidebar.tsx rename to web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/sidebar.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/states/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx rename to web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/states/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/api-tokens/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx rename to web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/api-tokens/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/billing/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx rename to web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/billing/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/exports/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx rename to web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/exports/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/imports/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/imports/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/imports/page.tsx rename to web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/imports/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/integrations/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/integrations/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/integrations/page.tsx rename to web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/integrations/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/layout.tsx b/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/layout.tsx similarity index 97% rename from web/app/[workspaceSlug]/(projects)/settings/layout.tsx rename to web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/layout.tsx index 788ca02ae83..6dfe44ed66c 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/layout.tsx @@ -10,7 +10,7 @@ import { AppHeader } from "@/components/core"; import { useUserPermissions } from "@/hooks/store"; // plane web constants // local components -import { WorkspaceSettingHeader } from "./header"; +import { WorkspaceSettingHeader } from "../header"; import { MobileWorkspaceSettingsTabs } from "./mobile-header-tabs"; import { WorkspaceSettingsSidebar } from "./sidebar"; diff --git a/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/members/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/members/page.tsx rename to web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/members/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/mobile-header-tabs.tsx b/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/mobile-header-tabs.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/mobile-header-tabs.tsx rename to web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/mobile-header-tabs.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/page.tsx rename to web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/sidebar.tsx b/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/sidebar.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/sidebar.tsx rename to web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/sidebar.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/webhooks/[webhookId]/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/[webhookId]/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/webhooks/[webhookId]/page.tsx rename to web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/[webhookId]/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx rename to web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/page.tsx diff --git a/web/ce/components/issues/issue-modal/index.ts b/web/ce/components/issues/issue-modal/index.ts index f2c8494163f..b35c5de10da 100644 --- a/web/ce/components/issues/issue-modal/index.ts +++ b/web/ce/components/issues/issue-modal/index.ts @@ -1,3 +1,5 @@ export * from "./provider"; export * from "./issue-type-select"; export * from "./additional-properties"; +export * from "./template-select"; + diff --git a/web/ce/components/issues/issue-modal/issue-type-select.tsx b/web/ce/components/issues/issue-modal/issue-type-select.tsx index 9514cb78f5b..b7b6e0898ce 100644 --- a/web/ce/components/issues/issue-modal/issue-type-select.tsx +++ b/web/ce/components/issues/issue-modal/issue-type-select.tsx @@ -1,4 +1,6 @@ import { Control } from "react-hook-form"; +// plane imports +import { EditorRefApi } from "@plane/editor"; // types import { TBulkIssueProperties, TIssue } from "@plane/types"; @@ -9,6 +11,7 @@ export type TIssueTypeDropdownVariant = "xs" | "sm"; export type TIssueTypeSelectProps> = { control: Control; projectId: string | null; + editorRef?: React.MutableRefObject; disabled?: boolean; variant?: TIssueTypeDropdownVariant; placeholder?: string; diff --git a/web/ce/components/issues/issue-modal/provider.tsx b/web/ce/components/issues/issue-modal/provider.tsx index f387feb5a82..9ea1ad8f637 100644 --- a/web/ce/components/issues/issue-modal/provider.tsx +++ b/web/ce/components/issues/issue-modal/provider.tsx @@ -1,17 +1,29 @@ -import React from "react"; +import React, { useState } from "react"; import { observer } from "mobx-react-lite"; +// plane imports +import { ISearchIssueResponse } from "@plane/types"; // components import { IssueModalContext } from "@/components/issues"; -type TIssueModalProviderProps = { +export type TIssueModalProviderProps = { + templateId?: string; children: React.ReactNode; }; export const IssueModalProvider = observer((props: TIssueModalProviderProps) => { const { children } = props; + // states + const [selectedParentIssue, setSelectedParentIssue] = useState(null); + return ( {}, + isApplyingTemplate: false, + setIsApplyingTemplate: () => {}, + selectedParentIssue, + setSelectedParentIssue, issuePropertyValues: {}, setIssuePropertyValues: () => {}, issuePropertyValueErrors: {}, @@ -20,6 +32,9 @@ export const IssueModalProvider = observer((props: TIssueModalProviderProps) => getActiveAdditionalPropertiesLength: () => 0, handlePropertyValuesValidation: () => true, handleCreateUpdatePropertyValues: () => Promise.resolve(), + handleParentWorkItemDetails: () => Promise.resolve(undefined), + handleProjectEntitiesFetch: () => Promise.resolve(), + handleTemplateChange: () => Promise.resolve(), }} > {children} diff --git a/web/ce/components/issues/issue-modal/template-select.tsx b/web/ce/components/issues/issue-modal/template-select.tsx new file mode 100644 index 00000000000..679e9419dec --- /dev/null +++ b/web/ce/components/issues/issue-modal/template-select.tsx @@ -0,0 +1,15 @@ +export type TWorkItemTemplateDropdownSize = "xs" | "sm"; + +export type TWorkItemTemplateSelect = { + projectId: string | null; + typeId: string | null; + disabled?: boolean; + size?: TWorkItemTemplateDropdownSize; + placeholder?: string; + renderChevron?: boolean; + dropDownContainerClassName?: string; + handleFormChange?: () => void; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const WorkItemTemplateSelect = (props: TWorkItemTemplateSelect) => <>; diff --git a/web/core/components/command-palette/actions/issue-actions/change-assignee.tsx b/web/core/components/command-palette/actions/issue-actions/change-assignee.tsx index 4e43ef8876d..ca953adcd30 100644 --- a/web/core/components/command-palette/actions/issue-actions/change-assignee.tsx +++ b/web/core/components/command-palette/actions/issue-actions/change-assignee.tsx @@ -26,7 +26,7 @@ export const ChangeIssueAssignee: React.FC = observer((props) => { } = useMember(); // derived values const projectId = issue?.project_id ?? ""; - const projectMemberIds = getProjectMemberIds(projectId); + const projectMemberIds = getProjectMemberIds(projectId, false); const options = projectMemberIds diff --git a/web/core/components/dropdowns/member/member-options.tsx b/web/core/components/dropdowns/member/member-options.tsx index 085aa8324c0..28f84553f85 100644 --- a/web/core/components/dropdowns/member/member-options.tsx +++ b/web/core/components/dropdowns/member/member-options.tsx @@ -68,7 +68,11 @@ export const MemberOptions: React.FC = observer((props: Props) => { } }, [isOpen, isMobile]); - const memberIds = propsMemberIds ? propsMemberIds : projectId ? getProjectMemberIds(projectId) : workspaceMemberIds; + const memberIds = propsMemberIds + ? propsMemberIds + : projectId + ? getProjectMemberIds(projectId, true) + : workspaceMemberIds; const onOpen = () => { if (!memberIds && workspaceSlug && projectId) fetchProjectMembers(workspaceSlug.toString(), projectId); }; diff --git a/web/core/components/empty-state/detailed-empty-state-root.tsx b/web/core/components/empty-state/detailed-empty-state-root.tsx index b90503682a6..4ae97e839c4 100644 --- a/web/core/components/empty-state/detailed-empty-state-root.tsx +++ b/web/core/components/empty-state/detailed-empty-state-root.tsx @@ -27,6 +27,7 @@ type Props = { secondaryButton?: ButtonConfig; customPrimaryButton?: React.ReactNode; customSecondaryButton?: React.ReactNode; + className?: string; }; const sizeClasses = { @@ -66,12 +67,18 @@ export const DetailedEmptyState: React.FC = observer((props) => { customPrimaryButton, customSecondaryButton, assetPath, + className, } = props; const hasButtons = primaryButton || secondaryButton || customPrimaryButton || customSecondaryButton; return ( -
+

{title}

diff --git a/web/core/components/issues/issue-modal/components/title-input.tsx b/web/core/components/issues/issue-modal/components/title-input.tsx index 124899e9e98..aec93914955 100644 --- a/web/core/components/issues/issue-modal/components/title-input.tsx +++ b/web/core/components/issues/issue-modal/components/title-input.tsx @@ -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; issueTitleRef: React.MutableRefObject; - errors: FieldErrors; + formState: FormState; handleFormChange: () => void; }; export const IssueTitleInput: React.FC = observer((props) => { - const { control, issueTitleRef, errors, handleFormChange } = props; + const { + control, + issueTitleRef, + formState: { errors }, + handleFormChange, + } = props; // store hooks const { isMobile } = usePlatformOS(); const { t } = useTranslation(); diff --git a/web/core/components/issues/issue-modal/context/issue-modal-context.tsx b/web/core/components/issues/issue-modal/context/issue-modal-context.tsx index 8181445a450..64f5f49b331 100644 --- a/web/core/components/issues/issue-modal/context/issue-modal-context.tsx +++ b/web/core/components/issues/issue-modal/context/issue-modal-context.tsx @@ -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; + watch: UseFormWatch; }; export type TActiveAdditionalPropertiesProps = { projectId: string | null; workspaceSlug: string; - watch: UseFormWatch; + watch: UseFormWatch; }; export type TCreateUpdatePropertyValuesProps = { @@ -25,7 +28,31 @@ export type TCreateUpdatePropertyValuesProps = { isDraft?: boolean; }; +export type THandleTemplateChangeProps = { + workspaceSlug: string; + reset: UseFormReset; + editorRef: React.MutableRefObject; +}; + +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>; + isApplyingTemplate: boolean; + setIsApplyingTemplate: React.Dispatch>; + selectedParentIssue: ISearchIssueResponse | null; + setSelectedParentIssue: React.Dispatch>; issuePropertyValues: TIssuePropertyValues; setIssuePropertyValues: React.Dispatch>; issuePropertyValueErrors: TIssuePropertyValueErrors; @@ -34,6 +61,9 @@ export type TIssueModalContext = { getActiveAdditionalPropertiesLength: (props: TActiveAdditionalPropertiesProps) => number; handlePropertyValuesValidation: (props: TPropertyValuesValidationProps) => boolean; handleCreateUpdatePropertyValues: (props: TCreateUpdatePropertyValuesProps) => Promise; + handleParentWorkItemDetails: (props: THandleParentWorkItemDetailsProps) => Promise; + handleProjectEntitiesFetch: (props: THandleProjectEntitiesFetchProps) => Promise; + handleTemplateChange: (props: THandleTemplateChangeProps) => Promise; }; export const IssueModalContext = createContext(undefined); diff --git a/web/core/components/issues/issue-modal/form.tsx b/web/core/components/issues/issue-modal/form.tsx index 7471853cb92..c278540e5c5 100644 --- a/web/core/components/issues/issue-modal/form.tsx +++ b/web/core/components/issues/issue-modal/form.tsx @@ -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 = { - 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; issueTitleRef: React.MutableRefObject; @@ -104,7 +92,6 @@ export const IssueFormRoot: FC = observer((props) => { // states const [labelModal, setLabelModal] = useState(false); - const [selectedParentIssue, setSelectedParentIssue] = useState(null); const [gptAssistantModal, setGptAssistantModal] = useState(false); const [isMoving, setIsMoving] = useState(false); @@ -120,10 +107,16 @@ export const IssueFormRoot: FC = 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 = observer((props) => { const { getStateById } = useProjectState(); // form info + const methods = useForm({ + 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({ - 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 = 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 = 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, is_draft_issue = false) => { // Check if the editor is ready to discard if (!editorRef.current?.isEditorReadyToDiscard()) { @@ -233,7 +236,7 @@ export const IssueFormRoot: FC = 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 = observer((props) => { ...data, ...getValues(), } as TWorkspaceDraftIssue); - } catch (error) { + } catch { setToast({ type: TOAST_TYPE.ERROR, title: "Error!", @@ -308,16 +311,9 @@ export const IssueFormRoot: FC = 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 = observer((props) => { const shouldRenderDuplicateModal = isDuplicateModalOpen && duplicateIssues?.length > 0; return ( - <> + {projectId && ( = observer((props) => { )} + {projectId && !data?.id && !data?.sourceIssueId && ( + + )}
{duplicateIssues.length > 0 && ( = observer((props) => {
@@ -526,6 +531,7 @@ export const IssueFormRoot: FC = observer((props) => { size="sm" ref={submitBtnRef} loading={isSubmitting} + disabled={isDisabled} > {isSubmitting ? primaryButtonText.loading : primaryButtonText.default} @@ -562,6 +568,6 @@ export const IssueFormRoot: FC = observer((props) => {
)}
- + ); }); diff --git a/web/core/components/issues/issue-modal/modal.tsx b/web/core/components/issues/issue-modal/modal.tsx index 56569bef8cf..0d7681388b7 100644 --- a/web/core/components/issues/issue-modal/modal.tsx +++ b/web/core/components/issues/issue-modal/modal.tsx @@ -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 = observer( (props) => props.isOpen && ( - + ) diff --git a/web/core/components/issues/select/label.tsx b/web/core/components/issues/select/label.tsx index 2dc21e7f41c..149ca0f7717 100644 --- a/web/core/components/issues/select/label.tsx +++ b/web/core/components/issues/select/label.tsx @@ -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 = observer((props) => { @@ -37,7 +40,9 @@ export const IssueLabelSelect: React.FC = observer((props) => { disabled = false, tabIndex, createLabelEnabled = false, + buttonContainerClassName, buttonClassName, + placement, } = props; const { t } = useTranslation(); // router @@ -55,7 +60,7 @@ export const IssueLabelSelect: React.FC = observer((props) => { const inputRef = useRef(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 = observer((props) => {