diff --git a/packages/constants/src/emoji.ts b/packages/constants/src/emoji.ts index 48be02b0196..9ba145e1c93 100644 --- a/packages/constants/src/emoji.ts +++ b/packages/constants/src/emoji.ts @@ -8,3 +8,18 @@ export const ISSUE_REACTION_EMOJI_CODES = [ "9992", "128064", ]; + +export const RANDOM_EMOJI_CODES = [ + "8986", + "9200", + "128204", + "127773", + "127891", + "128076", + "128077", + "128187", + "128188", + "128512", + "128522", + "128578", +]; diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index 7fedff05d78..74978c94376 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -29,3 +29,4 @@ export * from "./event-tracker"; export * from "./spreadsheet"; export * from "./dashboard"; export * from "./page"; +export * from "./emoji"; diff --git a/packages/constants/src/project.ts b/packages/constants/src/project.ts index 93a29a6c4b2..df22641e8d5 100644 --- a/packages/constants/src/project.ts +++ b/packages/constants/src/project.ts @@ -1,5 +1,7 @@ -// icons -import { TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types"; +// plane imports +import { IProject, TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types"; +// local imports +import { RANDOM_EMOJI_CODES } from "./emoji"; export type TNetworkChoiceIconKey = "Lock" | "Globe2"; @@ -132,3 +134,18 @@ export const PROJECT_ERROR_MESSAGES = { i18n_message: "workspace_projects.error.issue_delete", }, }; + +export const DEFAULT_PROJECT_FORM_VALUES: Partial = { + cover_image_url: PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)], + description: "", + logo_props: { + in_use: "emoji", + emoji: { + value: RANDOM_EMOJI_CODES[Math.floor(Math.random() * RANDOM_EMOJI_CODES.length)], + }, + }, + identifier: "", + name: "", + network: 2, + project_lead: null, +}; diff --git a/packages/constants/src/state.ts b/packages/constants/src/state.ts index 9f5db17c7f2..fa0f5d27700 100644 --- a/packages/constants/src/state.ts +++ b/packages/constants/src/state.ts @@ -1,9 +1,4 @@ -export type TStateGroups = - | "backlog" - | "unstarted" - | "started" - | "completed" - | "cancelled"; +export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; export type TDraggableData = { groupKey: TStateGroups; @@ -14,40 +9,43 @@ export const STATE_GROUPS: { [key in TStateGroups]: { key: TStateGroups; label: string; + defaultStateName: string; color: string; }; } = { backlog: { key: "backlog", label: "Backlog", + defaultStateName: "Backlog", color: "#d9d9d9", }, unstarted: { key: "unstarted", label: "Unstarted", + defaultStateName: "Todo", color: "#3f76ff", }, started: { key: "started", label: "Started", + defaultStateName: "In Progress", color: "#f59e0b", }, completed: { key: "completed", label: "Completed", + defaultStateName: "Done", color: "#16a34a", }, cancelled: { key: "cancelled", label: "Canceled", + defaultStateName: "Cancelled", color: "#dc2626", }, }; -export const ARCHIVABLE_STATE_GROUPS = [ - STATE_GROUPS.completed.key, - STATE_GROUPS.cancelled.key, -]; +export const ARCHIVABLE_STATE_GROUPS = [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key]; export const COMPLETED_STATE_GROUPS = [STATE_GROUPS.completed.key]; export const PENDING_STATE_GROUPS = [ STATE_GROUPS.backlog.key, diff --git a/packages/types/src/inbox.d.ts b/packages/types/src/inbox.d.ts index 69fb01f7c3d..e7065c6d0b3 100644 --- a/packages/types/src/inbox.d.ts +++ b/packages/types/src/inbox.d.ts @@ -1,6 +1,8 @@ +// plane constants +import { TInboxIssue, TInboxIssueStatus } from "@plane/constants"; +// plane types import { TPaginationInfo } from "./common"; import { TIssuePriorities } from "./issues"; -import { TIssue } from "./issues/base"; // filters export type TInboxIssueFilterMemberKeys = "assignees" | "created_by"; diff --git a/packages/types/src/state.d.ts b/packages/types/src/state.d.ts index 120b216da25..d28194dc931 100644 --- a/packages/types/src/state.d.ts +++ b/packages/types/src/state.d.ts @@ -24,3 +24,11 @@ export interface IStateLite { export interface IStateResponse { [key: string]: IState[]; } + +export type TStateOperationsCallbacks = { + createState: (data: Partial) => Promise; + updateState: (stateId: string, data: Partial) => Promise; + deleteState: (stateId: string) => Promise; + moveStatePosition: (stateId: string, data: Partial) => Promise; + markStateAsDefault: (stateId: string) => Promise; +}; diff --git a/packages/ui/src/collapsible/collapsible-button.tsx b/packages/ui/src/collapsible/collapsible-button.tsx index 2a141aa41cb..b6198fa6cc6 100644 --- a/packages/ui/src/collapsible/collapsible-button.tsx +++ b/packages/ui/src/collapsible/collapsible-button.tsx @@ -1,10 +1,10 @@ import React, { FC } from "react"; -import { DropdownIcon } from "../icons"; import { cn } from "../../helpers"; +import { DropdownIcon } from "../icons"; type Props = { isOpen: boolean; - title: string; + title: React.ReactNode; hideChevron?: boolean; indicatorElement?: React.ReactNode; actionItemElement?: React.ReactNode; diff --git a/packages/ui/src/dropdowns/helper.tsx b/packages/ui/src/dropdowns/helper.tsx index 4ce12d9f8ca..3d2b5a86696 100644 --- a/packages/ui/src/dropdowns/helper.tsx +++ b/packages/ui/src/dropdowns/helper.tsx @@ -45,14 +45,14 @@ interface CustomSearchSelectProps { onClose?: () => void; noResultsMessage?: string; options: - | { - value: any; - query: string; - content: React.ReactNode; - disabled?: boolean; - tooltip?: string | React.ReactNode; - }[] - | undefined; + | { + value: any; + query: string; + content: React.ReactNode; + disabled?: boolean; + tooltip?: string | React.ReactNode; + }[] + | undefined; } interface SingleValueProps { diff --git a/packages/utils/src/common.ts b/packages/utils/src/common.ts index fff5d9d8ef9..d2d02c299a0 100644 --- a/packages/utils/src/common.ts +++ b/packages/utils/src/common.ts @@ -1,5 +1,6 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; +import { CompleteOrEmpty } from "@plane/types"; // Support email can be configured by the application export const getSupportEmail = (defaultEmail: string = ""): string => defaultEmail; @@ -39,3 +40,21 @@ export const partitionValidIds = (ids: string[], validIds: string[]): { valid: s return { valid, invalid }; }; + +/** + * Checks if an object is complete (has properties) rather than empty. + * This helps TypeScript narrow the type from CompleteOrEmpty to T. + * + * @param obj The object to check, typed as CompleteOrEmpty + * @returns A boolean indicating if the object is complete (true) or empty (false) + */ +export const isComplete = (obj: CompleteOrEmpty): obj is T => { + // Check if object is not null or undefined + if (obj == null) return false; + + // Check if it's an object + if (typeof obj !== "object") return false; + + // Check if it has any own properties + return Object.keys(obj).length > 0; +}; diff --git a/web/ce/components/issues/issue-modal/provider.tsx b/web/ce/components/issues/issue-modal/provider.tsx index 9ea1ad8f637..18e122d97a9 100644 --- a/web/ce/components/issues/issue-modal/provider.tsx +++ b/web/ce/components/issues/issue-modal/provider.tsx @@ -1,12 +1,13 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // plane imports -import { ISearchIssueResponse } from "@plane/types"; +import { ISearchIssueResponse, TIssue } from "@plane/types"; // components import { IssueModalContext } from "@/components/issues"; export type TIssueModalProviderProps = { templateId?: string; + dataForPreload?: Partial; children: React.ReactNode; }; @@ -32,7 +33,6 @@ export const IssueModalProvider = observer((props: TIssueModalProviderProps) => getActiveAdditionalPropertiesLength: () => 0, handlePropertyValuesValidation: () => true, handleCreateUpdatePropertyValues: () => Promise.resolve(), - handleParentWorkItemDetails: () => Promise.resolve(undefined), handleProjectEntitiesFetch: () => Promise.resolve(), handleTemplateChange: () => Promise.resolve(), }} diff --git a/web/ce/components/projects/create/root.tsx b/web/ce/components/projects/create/root.tsx index c8ba67d503a..490d3c6b89a 100644 --- a/web/ce/components/projects/create/root.tsx +++ b/web/ce/components/projects/create/root.tsx @@ -3,7 +3,7 @@ import { useState, FC } from "react"; import { observer } from "mobx-react"; import { FormProvider, useForm } from "react-hook-form"; -import { PROJECT_UNSPLASH_COVERS, PROJECT_CREATED } from "@plane/constants"; +import { PROJECT_CREATED, DEFAULT_PROJECT_FORM_VALUES } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // ui import { setToast, TOAST_TYPE } from "@plane/ui"; @@ -11,8 +11,6 @@ import { setToast, TOAST_TYPE } from "@plane/ui"; import ProjectCommonAttributes from "@/components/project/create/common-attributes"; import ProjectCreateHeader from "@/components/project/create/header"; import ProjectCreateButtons from "@/components/project/create/project-create-buttons"; -// helpers -import { getRandomEmoji } from "@/helpers/emoji.helper"; // hooks import { useEventTracker, useProject } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -26,26 +24,12 @@ export type TCreateProjectFormProps = { onClose: () => void; handleNextStep: (projectId: string) => void; data?: Partial; + templateId?: string; updateCoverImageStatus: (projectId: string, coverImage: string) => Promise; }; -const defaultValues: Partial = { - cover_image_url: PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)], - description: "", - logo_props: { - in_use: "emoji", - emoji: { - value: getRandomEmoji(), - }, - }, - identifier: "", - name: "", - network: 2, - project_lead: null, -}; - export const CreateProjectForm: FC = observer((props) => { - const { setToFavorite, workspaceSlug, onClose, handleNextStep, updateCoverImageStatus } = props; + const { setToFavorite, workspaceSlug, data, onClose, handleNextStep, updateCoverImageStatus } = props; // store const { t } = useTranslation(); const { captureProjectEvent } = useEventTracker(); @@ -54,7 +38,7 @@ export const CreateProjectForm: FC = observer((props) = const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); // form info const methods = useForm({ - defaultValues, + defaultValues: { ...DEFAULT_PROJECT_FORM_VALUES, ...data }, reValidateMode: "onChange", }); const { handleSubmit, reset, setValue } = methods; @@ -105,7 +89,7 @@ export const CreateProjectForm: FC = observer((props) = handleNextStep(res.id); }) .catch((err) => { - Object.keys(err.data).map((key) => { + Object.keys(err?.data ?? {}).map((key) => { setToast({ type: TOAST_TYPE.ERROR, title: t("error"), diff --git a/web/ce/components/projects/create/template-select.tsx b/web/ce/components/projects/create/template-select.tsx new file mode 100644 index 00000000000..e304af83510 --- /dev/null +++ b/web/ce/components/projects/create/template-select.tsx @@ -0,0 +1,12 @@ +type TProjectTemplateDropdownSize = "xs" | "sm"; + +export type TProjectTemplateSelect = { + disabled?: boolean; + size?: TProjectTemplateDropdownSize; + placeholder?: string; + dropDownContainerClassName?: string; + handleModalClose: () => void; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const ProjectTemplateSelect = (props: TProjectTemplateSelect) => <>; diff --git a/web/ce/constants/project/settings/features.tsx b/web/ce/constants/project/settings/features.tsx index 8dda3495798..1441c70e2d1 100644 --- a/web/ce/constants/project/settings/features.tsx +++ b/web/ce/constants/project/settings/features.tsx @@ -1,5 +1,6 @@ import { ReactNode } from "react"; import { FileText, Layers, Timer } from "lucide-react"; +// plane imports import { IProject } from "@plane/types"; import { ContrastIcon, DiceIcon, Intake } from "@plane/ui"; @@ -13,16 +14,90 @@ export type TProperties = { isEnabled: boolean; renderChildren?: (currentProjectDetails: IProject, workspaceSlug: string) => ReactNode; }; -export type TFeatureList = { - [key: string]: TProperties; + +type TProjectBaseFeatureKeys = "cycles" | "modules" | "views" | "pages" | "inbox"; +type TProjectOtherFeatureKeys = "is_time_tracking_enabled"; + +type TBaseFeatureList = { + [key in TProjectBaseFeatureKeys]: TProperties; +}; + +export const PROJECT_BASE_FEATURES_LIST: TBaseFeatureList = { + cycles: { + key: "cycles", + property: "cycle_view", + title: "Cycles", + description: "Timebox work as you see fit per project and change frequency from one period to the next.", + icon: , + isPro: false, + isEnabled: true, + }, + modules: { + key: "modules", + property: "module_view", + title: "Modules", + description: "Group work into sub-project-like set-ups with their own leads and assignees.", + icon: , + isPro: false, + isEnabled: true, + }, + views: { + key: "views", + property: "issue_views_view", + title: "Views", + description: "Save sorts, filters, and display options for later or share them.", + icon: , + isPro: false, + isEnabled: true, + }, + pages: { + key: "pages", + property: "page_view", + title: "Pages", + description: "Write anything like you write anything.", + icon: , + isPro: false, + isEnabled: true, + }, + inbox: { + key: "intake", + property: "inbox_view", + title: "Intake", + description: "Consider and discuss work items before you add them to your project.", + icon: , + isPro: false, + isEnabled: true, + }, +}; + +type TOtherFeatureList = { + [key in TProjectOtherFeatureKeys]: TProperties; }; -export type TProjectFeatures = { - [key: string]: { +export const PROJECT_OTHER_FEATURES_LIST: TOtherFeatureList = { + is_time_tracking_enabled: { + key: "time_tracking", + property: "is_time_tracking_enabled", + title: "Time Tracking", + description: "Log time, see timesheets, and download full CSVs for your entire workspace.", + icon: , + isPro: true, + isEnabled: false, + }, +}; + +type TProjectFeatures = { + project_features: { + key: string; + title: string; + description: string; + featureList: TBaseFeatureList; + }; + project_others: { key: string; title: string; description: string; - featureList: TFeatureList; + featureList: TOtherFeatureList; }; }; @@ -31,68 +106,12 @@ export const PROJECT_FEATURES_LIST: TProjectFeatures = { key: "projects_and_issues", title: "Projects and work items", description: "Toggle these on or off this project.", - featureList: { - cycles: { - key: "cycles", - property: "cycle_view", - title: "Cycles", - description: "Timebox work as you see fit per project and change frequency from one period to the next.", - icon: , - isPro: false, - isEnabled: true, - }, - modules: { - key: "modules", - property: "module_view", - title: "Modules", - description: "Group work into sub-project-like set-ups with their own leads and assignees.", - icon: , - isPro: false, - isEnabled: true, - }, - views: { - key: "views", - property: "issue_views_view", - title: "Views", - description: "Save sorts, filters, and display options for later or share them.", - icon: , - isPro: false, - isEnabled: true, - }, - pages: { - key: "pages", - property: "page_view", - title: "Pages", - description: "Write anything like you write anything.", - icon: , - isPro: false, - isEnabled: true, - }, - inbox: { - key: "intake", - property: "inbox_view", - title: "Intake", - description: "Consider and discuss work items before you add them to your project.", - icon: , - isPro: false, - isEnabled: true, - }, - }, + featureList: PROJECT_BASE_FEATURES_LIST, }, project_others: { key: "work_management", title: "Work management", description: "Available only on some plans as indicated by the label next to the feature below.", - featureList: { - is_time_tracking_enabled: { - key: "time_tracking", - property: "is_time_tracking_enabled", - title: "Time Tracking", - description: "Log time, see timesheets, and download full CSVs for your entire workspace.", - icon: , - isPro: true, - isEnabled: false, - }, - }, + featureList: PROJECT_OTHER_FEATURES_LIST, }, }; 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 64f5f49b331..b73330a9370 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 @@ -61,7 +61,6 @@ 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; }; diff --git a/web/core/components/issues/issue-modal/modal.tsx b/web/core/components/issues/issue-modal/modal.tsx index 0d7681388b7..0ba526e1da8 100644 --- a/web/core/components/issues/issue-modal/modal.tsx +++ b/web/core/components/issues/issue-modal/modal.tsx @@ -2,6 +2,7 @@ import React from "react"; import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; // plane imports import { EIssuesStoreType } from "@plane/constants"; import type { TIssue } from "@plane/types"; @@ -30,11 +31,20 @@ export interface IssuesModalProps { templateId?: string; } -export const CreateUpdateIssueModal: React.FC = observer( - (props) => - props.isOpen && ( - - - - ) -); +export const CreateUpdateIssueModal: React.FC = observer((props) => { + // router params + const { cycleId, moduleId } = useParams(); + // derived values + const dataForPreload = { + ...props.data, + cycle_id: props.data?.cycle_id ? props.data?.cycle_id : cycleId ? cycleId.toString() : null, + module_ids: props.data?.module_ids ? props.data?.module_ids : moduleId ? [moduleId.toString()] : null, + }; + + if (!props.isOpen) return null; + return ( + + + + ); +}); diff --git a/web/core/components/labels/create-update-label-inline.tsx b/web/core/components/labels/create-update-label-inline.tsx index ddbe220b933..9127ee3e2a7 100644 --- a/web/core/components/labels/create-update-label-inline.tsx +++ b/web/core/components/labels/create-update-label-inline.tsx @@ -2,7 +2,6 @@ import React, { forwardRef, useEffect } from "react"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; import { TwitterPicker } from "react-color"; import { Controller, SubmitHandler, useForm } from "react-hook-form"; import { Popover, Transition } from "@headlessui/react"; @@ -11,13 +10,17 @@ import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IIssueLabel } from "@plane/types"; import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; -// hooks -import { useLabel } from "@/hooks/store"; -type Props = { +export type TLabelOperationsCallbacks = { + createLabel: (data: Partial) => Promise; + updateLabel: (labelId: string, data: Partial) => Promise; +}; + +type TCreateUpdateLabelInlineProps = { labelForm: boolean; setLabelForm: React.Dispatch>; isUpdating: boolean; + labelOperationsCallbacks: TLabelOperationsCallbacks; labelToUpdate?: IIssueLabel; onClose?: () => void; }; @@ -28,12 +31,8 @@ const defaultValues: Partial = { }; export const CreateUpdateLabelInline = observer( - forwardRef(function CreateUpdateLabelInline(props, ref) { - const { labelForm, setLabelForm, isUpdating, labelToUpdate, onClose } = props; - // router - const { workspaceSlug, projectId } = useParams(); - // store hooks - const { createLabel, updateLabel } = useLabel(); + forwardRef(function CreateUpdateLabelInline(props, ref) { + const { labelForm, setLabelForm, isUpdating, labelOperationsCallbacks, labelToUpdate, onClose } = props; // form info const { handleSubmit, @@ -56,9 +55,10 @@ export const CreateUpdateLabelInline = observer( }; const handleLabelCreate: SubmitHandler = async (formData) => { - if (!workspaceSlug || !projectId || isSubmitting) return; + if (isSubmitting) return; - await createLabel(workspaceSlug.toString(), projectId.toString(), formData) + await labelOperationsCallbacks + .createLabel(formData) .then(() => { handleClose(); reset(defaultValues); @@ -74,10 +74,10 @@ export const CreateUpdateLabelInline = observer( }; const handleLabelUpdate: SubmitHandler = async (formData) => { - if (!workspaceSlug || !projectId || isSubmitting) return; + if (!labelToUpdate?.id || isSubmitting) return; - // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - await updateLabel(workspaceSlug.toString(), projectId.toString(), labelToUpdate?.id!, formData) + await labelOperationsCallbacks + .updateLabel(labelToUpdate.id, formData) .then(() => { reset(defaultValues); handleClose(); @@ -92,6 +92,14 @@ export const CreateUpdateLabelInline = observer( }); }; + const handleFormSubmit = (formData: IIssueLabel) => { + if (isUpdating) { + handleLabelUpdate(formData); + } else { + handleLabelCreate(formData); + } + }; + /** * For settings focus on name input */ @@ -117,12 +125,8 @@ export const CreateUpdateLabelInline = observer( return ( <> -
{ - e.preventDefault(); - handleSubmit(isUpdating ? handleLabelUpdate : handleLabelCreate)(); - }} className={`flex w-full scroll-m-8 items-center gap-2 bg-custom-background-100 ${labelForm ? "" : "hidden"}`} >
@@ -199,10 +203,18 @@ export const CreateUpdateLabelInline = observer( - - +
{errors.name?.message &&

{errors.name?.message}

} ); diff --git a/web/core/components/labels/label-block/label-item-block.tsx b/web/core/components/labels/label-block/label-item-block.tsx index 4068e7defc9..6e1d2d82e4d 100644 --- a/web/core/components/labels/label-block/label-item-block.tsx +++ b/web/core/components/labels/label-block/label-item-block.tsx @@ -29,6 +29,7 @@ interface ILabelItemBlock { isLabelGroup?: boolean; dragHandleRef: MutableRefObject; disabled?: boolean; + draggable?: boolean; } export const LabelItemBlock = (props: ILabelItemBlock) => { @@ -40,9 +41,10 @@ export const LabelItemBlock = (props: ILabelItemBlock) => { isLabelGroup, dragHandleRef, disabled = false, + draggable = true, } = props; // states - const [isMenuActive, setIsMenuActive] = useState(false); + const [isMenuActive, setIsMenuActive] = useState(true); // refs const actionSectionRef = useRef(null); @@ -51,7 +53,7 @@ export const LabelItemBlock = (props: ILabelItemBlock) => { return (
- {!disabled && ( + {!disabled && draggable && ( { {!disabled && (
{ isVisible && ( onClick(label)}> - + {text} @@ -87,10 +89,10 @@ export const LabelItemBlock = (props: ILabelItemBlock) => { {!isLabelGroup && (
)} diff --git a/web/core/components/labels/project-setting-label-group.tsx b/web/core/components/labels/project-setting-label-group.tsx index 898931b3443..070a32c939d 100644 --- a/web/core/components/labels/project-setting-label-group.tsx +++ b/web/core/components/labels/project-setting-label-group.tsx @@ -7,7 +7,7 @@ import { Disclosure, Transition } from "@headlessui/react"; // types import { IIssueLabel } from "@plane/types"; // components -import { CreateUpdateLabelInline } from "./create-update-label-inline"; +import { CreateUpdateLabelInline, TLabelOperationsCallbacks } from "./create-update-label-inline"; import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block"; import { LabelDndHOC } from "./label-drag-n-drop-HOC"; import { ProjectSettingLabelItem } from "./project-setting-label-item"; @@ -25,6 +25,7 @@ type Props = { droppedLabelId: string | undefined, dropAtEndOfList: boolean ) => void; + labelOperationsCallbacks: TLabelOperationsCallbacks; isEditable?: boolean; }; @@ -38,6 +39,7 @@ export const ProjectSettingLabelGroup: React.FC = observer((props) => { isLastChild, onDrop, isEditable = false, + labelOperationsCallbacks, } = props; // states @@ -87,6 +89,7 @@ export const ProjectSettingLabelGroup: React.FC = observer((props) => { setLabelForm={setEditLabelForm} isUpdating labelToUpdate={label} + labelOperationsCallbacks={labelOperationsCallbacks} onClose={() => { setEditLabelForm(false); setIsUpdating(false); @@ -134,6 +137,7 @@ export const ProjectSettingLabelGroup: React.FC = observer((props) => { isLastChild={index === labelChildren.length - 1} onDrop={onDrop} isEditable={isEditable} + labelOperationsCallbacks={labelOperationsCallbacks} />
diff --git a/web/core/components/labels/project-setting-label-item.tsx b/web/core/components/labels/project-setting-label-item.tsx index 0b9a8732e27..5eabffd772a 100644 --- a/web/core/components/labels/project-setting-label-item.tsx +++ b/web/core/components/labels/project-setting-label-item.tsx @@ -6,7 +6,7 @@ import { IIssueLabel } from "@plane/types"; // hooks import { useLabel } from "@/hooks/store"; // components -import { CreateUpdateLabelInline } from "./create-update-label-inline"; +import { CreateUpdateLabelInline, TLabelOperationsCallbacks } from "./create-update-label-inline"; import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block"; import { LabelDndHOC } from "./label-drag-n-drop-HOC"; @@ -23,6 +23,7 @@ type Props = { droppedLabelId: string | undefined, dropAtEndOfList: boolean ) => void; + labelOperationsCallbacks: TLabelOperationsCallbacks; isEditable?: boolean; }; @@ -35,6 +36,7 @@ export const ProjectSettingLabelItem: React.FC = (props) => { isLastChild, isParentDragging = false, onDrop, + labelOperationsCallbacks, isEditable = false, } = props; // states @@ -89,6 +91,7 @@ export const ProjectSettingLabelItem: React.FC = (props) => { setLabelForm={setEditLabelForm} isUpdating labelToUpdate={label} + labelOperationsCallbacks={labelOperationsCallbacks} onClose={() => { setEditLabelForm(false); setIsUpdating(false); diff --git a/web/core/components/labels/project-setting-label-list.tsx b/web/core/components/labels/project-setting-label-list.tsx index b36e1c6c644..8150eac5fce 100644 --- a/web/core/components/labels/project-setting-label-list.tsx +++ b/web/core/components/labels/project-setting-label-list.tsx @@ -14,6 +14,7 @@ import { DeleteLabelModal, ProjectSettingLabelGroup, ProjectSettingLabelItem, + TLabelOperationsCallbacks, } from "@/components/labels"; // hooks import { useLabel, useUserPermissions } from "@/hooks/store"; @@ -24,7 +25,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { // router const { workspaceSlug, projectId } = useParams(); // refs - const scrollToRef = useRef(null); + const scrollToRef = useRef(null); // states const [showLabelForm, setLabelForm] = useState(false); const [isUpdating, setIsUpdating] = useState(false); @@ -32,11 +33,16 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { // plane hooks const { t } = useTranslation(); // store hooks - const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel(); + const { projectLabels, updateLabelPosition, projectLabelsTree, createLabel, updateLabel } = useLabel(); const { allowPermissions } = useUserPermissions(); // derived values const isEditable = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/project-settings/labels" }); + const labelOperationsCallbacks: TLabelOperationsCallbacks = { + createLabel: (data: Partial) => createLabel(workspaceSlug?.toString(), projectId?.toString(), data), + updateLabel: (labelId: string, data: Partial) => + updateLabel(workspaceSlug?.toString(), projectId?.toString(), labelId, data), + }; const newLabel = () => { setIsUpdating(false); @@ -84,6 +90,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { labelForm={showLabelForm} setLabelForm={setLabelForm} isUpdating={isUpdating} + labelOperationsCallbacks={labelOperationsCallbacks} ref={scrollToRef} onClose={() => { setLabelForm(false); @@ -117,6 +124,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { isLastChild={index === projectLabelsTree.length - 1} onDrop={onDrop} isEditable={isEditable} + labelOperationsCallbacks={labelOperationsCallbacks} /> ); } @@ -130,6 +138,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { isLastChild={index === projectLabelsTree.length - 1} onDrop={onDrop} isEditable={isEditable} + labelOperationsCallbacks={labelOperationsCallbacks} /> ); })} diff --git a/web/core/components/project-states/create-update/create.tsx b/web/core/components/project-states/create-update/create.tsx index 6a87082b438..fa72d0844e6 100644 --- a/web/core/components/project-states/create-update/create.tsx +++ b/web/core/components/project-states/create-update/create.tsx @@ -2,41 +2,48 @@ import { FC, useState } from "react"; import { observer } from "mobx-react"; -import { STATE_CREATED, STATE_GROUPS } from "@plane/constants"; -import { IState, TStateGroups } from "@plane/types"; +import { EventProps, STATE_CREATED, STATE_GROUPS } from "@plane/constants"; +import { IState, TStateGroups, TStateOperationsCallbacks } from "@plane/types"; import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { StateForm } from "@/components/project-states"; // hooks -import { useEventTracker, useProjectState } from "@/hooks/store"; +import { useEventTracker } from "@/hooks/store"; type TStateCreate = { - workspaceSlug: string; - projectId: string; groupKey: TStateGroups; + shouldTrackEvents: boolean; + createStateCallback: TStateOperationsCallbacks["createState"]; handleClose: () => void; }; export const StateCreate: FC = observer((props) => { - const { workspaceSlug, projectId, groupKey, handleClose } = props; + const { groupKey, shouldTrackEvents, createStateCallback, handleClose } = props; // hooks const { captureProjectStateEvent, setTrackElement } = useEventTracker(); - const { createState } = useProjectState(); // states const [loader, setLoader] = useState(false); + const captureEventIfEnabled = (props: EventProps) => { + if (shouldTrackEvents) { + captureProjectStateEvent(props); + } + }; + const onCancel = () => { setLoader(false); handleClose(); }; const onSubmit = async (formData: Partial) => { - if (!workspaceSlug || !projectId || !groupKey) return { status: "error" }; + if (!groupKey) return { status: "error" }; - setTrackElement("PROJECT_SETTINGS_STATE_PAGE"); + if (shouldTrackEvents) { + setTrackElement("PROJECT_SETTINGS_STATE_PAGE"); + } try { - const stateResponse = await createState(workspaceSlug, projectId, { ...formData, group: groupKey }); - captureProjectStateEvent({ + const stateResponse = await createStateCallback({ ...formData, group: groupKey }); + captureEventIfEnabled({ eventName: STATE_CREATED, payload: { ...stateResponse, @@ -53,7 +60,7 @@ export const StateCreate: FC = observer((props) => { return { status: "success" }; } catch (error) { const errorStatus = error as unknown as { status: number; data: { error: string } }; - captureProjectStateEvent({ + captureEventIfEnabled({ eventName: STATE_CREATED, payload: { ...formData, diff --git a/web/core/components/project-states/create-update/form.tsx b/web/core/components/project-states/create-update/form.tsx index 105fb984f49..2b985edd96f 100644 --- a/web/core/components/project-states/create-update/form.tsx +++ b/web/core/components/project-states/create-update/form.tsx @@ -1,6 +1,6 @@ "use client"; -import { FormEvent, FC, useEffect, useState, useMemo } from "react"; +import { FC, useEffect, useState, useMemo } from "react"; import { TwitterPicker } from "react-color"; import { IState } from "@plane/types"; import { Button, Popover, Input, TextArea } from "@plane/ui"; @@ -28,7 +28,7 @@ export const StateForm: FC = (props) => { setErrors((prev) => ({ ...prev, [key]: "" })); }; - const formSubmit = async (event: FormEvent) => { + const formSubmit = async (event: React.MouseEvent) => { event.preventDefault(); const name = formData?.name || undefined; @@ -59,7 +59,7 @@ export const StateForm: FC = (props) => { ); return ( -
+
{/* color */}
@@ -94,7 +94,7 @@ export const StateForm: FC = (props) => { />
-
- +
); }; diff --git a/web/core/components/project-states/create-update/update.tsx b/web/core/components/project-states/create-update/update.tsx index 80014eeba42..000177f6415 100644 --- a/web/core/components/project-states/create-update/update.tsx +++ b/web/core/components/project-states/create-update/update.tsx @@ -2,27 +2,25 @@ import { FC, useState } from "react"; import { observer } from "mobx-react"; -import { STATE_UPDATED } from "@plane/constants"; -import { IState } from "@plane/types"; +import { EventProps, STATE_UPDATED } from "@plane/constants"; +import { IState, TStateOperationsCallbacks } from "@plane/types"; import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { StateForm } from "@/components/project-states"; -// constants // hooks -import { useEventTracker, useProjectState } from "@/hooks/store"; +import { useEventTracker } from "@/hooks/store"; type TStateUpdate = { - workspaceSlug: string; - projectId: string; state: IState; + updateStateCallback: TStateOperationsCallbacks["updateState"]; + shouldTrackEvents: boolean; handleClose: () => void; }; export const StateUpdate: FC = observer((props) => { - const { workspaceSlug, projectId, state, handleClose } = props; + const { state, updateStateCallback, shouldTrackEvents, handleClose } = props; // hooks const { captureProjectStateEvent, setTrackElement } = useEventTracker(); - const { updateState } = useProjectState(); // states const [loader, setLoader] = useState(false); @@ -31,13 +29,21 @@ export const StateUpdate: FC = observer((props) => { handleClose(); }; + const captureEventIfEnabled = (props: EventProps) => { + if (shouldTrackEvents) { + captureProjectStateEvent(props); + } + }; + const onSubmit = async (formData: Partial) => { - if (!workspaceSlug || !projectId || !state.id) return { status: "error" }; + if (!state.id) return { status: "error" }; - setTrackElement("PROJECT_SETTINGS_STATE_PAGE"); + if (shouldTrackEvents) { + setTrackElement("PROJECT_SETTINGS_STATE_PAGE"); + } try { - const stateResponse = await updateState(workspaceSlug, projectId, state.id, formData); - captureProjectStateEvent({ + const stateResponse = await updateStateCallback(state.id, formData); + captureEventIfEnabled({ eventName: STATE_UPDATED, payload: { ...stateResponse, @@ -67,7 +73,7 @@ export const StateUpdate: FC = observer((props) => { title: "Error!", message: "State could not be updated. Please try again.", }); - captureProjectStateEvent({ + captureEventIfEnabled({ eventName: STATE_UPDATED, payload: { ...formData, diff --git a/web/core/components/project-states/group-item.tsx b/web/core/components/project-states/group-item.tsx index fe9fcad152d..43c31a079ec 100644 --- a/web/core/components/project-states/group-item.tsx +++ b/web/core/components/project-states/group-item.tsx @@ -4,35 +4,38 @@ import { FC, useState, useRef } from "react"; import { observer } from "mobx-react"; import { ChevronDown, Plus } from "lucide-react"; // plane imports -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { IState, TStateGroups } from "@plane/types"; +import { IState, TStateGroups, TStateOperationsCallbacks } from "@plane/types"; import { StateGroupIcon } from "@plane/ui"; import { cn } from "@plane/utils"; // components import { StateList, StateCreate } from "@/components/project-states"; -// hooks -import { useUserPermissions } from "@/hooks/store"; type TGroupItem = { - workspaceSlug: string; - projectId: string; groupKey: TStateGroups; groupsExpanded: Partial[]; - handleGroupCollapse: (groupKey: TStateGroups) => void; - handleExpand: (groupKey: TStateGroups) => void; groupedStates: Record; states: IState[]; + stateOperationsCallbacks: TStateOperationsCallbacks; + isEditable: boolean; + shouldTrackEvents: boolean; + groupItemClassName?: string; + stateItemClassName?: string; + handleGroupCollapse: (groupKey: TStateGroups) => void; + handleExpand: (groupKey: TStateGroups) => void; }; export const GroupItem: FC = observer((props) => { const { - workspaceSlug, - projectId, groupKey, groupedStates, states, groupsExpanded, + isEditable, + stateOperationsCallbacks, + shouldTrackEvents, + groupItemClassName, + stateItemClassName, handleExpand, handleGroupCollapse, } = props; @@ -40,18 +43,18 @@ export const GroupItem: FC = observer((props) => { const dropElementRef = useRef(null); // plane hooks const { t } = useTranslation(); - // store hooks - const { allowPermissions } = useUserPermissions(); // state const [createState, setCreateState] = useState(false); // derived values const currentStateExpanded = groupsExpanded.includes(groupKey); - const isEditable = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); const shouldShowEmptyState = states.length === 0 && currentStateExpanded && !createState; return (
@@ -76,12 +79,18 @@ export const GroupItem: FC = observer((props) => {
{groupKey}
@@ -97,12 +106,13 @@ export const GroupItem: FC = observer((props) => { {currentStateExpanded && (
)} @@ -110,10 +120,10 @@ export const GroupItem: FC = observer((props) => { {isEditable && createState && (
setCreateState(false)} + createStateCallback={stateOperationsCallbacks.createState} + shouldTrackEvents={shouldTrackEvents} />
)} diff --git a/web/core/components/project-states/group-list.tsx b/web/core/components/project-states/group-list.tsx index 1fbc8f1daa9..001a534ac80 100644 --- a/web/core/components/project-states/group-list.tsx +++ b/web/core/components/project-states/group-list.tsx @@ -2,18 +2,32 @@ import { FC, useState } from "react"; import { observer } from "mobx-react"; -import { IState, TStateGroups } from "@plane/types"; +// plane imports +import { IState, TStateGroups, TStateOperationsCallbacks } from "@plane/types"; +import { cn } from "@plane/utils"; // components import { GroupItem } from "@/components/project-states"; type TGroupList = { - workspaceSlug: string; - projectId: string; groupedStates: Record; + stateOperationsCallbacks: TStateOperationsCallbacks; + isEditable: boolean; + shouldTrackEvents: boolean; + groupListClassName?: string; + groupItemClassName?: string; + stateItemClassName?: string; }; export const GroupList: FC = observer((props) => { - const { workspaceSlug, projectId, groupedStates } = props; + const { + groupedStates, + stateOperationsCallbacks, + isEditable, + shouldTrackEvents, + groupListClassName, + groupItemClassName, + stateItemClassName, + } = props; // states const [groupsExpanded, setGroupsExpanded] = useState[]>([]); @@ -35,21 +49,24 @@ export const GroupList: FC = observer((props) => { }); }; return ( -
+
{Object.entries(groupedStates).map(([key, value]) => { const groupKey = key as TStateGroups; const groupStates = value; return ( ); })} diff --git a/web/core/components/project-states/options/delete.tsx b/web/core/components/project-states/options/delete.tsx index f123a2513ba..13a37a7641d 100644 --- a/web/core/components/project-states/options/delete.tsx +++ b/web/core/components/project-states/options/delete.tsx @@ -3,45 +3,49 @@ import { FC, useState } from "react"; import { observer } from "mobx-react"; import { Loader, X } from "lucide-react"; -import { STATE_DELETED } from "@plane/constants"; -import { IState } from "@plane/types"; +// plane imports +import { EventProps, STATE_DELETED } from "@plane/constants"; +import { IState, TStateOperationsCallbacks } from "@plane/types"; import { AlertModalCore, TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; -// constants -// helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks -import { useEventTracker, useProjectState } from "@/hooks/store"; +import { useEventTracker } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; type TStateDelete = { - workspaceSlug: string; - projectId: string; totalStates: number; state: IState; + deleteStateCallback: TStateOperationsCallbacks["deleteState"]; + shouldTrackEvents: boolean; }; export const StateDelete: FC = observer((props) => { - const { workspaceSlug, projectId, totalStates, state } = props; + const { totalStates, state, deleteStateCallback, shouldTrackEvents } = props; // hooks const { isMobile } = usePlatformOS(); const { captureProjectStateEvent, setTrackElement } = useEventTracker(); - const { deleteState } = useProjectState(); // states const [isDeleteModal, setIsDeleteModal] = useState(false); const [isDelete, setIsDelete] = useState(false); - // derived values const isDeleteDisabled = state.default ? true : totalStates === 1 ? true : false; - const handleDeleteState = async () => { - if (!workspaceSlug || !projectId || isDeleteDisabled) return; + const captureEventIfEnabled = (props: EventProps) => { + if (shouldTrackEvents) { + captureProjectStateEvent(props); + } + }; - setTrackElement("PROJECT_SETTINGS_STATE_PAGE"); + const handleDeleteState = async () => { + if (isDeleteDisabled) return; + if (shouldTrackEvents) { + setTrackElement("PROJECT_SETTINGS_STATE_PAGE"); + } setIsDelete(true); try { - await deleteState(workspaceSlug, projectId, state.id); - captureProjectStateEvent({ + await deleteStateCallback(state.id); + captureEventIfEnabled({ eventName: STATE_DELETED, payload: { ...state, @@ -51,7 +55,7 @@ export const StateDelete: FC = observer((props) => { setIsDelete(false); } catch (error) { const errorStatus = error as unknown as { status: number; data: { error: string } }; - captureProjectStateEvent({ + captureEventIfEnabled({ eventName: STATE_DELETED, payload: { ...state, @@ -94,6 +98,7 @@ export const StateDelete: FC = observer((props) => { /> - +
)} diff --git a/web/core/components/project-states/state-item.tsx b/web/core/components/project-states/state-item.tsx index e3ed07a30c3..192211da305 100644 --- a/web/core/components/project-states/state-item.tsx +++ b/web/core/components/project-states/state-item.tsx @@ -7,55 +7,63 @@ import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag- import { observer } from "mobx-react"; // Plane import { TDraggableData } from "@plane/constants"; -import { IState, TStateGroups } from "@plane/types"; +import { IState, TStateGroups, TStateOperationsCallbacks } from "@plane/types"; import { DropIndicator } from "@plane/ui"; // components import { StateItemTitle, StateUpdate } from "@/components/project-states"; // helpers import { cn } from "@/helpers/common.helper"; import { getCurrentStateSequence } from "@/helpers/state.helper"; -// hooks -import { useProjectState } from "@/hooks/store"; type TStateItem = { - workspaceSlug: string; - projectId: string; groupKey: TStateGroups; groupedStates: Record; totalStates: number; state: IState; + stateOperationsCallbacks: TStateOperationsCallbacks; + shouldTrackEvents: boolean; disabled?: boolean; + stateItemClassName?: string; }; export const StateItem: FC = observer((props) => { - const { workspaceSlug, projectId, groupKey, groupedStates, totalStates, state, disabled = false } = props; - // hooks - const { moveStatePosition } = useProjectState(); + const { + groupKey, + groupedStates, + totalStates, + state, + stateOperationsCallbacks, + shouldTrackEvents, + disabled = false, + stateItemClassName, + } = props; + // ref + const draggableElementRef = useRef(null); // states const [updateStateModal, setUpdateStateModal] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const [isDraggedOver, setIsDraggedOver] = useState(false); + const [closestEdge, setClosestEdge] = useState(null); + // derived values + const isDraggable = totalStates === 1 ? false : true; + const commonStateItemListProps = { + stateCount: totalStates, + state: state, + setUpdateStateModal: setUpdateStateModal, + }; const handleStateSequence = useCallback( async (payload: Partial) => { try { - if (!workspaceSlug || !projectId || !payload.id) return; - await moveStatePosition(workspaceSlug, projectId, payload.id, payload); + if (!payload.id) return; + await stateOperationsCallbacks.moveStatePosition(payload.id, payload); } catch (error) { console.error("error", error); } }, - [workspaceSlug, projectId, moveStatePosition] + [stateOperationsCallbacks] ); - // derived values - const isDraggable = totalStates === 1 ? false : true; - - // DND starts - // ref - const draggableElementRef = useRef(null); - // states - const [isDragging, setIsDragging] = useState(false); - const [isDraggedOver, setIsDraggedOver] = useState(false); - const [closestEdge, setClosestEdge] = useState(null); useEffect(() => { const elementRef = draggableElementRef.current; const initialData: TDraggableData = { groupKey: groupKey, id: state.id }; @@ -111,9 +119,9 @@ export const StateItem: FC = observer((props) => { if (updateStateModal) return ( setUpdateStateModal(false)} /> ); @@ -122,25 +130,29 @@ export const StateItem: FC = observer((props) => { {/* draggable drop top indicator */} -
- + {disabled ? ( + + ) : ( + + )}
- {/* draggable drop bottom indicator */}
diff --git a/web/core/components/project-states/state-list.tsx b/web/core/components/project-states/state-list.tsx index 5e7f79916d3..537985aeb46 100644 --- a/web/core/components/project-states/state-list.tsx +++ b/web/core/components/project-states/state-list.tsx @@ -2,34 +2,44 @@ import { FC } from "react"; import { observer } from "mobx-react"; -import { IState, TStateGroups } from "@plane/types"; +import { IState, TStateGroups, TStateOperationsCallbacks } from "@plane/types"; // components import { StateItem } from "@/components/project-states"; type TStateList = { - workspaceSlug: string; - projectId: string; groupKey: TStateGroups; groupedStates: Record; states: IState[]; + stateOperationsCallbacks: TStateOperationsCallbacks; + shouldTrackEvents: boolean; disabled?: boolean; + stateItemClassName?: string; }; export const StateList: FC = observer((props) => { - const { workspaceSlug, projectId, groupKey, groupedStates, states, disabled = false } = props; + const { + groupKey, + groupedStates, + states, + stateOperationsCallbacks, + shouldTrackEvents, + disabled = false, + stateItemClassName, + } = props; return ( <> {states.map((state: IState) => ( ))} diff --git a/web/core/components/project/create-project-modal.tsx b/web/core/components/project/create-project-modal.tsx index d0015e32a19..0af90885c69 100644 --- a/web/core/components/project/create-project-modal.tsx +++ b/web/core/components/project/create-project-modal.tsx @@ -19,6 +19,7 @@ type Props = { setToFavorite?: boolean; workspaceSlug: string; data?: Partial; + templateId?: string; }; enum EProjectCreationSteps { @@ -27,7 +28,7 @@ enum EProjectCreationSteps { } export const CreateProjectModal: FC = (props) => { - const { isOpen, onClose, setToFavorite = false, workspaceSlug, data } = props; + const { isOpen, onClose, setToFavorite = false, workspaceSlug, data, templateId } = props; // states const [currentStep, setCurrentStep] = useState(EProjectCreationSteps.CREATE_PROJECT); const [createdProjectId, setCreatedProjectId] = useState(null); @@ -63,6 +64,7 @@ export const CreateProjectModal: FC = (props) => { updateCoverImageStatus={handleCoverImageStatusUpdate} handleNextStep={handleNextStep} data={data} + templateId={templateId} /> )} {currentStep === EProjectCreationSteps.FEATURE_SELECTION && ( diff --git a/web/core/components/project/create/header.tsx b/web/core/components/project/create/header.tsx index 2353b36d172..fa841151f24 100644 --- a/web/core/components/project/create/header.tsx +++ b/web/core/components/project/create/header.tsx @@ -14,6 +14,8 @@ import { ImagePickerPopover } from "@/components/core"; import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper"; import { getFileURL } from "@/helpers/file.helper"; import { getTabIndex } from "@/helpers/tab-indices.helper"; +// plane web imports +import { ProjectTemplateSelect } from "@/plane-web/components/projects/create/template-select"; type Props = { handleClose: () => void; @@ -39,6 +41,9 @@ const ProjectCreateHeader: React.FC = (props) => { /> )} +
+ +