From bbbd2a78a5114a94c6e4d435d5d0dad0b558eefd Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Mon, 7 Apr 2025 18:03:13 +0530 Subject: [PATCH 1/8] improvement: work item modal data preload and parent work item details --- .../issues/issue-modal/provider.tsx | 4 +-- .../context/issue-modal-context.tsx | 1 - .../components/issues/issue-modal/modal.tsx | 26 +++++++++++++------ 3 files changed, 20 insertions(+), 11 deletions(-) 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/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 ( + + + + ); +}); From ae1a7711f13172e2de55e12eb58f87bd920cfeef Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Mon, 7 Apr 2025 18:04:23 +0530 Subject: [PATCH 2/8] improvement: collapsible button title --- packages/ui/src/collapsible/collapsible-button.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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; From 56d7c29a20700db1a1ddd36bf3a92fb024009d52 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Mon, 7 Apr 2025 20:06:45 +0530 Subject: [PATCH 3/8] improvement: project creation form and modal --- packages/constants/src/project.ts | 21 ++++++++- packages/ui/src/dropdowns/helper.tsx | 16 +++---- web/ce/components/projects/create/root.tsx | 26 +++-------- .../projects/create/template-select.tsx | 12 ++++++ .../project/create-project-modal.tsx | 4 +- web/core/components/project/create/header.tsx | 5 +++ web/core/store/project/project.store.ts | 43 ++++++++++++++----- 7 files changed, 85 insertions(+), 42 deletions(-) create mode 100644 web/ce/components/projects/create/template-select.tsx 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/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/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/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) => { /> )} +
+ +
- - +
{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} /> ); })} From 1c472503658d4cfdc12f4e4ff6f6b1636a0bae13 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Mon, 7 Apr 2025 20:14:47 +0530 Subject: [PATCH 6/8] improvement: enable state group and state list components modularity --- packages/constants/src/state.ts | 18 ++--- packages/types/src/inbox.d.ts | 4 +- packages/types/src/state.d.ts | 8 ++ .../project-states/create-update/create.tsx | 31 ++++--- .../project-states/create-update/form.tsx | 10 +-- .../project-states/create-update/update.tsx | 32 +++++--- .../components/project-states/group-item.tsx | 52 +++++++----- .../components/project-states/group-list.tsx | 31 +++++-- .../project-states/options/delete.tsx | 39 +++++---- .../options/mark-as-default.tsx | 23 +++--- web/core/components/project-states/root.tsx | 46 ++++++++++- .../project-states/state-item-title.tsx | 46 ++++++----- .../components/project-states/state-item.tsx | 80 +++++++++++-------- .../components/project-states/state-list.tsx | 22 +++-- 14 files changed, 280 insertions(+), 162 deletions(-) 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/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) => ( ))} From b33004b370f55552246fa57e65f8de4eac3d3fb1 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Mon, 7 Apr 2025 20:15:18 +0530 Subject: [PATCH 7/8] improvement: project settings feature list --- .../constants/project/settings/features.tsx | 145 ++++++++++-------- .../project/settings/features-list.tsx | 91 +++++------ 2 files changed, 124 insertions(+), 112 deletions(-) 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/project/settings/features-list.tsx b/web/core/components/project/settings/features-list.tsx index f31f5a754e9..05045f7178c 100644 --- a/web/core/components/project/settings/features-list.tsx +++ b/web/core/components/project/settings/features-list.tsx @@ -59,58 +59,51 @@ export const ProjectFeaturesList: FC = observer((props) => { return (
- {Object.keys(PROJECT_FEATURES_LIST).map((featureSectionKey) => { - const feature = PROJECT_FEATURES_LIST[featureSectionKey]; - return ( -
-
-

{t(feature.key)}

-

{t(`${feature.key}_description`)}

-
- {Object.keys(feature.featureList).map((featureItemKey) => { - const featureItem = feature.featureList[featureItemKey]; - return ( -
-
-
-
- {featureItem.icon} -
-
-
-

{t(featureItem.key)}

- {featureItem.isPro && ( - - - - )} -
-

- {t(`${featureItem.key}_description`)} -

-
-
- - handleSubmit(featureItemKey, featureItem.property)} - disabled={!featureItem.isEnabled || !isAdmin} - size="sm" - /> + {Object.entries(PROJECT_FEATURES_LIST).map(([featureSectionKey, feature]) => ( +
+
+

{t(feature.key)}

+

{t(`${feature.key}_description`)}

+
+ {Object.entries(feature.featureList).map(([featureItemKey, featureItem]) => ( +
+
+
+
+ {featureItem.icon}
-
- {currentProjectDetails?.[featureItem.property as keyof IProject] && - featureItem.renderChildren?.(currentProjectDetails, workspaceSlug)} +
+
+

{t(featureItem.key)}

+ {featureItem.isPro && ( + + + + )} +
+

+ {t(`${featureItem.key}_description`)} +

- ); - })} -
- ); - })} + handleSubmit(featureItemKey, featureItem.property)} + disabled={!featureItem.isEnabled || !isAdmin} + size="sm" + /> +
+
+ {currentProjectDetails?.[featureItem.property as keyof IProject] && + featureItem.renderChildren?.(currentProjectDetails, workspaceSlug)} +
+
+ ))} +
+ ))}
); }); From c9e4fe3a36c874bf4cf71af55e627ae2898b75da Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Mon, 7 Apr 2025 20:15:37 +0530 Subject: [PATCH 8/8] improvement: common utils --- packages/utils/src/common.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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; +};