[WEB-3788] improvement: enhance project properties related components modularity (#6882)
* improvement: work item modal data preload and parent work item details * improvement: collapsible button title * improvement: project creation form and modal * improvement: emoji helper * improvement: enhance labels component modularity * improvement: enable state group and state list components modularity * improvement: project settings feature list * improvement: common utils
This commit is contained in:
parent
670134562f
commit
1f9222065e
36 changed files with 622 additions and 381 deletions
|
|
@ -8,3 +8,18 @@ export const ISSUE_REACTION_EMOJI_CODES = [
|
||||||
"9992",
|
"9992",
|
||||||
"128064",
|
"128064",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const RANDOM_EMOJI_CODES = [
|
||||||
|
"8986",
|
||||||
|
"9200",
|
||||||
|
"128204",
|
||||||
|
"127773",
|
||||||
|
"127891",
|
||||||
|
"128076",
|
||||||
|
"128077",
|
||||||
|
"128187",
|
||||||
|
"128188",
|
||||||
|
"128512",
|
||||||
|
"128522",
|
||||||
|
"128578",
|
||||||
|
];
|
||||||
|
|
|
||||||
|
|
@ -29,3 +29,4 @@ export * from "./event-tracker";
|
||||||
export * from "./spreadsheet";
|
export * from "./spreadsheet";
|
||||||
export * from "./dashboard";
|
export * from "./dashboard";
|
||||||
export * from "./page";
|
export * from "./page";
|
||||||
|
export * from "./emoji";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
// icons
|
// plane imports
|
||||||
import { TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types";
|
import { IProject, TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types";
|
||||||
|
// local imports
|
||||||
|
import { RANDOM_EMOJI_CODES } from "./emoji";
|
||||||
|
|
||||||
export type TNetworkChoiceIconKey = "Lock" | "Globe2";
|
export type TNetworkChoiceIconKey = "Lock" | "Globe2";
|
||||||
|
|
||||||
|
|
@ -132,3 +134,18 @@ export const PROJECT_ERROR_MESSAGES = {
|
||||||
i18n_message: "workspace_projects.error.issue_delete",
|
i18n_message: "workspace_projects.error.issue_delete",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_PROJECT_FORM_VALUES: Partial<IProject> = {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
export type TStateGroups =
|
export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled";
|
||||||
| "backlog"
|
|
||||||
| "unstarted"
|
|
||||||
| "started"
|
|
||||||
| "completed"
|
|
||||||
| "cancelled";
|
|
||||||
|
|
||||||
export type TDraggableData = {
|
export type TDraggableData = {
|
||||||
groupKey: TStateGroups;
|
groupKey: TStateGroups;
|
||||||
|
|
@ -14,40 +9,43 @@ export const STATE_GROUPS: {
|
||||||
[key in TStateGroups]: {
|
[key in TStateGroups]: {
|
||||||
key: TStateGroups;
|
key: TStateGroups;
|
||||||
label: string;
|
label: string;
|
||||||
|
defaultStateName: string;
|
||||||
color: string;
|
color: string;
|
||||||
};
|
};
|
||||||
} = {
|
} = {
|
||||||
backlog: {
|
backlog: {
|
||||||
key: "backlog",
|
key: "backlog",
|
||||||
label: "Backlog",
|
label: "Backlog",
|
||||||
|
defaultStateName: "Backlog",
|
||||||
color: "#d9d9d9",
|
color: "#d9d9d9",
|
||||||
},
|
},
|
||||||
unstarted: {
|
unstarted: {
|
||||||
key: "unstarted",
|
key: "unstarted",
|
||||||
label: "Unstarted",
|
label: "Unstarted",
|
||||||
|
defaultStateName: "Todo",
|
||||||
color: "#3f76ff",
|
color: "#3f76ff",
|
||||||
},
|
},
|
||||||
started: {
|
started: {
|
||||||
key: "started",
|
key: "started",
|
||||||
label: "Started",
|
label: "Started",
|
||||||
|
defaultStateName: "In Progress",
|
||||||
color: "#f59e0b",
|
color: "#f59e0b",
|
||||||
},
|
},
|
||||||
completed: {
|
completed: {
|
||||||
key: "completed",
|
key: "completed",
|
||||||
label: "Completed",
|
label: "Completed",
|
||||||
|
defaultStateName: "Done",
|
||||||
color: "#16a34a",
|
color: "#16a34a",
|
||||||
},
|
},
|
||||||
cancelled: {
|
cancelled: {
|
||||||
key: "cancelled",
|
key: "cancelled",
|
||||||
label: "Canceled",
|
label: "Canceled",
|
||||||
|
defaultStateName: "Cancelled",
|
||||||
color: "#dc2626",
|
color: "#dc2626",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ARCHIVABLE_STATE_GROUPS = [
|
export const ARCHIVABLE_STATE_GROUPS = [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key];
|
||||||
STATE_GROUPS.completed.key,
|
|
||||||
STATE_GROUPS.cancelled.key,
|
|
||||||
];
|
|
||||||
export const COMPLETED_STATE_GROUPS = [STATE_GROUPS.completed.key];
|
export const COMPLETED_STATE_GROUPS = [STATE_GROUPS.completed.key];
|
||||||
export const PENDING_STATE_GROUPS = [
|
export const PENDING_STATE_GROUPS = [
|
||||||
STATE_GROUPS.backlog.key,
|
STATE_GROUPS.backlog.key,
|
||||||
|
|
|
||||||
4
packages/types/src/inbox.d.ts
vendored
4
packages/types/src/inbox.d.ts
vendored
|
|
@ -1,6 +1,8 @@
|
||||||
|
// plane constants
|
||||||
|
import { TInboxIssue, TInboxIssueStatus } from "@plane/constants";
|
||||||
|
// plane types
|
||||||
import { TPaginationInfo } from "./common";
|
import { TPaginationInfo } from "./common";
|
||||||
import { TIssuePriorities } from "./issues";
|
import { TIssuePriorities } from "./issues";
|
||||||
import { TIssue } from "./issues/base";
|
|
||||||
|
|
||||||
// filters
|
// filters
|
||||||
export type TInboxIssueFilterMemberKeys = "assignees" | "created_by";
|
export type TInboxIssueFilterMemberKeys = "assignees" | "created_by";
|
||||||
|
|
|
||||||
8
packages/types/src/state.d.ts
vendored
8
packages/types/src/state.d.ts
vendored
|
|
@ -24,3 +24,11 @@ export interface IStateLite {
|
||||||
export interface IStateResponse {
|
export interface IStateResponse {
|
||||||
[key: string]: IState[];
|
[key: string]: IState[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TStateOperationsCallbacks = {
|
||||||
|
createState: (data: Partial<IState>) => Promise<IState>;
|
||||||
|
updateState: (stateId: string, data: Partial<IState>) => Promise<IState | undefined>;
|
||||||
|
deleteState: (stateId: string) => Promise<void>;
|
||||||
|
moveStatePosition: (stateId: string, data: Partial<IState>) => Promise<void>;
|
||||||
|
markStateAsDefault: (stateId: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { DropdownIcon } from "../icons";
|
|
||||||
import { cn } from "../../helpers";
|
import { cn } from "../../helpers";
|
||||||
|
import { DropdownIcon } from "../icons";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
title: string;
|
title: React.ReactNode;
|
||||||
hideChevron?: boolean;
|
hideChevron?: boolean;
|
||||||
indicatorElement?: React.ReactNode;
|
indicatorElement?: React.ReactNode;
|
||||||
actionItemElement?: React.ReactNode;
|
actionItemElement?: React.ReactNode;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { clsx, type ClassValue } from "clsx";
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { CompleteOrEmpty } from "@plane/types";
|
||||||
|
|
||||||
// Support email can be configured by the application
|
// Support email can be configured by the application
|
||||||
export const getSupportEmail = (defaultEmail: string = ""): string => defaultEmail;
|
export const getSupportEmail = (defaultEmail: string = ""): string => defaultEmail;
|
||||||
|
|
@ -39,3 +40,21 @@ export const partitionValidIds = (ids: string[], validIds: string[]): { valid: s
|
||||||
|
|
||||||
return { valid, invalid };
|
return { valid, invalid };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an object is complete (has properties) rather than empty.
|
||||||
|
* This helps TypeScript narrow the type from CompleteOrEmpty<T> to T.
|
||||||
|
*
|
||||||
|
* @param obj The object to check, typed as CompleteOrEmpty<T>
|
||||||
|
* @returns A boolean indicating if the object is complete (true) or empty (false)
|
||||||
|
*/
|
||||||
|
export const isComplete = <T>(obj: CompleteOrEmpty<T>): 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;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { ISearchIssueResponse } from "@plane/types";
|
import { ISearchIssueResponse, TIssue } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { IssueModalContext } from "@/components/issues";
|
import { IssueModalContext } from "@/components/issues";
|
||||||
|
|
||||||
export type TIssueModalProviderProps = {
|
export type TIssueModalProviderProps = {
|
||||||
templateId?: string;
|
templateId?: string;
|
||||||
|
dataForPreload?: Partial<TIssue>;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -32,7 +33,6 @@ export const IssueModalProvider = observer((props: TIssueModalProviderProps) =>
|
||||||
getActiveAdditionalPropertiesLength: () => 0,
|
getActiveAdditionalPropertiesLength: () => 0,
|
||||||
handlePropertyValuesValidation: () => true,
|
handlePropertyValuesValidation: () => true,
|
||||||
handleCreateUpdatePropertyValues: () => Promise.resolve(),
|
handleCreateUpdatePropertyValues: () => Promise.resolve(),
|
||||||
handleParentWorkItemDetails: () => Promise.resolve(undefined),
|
|
||||||
handleProjectEntitiesFetch: () => Promise.resolve(),
|
handleProjectEntitiesFetch: () => Promise.resolve(),
|
||||||
handleTemplateChange: () => Promise.resolve(),
|
handleTemplateChange: () => Promise.resolve(),
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { useState, FC } from "react";
|
import { useState, FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
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";
|
import { useTranslation } from "@plane/i18n";
|
||||||
// ui
|
// ui
|
||||||
import { setToast, TOAST_TYPE } from "@plane/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 ProjectCommonAttributes from "@/components/project/create/common-attributes";
|
||||||
import ProjectCreateHeader from "@/components/project/create/header";
|
import ProjectCreateHeader from "@/components/project/create/header";
|
||||||
import ProjectCreateButtons from "@/components/project/create/project-create-buttons";
|
import ProjectCreateButtons from "@/components/project/create/project-create-buttons";
|
||||||
// helpers
|
|
||||||
import { getRandomEmoji } from "@/helpers/emoji.helper";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useProject } from "@/hooks/store";
|
import { useEventTracker, useProject } from "@/hooks/store";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
|
|
@ -26,26 +24,12 @@ export type TCreateProjectFormProps = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
handleNextStep: (projectId: string) => void;
|
handleNextStep: (projectId: string) => void;
|
||||||
data?: Partial<TProject>;
|
data?: Partial<TProject>;
|
||||||
|
templateId?: string;
|
||||||
updateCoverImageStatus: (projectId: string, coverImage: string) => Promise<void>;
|
updateCoverImageStatus: (projectId: string, coverImage: string) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues: Partial<TProject> = {
|
|
||||||
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<TCreateProjectFormProps> = observer((props) => {
|
export const CreateProjectForm: FC<TCreateProjectFormProps> = observer((props) => {
|
||||||
const { setToFavorite, workspaceSlug, onClose, handleNextStep, updateCoverImageStatus } = props;
|
const { setToFavorite, workspaceSlug, data, onClose, handleNextStep, updateCoverImageStatus } = props;
|
||||||
// store
|
// store
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { captureProjectEvent } = useEventTracker();
|
const { captureProjectEvent } = useEventTracker();
|
||||||
|
|
@ -54,7 +38,7 @@ export const CreateProjectForm: FC<TCreateProjectFormProps> = observer((props) =
|
||||||
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
|
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
|
||||||
// form info
|
// form info
|
||||||
const methods = useForm<TProject>({
|
const methods = useForm<TProject>({
|
||||||
defaultValues,
|
defaultValues: { ...DEFAULT_PROJECT_FORM_VALUES, ...data },
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
});
|
});
|
||||||
const { handleSubmit, reset, setValue } = methods;
|
const { handleSubmit, reset, setValue } = methods;
|
||||||
|
|
@ -105,7 +89,7 @@ export const CreateProjectForm: FC<TCreateProjectFormProps> = observer((props) =
|
||||||
handleNextStep(res.id);
|
handleNextStep(res.id);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
Object.keys(err.data).map((key) => {
|
Object.keys(err?.data ?? {}).map((key) => {
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.ERROR,
|
type: TOAST_TYPE.ERROR,
|
||||||
title: t("error"),
|
title: t("error"),
|
||||||
|
|
|
||||||
12
web/ce/components/projects/create/template-select.tsx
Normal file
12
web/ce/components/projects/create/template-select.tsx
Normal file
|
|
@ -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) => <></>;
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { FileText, Layers, Timer } from "lucide-react";
|
import { FileText, Layers, Timer } from "lucide-react";
|
||||||
|
// plane imports
|
||||||
import { IProject } from "@plane/types";
|
import { IProject } from "@plane/types";
|
||||||
import { ContrastIcon, DiceIcon, Intake } from "@plane/ui";
|
import { ContrastIcon, DiceIcon, Intake } from "@plane/ui";
|
||||||
|
|
||||||
|
|
@ -13,25 +14,15 @@ export type TProperties = {
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
renderChildren?: (currentProjectDetails: IProject, workspaceSlug: string) => ReactNode;
|
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 type TProjectFeatures = {
|
export const PROJECT_BASE_FEATURES_LIST: TBaseFeatureList = {
|
||||||
[key: string]: {
|
|
||||||
key: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
featureList: TFeatureList;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PROJECT_FEATURES_LIST: TProjectFeatures = {
|
|
||||||
project_features: {
|
|
||||||
key: "projects_and_issues",
|
|
||||||
title: "Projects and work items",
|
|
||||||
description: "Toggle these on or off this project.",
|
|
||||||
featureList: {
|
|
||||||
cycles: {
|
cycles: {
|
||||||
key: "cycles",
|
key: "cycles",
|
||||||
property: "cycle_view",
|
property: "cycle_view",
|
||||||
|
|
@ -77,13 +68,13 @@ export const PROJECT_FEATURES_LIST: TProjectFeatures = {
|
||||||
isPro: false,
|
isPro: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
},
|
|
||||||
project_others: {
|
type TOtherFeatureList = {
|
||||||
key: "work_management",
|
[key in TProjectOtherFeatureKeys]: TProperties;
|
||||||
title: "Work management",
|
};
|
||||||
description: "Available only on some plans as indicated by the label next to the feature below.",
|
|
||||||
featureList: {
|
export const PROJECT_OTHER_FEATURES_LIST: TOtherFeatureList = {
|
||||||
is_time_tracking_enabled: {
|
is_time_tracking_enabled: {
|
||||||
key: "time_tracking",
|
key: "time_tracking",
|
||||||
property: "is_time_tracking_enabled",
|
property: "is_time_tracking_enabled",
|
||||||
|
|
@ -93,6 +84,34 @@ export const PROJECT_FEATURES_LIST: TProjectFeatures = {
|
||||||
isPro: true,
|
isPro: true,
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type TProjectFeatures = {
|
||||||
|
project_features: {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
featureList: TBaseFeatureList;
|
||||||
|
};
|
||||||
|
project_others: {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
featureList: TOtherFeatureList;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PROJECT_FEATURES_LIST: TProjectFeatures = {
|
||||||
|
project_features: {
|
||||||
|
key: "projects_and_issues",
|
||||||
|
title: "Projects and work items",
|
||||||
|
description: "Toggle these on or off this project.",
|
||||||
|
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: PROJECT_OTHER_FEATURES_LIST,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,6 @@ export type TIssueModalContext = {
|
||||||
getActiveAdditionalPropertiesLength: (props: TActiveAdditionalPropertiesProps) => number;
|
getActiveAdditionalPropertiesLength: (props: TActiveAdditionalPropertiesProps) => number;
|
||||||
handlePropertyValuesValidation: (props: TPropertyValuesValidationProps) => boolean;
|
handlePropertyValuesValidation: (props: TPropertyValuesValidationProps) => boolean;
|
||||||
handleCreateUpdatePropertyValues: (props: TCreateUpdatePropertyValuesProps) => Promise<void>;
|
handleCreateUpdatePropertyValues: (props: TCreateUpdatePropertyValuesProps) => Promise<void>;
|
||||||
handleParentWorkItemDetails: (props: THandleParentWorkItemDetailsProps) => Promise<ISearchIssueResponse | undefined>;
|
|
||||||
handleProjectEntitiesFetch: (props: THandleProjectEntitiesFetchProps) => Promise<void>;
|
handleProjectEntitiesFetch: (props: THandleProjectEntitiesFetchProps) => Promise<void>;
|
||||||
handleTemplateChange: (props: THandleTemplateChangeProps) => Promise<void>;
|
handleTemplateChange: (props: THandleTemplateChangeProps) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { EIssuesStoreType } from "@plane/constants";
|
import { EIssuesStoreType } from "@plane/constants";
|
||||||
import type { TIssue } from "@plane/types";
|
import type { TIssue } from "@plane/types";
|
||||||
|
|
@ -30,11 +31,20 @@ export interface IssuesModalProps {
|
||||||
templateId?: string;
|
templateId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer(
|
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
|
||||||
(props) =>
|
// router params
|
||||||
props.isOpen && (
|
const { cycleId, moduleId } = useParams();
|
||||||
<IssueModalProvider templateId={props.templateId}>
|
// 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 (
|
||||||
|
<IssueModalProvider templateId={props.templateId} dataForPreload={dataForPreload}>
|
||||||
<CreateUpdateIssueModalBase {...props} />
|
<CreateUpdateIssueModalBase {...props} />
|
||||||
</IssueModalProvider>
|
</IssueModalProvider>
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import React, { forwardRef, useEffect } from "react";
|
import React, { forwardRef, useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { TwitterPicker } from "react-color";
|
import { TwitterPicker } from "react-color";
|
||||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
|
|
@ -11,13 +10,17 @@ import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { IIssueLabel } from "@plane/types";
|
import { IIssueLabel } from "@plane/types";
|
||||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// hooks
|
|
||||||
import { useLabel } from "@/hooks/store";
|
|
||||||
|
|
||||||
type Props = {
|
export type TLabelOperationsCallbacks = {
|
||||||
|
createLabel: (data: Partial<IIssueLabel>) => Promise<IIssueLabel>;
|
||||||
|
updateLabel: (labelId: string, data: Partial<IIssueLabel>) => Promise<IIssueLabel>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TCreateUpdateLabelInlineProps = {
|
||||||
labelForm: boolean;
|
labelForm: boolean;
|
||||||
setLabelForm: React.Dispatch<React.SetStateAction<boolean>>;
|
setLabelForm: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
isUpdating: boolean;
|
isUpdating: boolean;
|
||||||
|
labelOperationsCallbacks: TLabelOperationsCallbacks;
|
||||||
labelToUpdate?: IIssueLabel;
|
labelToUpdate?: IIssueLabel;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
};
|
};
|
||||||
|
|
@ -28,12 +31,8 @@ const defaultValues: Partial<IIssueLabel> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateUpdateLabelInline = observer(
|
export const CreateUpdateLabelInline = observer(
|
||||||
forwardRef<HTMLFormElement, Props>(function CreateUpdateLabelInline(props, ref) {
|
forwardRef<HTMLDivElement, TCreateUpdateLabelInlineProps>(function CreateUpdateLabelInline(props, ref) {
|
||||||
const { labelForm, setLabelForm, isUpdating, labelToUpdate, onClose } = props;
|
const { labelForm, setLabelForm, isUpdating, labelOperationsCallbacks, labelToUpdate, onClose } = props;
|
||||||
// router
|
|
||||||
const { workspaceSlug, projectId } = useParams();
|
|
||||||
// store hooks
|
|
||||||
const { createLabel, updateLabel } = useLabel();
|
|
||||||
// form info
|
// form info
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
|
@ -56,9 +55,10 @@ export const CreateUpdateLabelInline = observer(
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLabelCreate: SubmitHandler<IIssueLabel> = async (formData) => {
|
const handleLabelCreate: SubmitHandler<IIssueLabel> = async (formData) => {
|
||||||
if (!workspaceSlug || !projectId || isSubmitting) return;
|
if (isSubmitting) return;
|
||||||
|
|
||||||
await createLabel(workspaceSlug.toString(), projectId.toString(), formData)
|
await labelOperationsCallbacks
|
||||||
|
.createLabel(formData)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
handleClose();
|
handleClose();
|
||||||
reset(defaultValues);
|
reset(defaultValues);
|
||||||
|
|
@ -74,10 +74,10 @@ export const CreateUpdateLabelInline = observer(
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLabelUpdate: SubmitHandler<IIssueLabel> = async (formData) => {
|
const handleLabelUpdate: SubmitHandler<IIssueLabel> = 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 labelOperationsCallbacks
|
||||||
await updateLabel(workspaceSlug.toString(), projectId.toString(), labelToUpdate?.id!, formData)
|
.updateLabel(labelToUpdate.id, formData)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
reset(defaultValues);
|
reset(defaultValues);
|
||||||
handleClose();
|
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
|
* For settings focus on name input
|
||||||
*/
|
*/
|
||||||
|
|
@ -117,12 +125,8 @@ export const CreateUpdateLabelInline = observer(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSubmit(isUpdating ? handleLabelUpdate : handleLabelCreate)();
|
|
||||||
}}
|
|
||||||
className={`flex w-full scroll-m-8 items-center gap-2 bg-custom-background-100 ${labelForm ? "" : "hidden"}`}
|
className={`flex w-full scroll-m-8 items-center gap-2 bg-custom-background-100 ${labelForm ? "" : "hidden"}`}
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
|
|
@ -199,10 +203,18 @@ export const CreateUpdateLabelInline = observer(
|
||||||
<Button variant="neutral-primary" onClick={() => handleClose()} size="sm">
|
<Button variant="neutral-primary" onClick={() => handleClose()} size="sm">
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" type="submit" size="sm" loading={isSubmitting}>
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit(handleFormSubmit)();
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
{isUpdating ? (isSubmitting ? t("updating") : t("update")) : isSubmitting ? t("adding") : t("add")}
|
{isUpdating ? (isSubmitting ? t("updating") : t("update")) : isSubmitting ? t("adding") : t("add")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</div>
|
||||||
{errors.name?.message && <p className="p-0.5 pl-8 text-sm text-red-500">{errors.name?.message}</p>}
|
{errors.name?.message && <p className="p-0.5 pl-8 text-sm text-red-500">{errors.name?.message}</p>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ interface ILabelItemBlock {
|
||||||
isLabelGroup?: boolean;
|
isLabelGroup?: boolean;
|
||||||
dragHandleRef: MutableRefObject<HTMLButtonElement | null>;
|
dragHandleRef: MutableRefObject<HTMLButtonElement | null>;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
draggable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LabelItemBlock = (props: ILabelItemBlock) => {
|
export const LabelItemBlock = (props: ILabelItemBlock) => {
|
||||||
|
|
@ -40,9 +41,10 @@ export const LabelItemBlock = (props: ILabelItemBlock) => {
|
||||||
isLabelGroup,
|
isLabelGroup,
|
||||||
dragHandleRef,
|
dragHandleRef,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
draggable = true,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
const [isMenuActive, setIsMenuActive] = useState(true);
|
||||||
// refs
|
// refs
|
||||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
|
@ -51,7 +53,7 @@ export const LabelItemBlock = (props: ILabelItemBlock) => {
|
||||||
return (
|
return (
|
||||||
<div className="group flex items-center">
|
<div className="group flex items-center">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{!disabled && (
|
{!disabled && draggable && (
|
||||||
<DragHandle
|
<DragHandle
|
||||||
className={cn("opacity-0 group-hover:opacity-100", {
|
className={cn("opacity-0 group-hover:opacity-100", {
|
||||||
"opacity-100": isDragging,
|
"opacity-100": isDragging,
|
||||||
|
|
@ -65,7 +67,7 @@ export const LabelItemBlock = (props: ILabelItemBlock) => {
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
<div
|
<div
|
||||||
ref={actionSectionRef}
|
ref={actionSectionRef}
|
||||||
className={`absolute right-2.5 flex items-start gap-3.5 px-4 ${
|
className={`absolute right-2.5 flex items-center gap-2 px-4 ${
|
||||||
isMenuActive || isLabelGroup
|
isMenuActive || isLabelGroup
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0 group-hover:pointer-events-auto group-hover:opacity-100"
|
: "opacity-0 group-hover:pointer-events-auto group-hover:opacity-100"
|
||||||
|
|
@ -77,7 +79,7 @@ export const LabelItemBlock = (props: ILabelItemBlock) => {
|
||||||
isVisible && (
|
isVisible && (
|
||||||
<CustomMenu.MenuItem key={key} onClick={() => onClick(label)}>
|
<CustomMenu.MenuItem key={key} onClick={() => onClick(label)}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<CustomIcon className="h-4 w-4" />
|
<CustomIcon className="size-4" />
|
||||||
<span>{text}</span>
|
<span>{text}</span>
|
||||||
</span>
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
|
|
@ -87,10 +89,10 @@ export const LabelItemBlock = (props: ILabelItemBlock) => {
|
||||||
{!isLabelGroup && (
|
{!isLabelGroup && (
|
||||||
<div className="py-0.5">
|
<div className="py-0.5">
|
||||||
<button
|
<button
|
||||||
className="flex h-4 w-4 items-center justify-start gap-2"
|
className="flex size-5 items-center justify-center rounded hover:bg-custom-background-80"
|
||||||
onClick={() => handleLabelDelete(label)}
|
onClick={() => handleLabelDelete(label)}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4 flex-shrink-0 text-custom-sidebar-text-400" />
|
<X className="size-3.5 flex-shrink-0 text-custom-sidebar-text-300" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Disclosure, Transition } from "@headlessui/react";
|
||||||
// types
|
// types
|
||||||
import { IIssueLabel } from "@plane/types";
|
import { IIssueLabel } from "@plane/types";
|
||||||
// components
|
// 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 { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block";
|
||||||
import { LabelDndHOC } from "./label-drag-n-drop-HOC";
|
import { LabelDndHOC } from "./label-drag-n-drop-HOC";
|
||||||
import { ProjectSettingLabelItem } from "./project-setting-label-item";
|
import { ProjectSettingLabelItem } from "./project-setting-label-item";
|
||||||
|
|
@ -25,6 +25,7 @@ type Props = {
|
||||||
droppedLabelId: string | undefined,
|
droppedLabelId: string | undefined,
|
||||||
dropAtEndOfList: boolean
|
dropAtEndOfList: boolean
|
||||||
) => void;
|
) => void;
|
||||||
|
labelOperationsCallbacks: TLabelOperationsCallbacks;
|
||||||
isEditable?: boolean;
|
isEditable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -38,6 +39,7 @@ export const ProjectSettingLabelGroup: React.FC<Props> = observer((props) => {
|
||||||
isLastChild,
|
isLastChild,
|
||||||
onDrop,
|
onDrop,
|
||||||
isEditable = false,
|
isEditable = false,
|
||||||
|
labelOperationsCallbacks,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
// states
|
// states
|
||||||
|
|
@ -87,6 +89,7 @@ export const ProjectSettingLabelGroup: React.FC<Props> = observer((props) => {
|
||||||
setLabelForm={setEditLabelForm}
|
setLabelForm={setEditLabelForm}
|
||||||
isUpdating
|
isUpdating
|
||||||
labelToUpdate={label}
|
labelToUpdate={label}
|
||||||
|
labelOperationsCallbacks={labelOperationsCallbacks}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setEditLabelForm(false);
|
setEditLabelForm(false);
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
|
|
@ -134,6 +137,7 @@ export const ProjectSettingLabelGroup: React.FC<Props> = observer((props) => {
|
||||||
isLastChild={index === labelChildren.length - 1}
|
isLastChild={index === labelChildren.length - 1}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
isEditable={isEditable}
|
isEditable={isEditable}
|
||||||
|
labelOperationsCallbacks={labelOperationsCallbacks}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { IIssueLabel } from "@plane/types";
|
||||||
// hooks
|
// hooks
|
||||||
import { useLabel } from "@/hooks/store";
|
import { useLabel } from "@/hooks/store";
|
||||||
// components
|
// 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 { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block";
|
||||||
import { LabelDndHOC } from "./label-drag-n-drop-HOC";
|
import { LabelDndHOC } from "./label-drag-n-drop-HOC";
|
||||||
|
|
||||||
|
|
@ -23,6 +23,7 @@ type Props = {
|
||||||
droppedLabelId: string | undefined,
|
droppedLabelId: string | undefined,
|
||||||
dropAtEndOfList: boolean
|
dropAtEndOfList: boolean
|
||||||
) => void;
|
) => void;
|
||||||
|
labelOperationsCallbacks: TLabelOperationsCallbacks;
|
||||||
isEditable?: boolean;
|
isEditable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -35,6 +36,7 @@ export const ProjectSettingLabelItem: React.FC<Props> = (props) => {
|
||||||
isLastChild,
|
isLastChild,
|
||||||
isParentDragging = false,
|
isParentDragging = false,
|
||||||
onDrop,
|
onDrop,
|
||||||
|
labelOperationsCallbacks,
|
||||||
isEditable = false,
|
isEditable = false,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
|
|
@ -89,6 +91,7 @@ export const ProjectSettingLabelItem: React.FC<Props> = (props) => {
|
||||||
setLabelForm={setEditLabelForm}
|
setLabelForm={setEditLabelForm}
|
||||||
isUpdating
|
isUpdating
|
||||||
labelToUpdate={label}
|
labelToUpdate={label}
|
||||||
|
labelOperationsCallbacks={labelOperationsCallbacks}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setEditLabelForm(false);
|
setEditLabelForm(false);
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
DeleteLabelModal,
|
DeleteLabelModal,
|
||||||
ProjectSettingLabelGroup,
|
ProjectSettingLabelGroup,
|
||||||
ProjectSettingLabelItem,
|
ProjectSettingLabelItem,
|
||||||
|
TLabelOperationsCallbacks,
|
||||||
} from "@/components/labels";
|
} from "@/components/labels";
|
||||||
// hooks
|
// hooks
|
||||||
import { useLabel, useUserPermissions } from "@/hooks/store";
|
import { useLabel, useUserPermissions } from "@/hooks/store";
|
||||||
|
|
@ -24,7 +25,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||||
// router
|
// router
|
||||||
const { workspaceSlug, projectId } = useParams();
|
const { workspaceSlug, projectId } = useParams();
|
||||||
// refs
|
// refs
|
||||||
const scrollToRef = useRef<HTMLFormElement>(null);
|
const scrollToRef = useRef<HTMLDivElement>(null);
|
||||||
// states
|
// states
|
||||||
const [showLabelForm, setLabelForm] = useState(false);
|
const [showLabelForm, setLabelForm] = useState(false);
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|
@ -32,11 +33,16 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||||
// plane hooks
|
// plane hooks
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel();
|
const { projectLabels, updateLabelPosition, projectLabelsTree, createLabel, updateLabel } = useLabel();
|
||||||
const { allowPermissions } = useUserPermissions();
|
const { allowPermissions } = useUserPermissions();
|
||||||
// derived values
|
// derived values
|
||||||
const isEditable = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
|
const isEditable = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/project-settings/labels" });
|
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/project-settings/labels" });
|
||||||
|
const labelOperationsCallbacks: TLabelOperationsCallbacks = {
|
||||||
|
createLabel: (data: Partial<IIssueLabel>) => createLabel(workspaceSlug?.toString(), projectId?.toString(), data),
|
||||||
|
updateLabel: (labelId: string, data: Partial<IIssueLabel>) =>
|
||||||
|
updateLabel(workspaceSlug?.toString(), projectId?.toString(), labelId, data),
|
||||||
|
};
|
||||||
|
|
||||||
const newLabel = () => {
|
const newLabel = () => {
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
|
|
@ -84,6 +90,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||||
labelForm={showLabelForm}
|
labelForm={showLabelForm}
|
||||||
setLabelForm={setLabelForm}
|
setLabelForm={setLabelForm}
|
||||||
isUpdating={isUpdating}
|
isUpdating={isUpdating}
|
||||||
|
labelOperationsCallbacks={labelOperationsCallbacks}
|
||||||
ref={scrollToRef}
|
ref={scrollToRef}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setLabelForm(false);
|
setLabelForm(false);
|
||||||
|
|
@ -117,6 +124,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||||
isLastChild={index === projectLabelsTree.length - 1}
|
isLastChild={index === projectLabelsTree.length - 1}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
isEditable={isEditable}
|
isEditable={isEditable}
|
||||||
|
labelOperationsCallbacks={labelOperationsCallbacks}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -130,6 +138,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||||
isLastChild={index === projectLabelsTree.length - 1}
|
isLastChild={index === projectLabelsTree.length - 1}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
isEditable={isEditable}
|
isEditable={isEditable}
|
||||||
|
labelOperationsCallbacks={labelOperationsCallbacks}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -2,41 +2,48 @@
|
||||||
|
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { STATE_CREATED, STATE_GROUPS } from "@plane/constants";
|
import { EventProps, STATE_CREATED, STATE_GROUPS } from "@plane/constants";
|
||||||
import { IState, TStateGroups } from "@plane/types";
|
import { IState, TStateGroups, TStateOperationsCallbacks } from "@plane/types";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { StateForm } from "@/components/project-states";
|
import { StateForm } from "@/components/project-states";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useProjectState } from "@/hooks/store";
|
import { useEventTracker } from "@/hooks/store";
|
||||||
|
|
||||||
type TStateCreate = {
|
type TStateCreate = {
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
groupKey: TStateGroups;
|
groupKey: TStateGroups;
|
||||||
|
shouldTrackEvents: boolean;
|
||||||
|
createStateCallback: TStateOperationsCallbacks["createState"];
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StateCreate: FC<TStateCreate> = observer((props) => {
|
export const StateCreate: FC<TStateCreate> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, groupKey, handleClose } = props;
|
const { groupKey, shouldTrackEvents, createStateCallback, handleClose } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { captureProjectStateEvent, setTrackElement } = useEventTracker();
|
const { captureProjectStateEvent, setTrackElement } = useEventTracker();
|
||||||
const { createState } = useProjectState();
|
|
||||||
// states
|
// states
|
||||||
const [loader, setLoader] = useState(false);
|
const [loader, setLoader] = useState(false);
|
||||||
|
|
||||||
|
const captureEventIfEnabled = (props: EventProps) => {
|
||||||
|
if (shouldTrackEvents) {
|
||||||
|
captureProjectStateEvent(props);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
setLoader(false);
|
setLoader(false);
|
||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (formData: Partial<IState>) => {
|
const onSubmit = async (formData: Partial<IState>) => {
|
||||||
if (!workspaceSlug || !projectId || !groupKey) return { status: "error" };
|
if (!groupKey) return { status: "error" };
|
||||||
|
|
||||||
|
if (shouldTrackEvents) {
|
||||||
setTrackElement("PROJECT_SETTINGS_STATE_PAGE");
|
setTrackElement("PROJECT_SETTINGS_STATE_PAGE");
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const stateResponse = await createState(workspaceSlug, projectId, { ...formData, group: groupKey });
|
const stateResponse = await createStateCallback({ ...formData, group: groupKey });
|
||||||
captureProjectStateEvent({
|
captureEventIfEnabled({
|
||||||
eventName: STATE_CREATED,
|
eventName: STATE_CREATED,
|
||||||
payload: {
|
payload: {
|
||||||
...stateResponse,
|
...stateResponse,
|
||||||
|
|
@ -53,7 +60,7 @@ export const StateCreate: FC<TStateCreate> = observer((props) => {
|
||||||
return { status: "success" };
|
return { status: "success" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorStatus = error as unknown as { status: number; data: { error: string } };
|
const errorStatus = error as unknown as { status: number; data: { error: string } };
|
||||||
captureProjectStateEvent({
|
captureEventIfEnabled({
|
||||||
eventName: STATE_CREATED,
|
eventName: STATE_CREATED,
|
||||||
payload: {
|
payload: {
|
||||||
...formData,
|
...formData,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FormEvent, FC, useEffect, useState, useMemo } from "react";
|
import { FC, useEffect, useState, useMemo } from "react";
|
||||||
import { TwitterPicker } from "react-color";
|
import { TwitterPicker } from "react-color";
|
||||||
import { IState } from "@plane/types";
|
import { IState } from "@plane/types";
|
||||||
import { Button, Popover, Input, TextArea } from "@plane/ui";
|
import { Button, Popover, Input, TextArea } from "@plane/ui";
|
||||||
|
|
@ -28,7 +28,7 @@ export const StateForm: FC<TStateForm> = (props) => {
|
||||||
setErrors((prev) => ({ ...prev, [key]: "" }));
|
setErrors((prev) => ({ ...prev, [key]: "" }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const formSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
const formSubmit = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const name = formData?.name || undefined;
|
const name = formData?.name || undefined;
|
||||||
|
|
@ -59,7 +59,7 @@ export const StateForm: FC<TStateForm> = (props) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={formSubmit} className="relative flex space-x-2 bg-custom-background-100 p-3 rounded">
|
<div className="relative flex space-x-2 bg-custom-background-100 p-3 rounded">
|
||||||
{/* color */}
|
{/* color */}
|
||||||
<div className="flex-shrink-0 h-full mt-2">
|
<div className="flex-shrink-0 h-full mt-2">
|
||||||
<Popover button={PopoverButton} panelClassName="mt-4 -ml-3">
|
<Popover button={PopoverButton} panelClassName="mt-4 -ml-3">
|
||||||
|
|
@ -94,7 +94,7 @@ export const StateForm: FC<TStateForm> = (props) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex space-x-2 items-center">
|
<div className="flex space-x-2 items-center">
|
||||||
<Button type="submit" variant="primary" size="sm" disabled={buttonDisabled}>
|
<Button onClick={formSubmit} variant="primary" size="sm" disabled={buttonDisabled}>
|
||||||
{buttonTitle}
|
{buttonTitle}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" variant="neutral-primary" size="sm" disabled={buttonDisabled} onClick={onCancel}>
|
<Button type="button" variant="neutral-primary" size="sm" disabled={buttonDisabled} onClick={onCancel}>
|
||||||
|
|
@ -102,6 +102,6 @@ export const StateForm: FC<TStateForm> = (props) => {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,25 @@
|
||||||
|
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { STATE_UPDATED } from "@plane/constants";
|
import { EventProps, STATE_UPDATED } from "@plane/constants";
|
||||||
import { IState } from "@plane/types";
|
import { IState, TStateOperationsCallbacks } from "@plane/types";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { StateForm } from "@/components/project-states";
|
import { StateForm } from "@/components/project-states";
|
||||||
// constants
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useProjectState } from "@/hooks/store";
|
import { useEventTracker } from "@/hooks/store";
|
||||||
|
|
||||||
type TStateUpdate = {
|
type TStateUpdate = {
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
state: IState;
|
state: IState;
|
||||||
|
updateStateCallback: TStateOperationsCallbacks["updateState"];
|
||||||
|
shouldTrackEvents: boolean;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StateUpdate: FC<TStateUpdate> = observer((props) => {
|
export const StateUpdate: FC<TStateUpdate> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, state, handleClose } = props;
|
const { state, updateStateCallback, shouldTrackEvents, handleClose } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { captureProjectStateEvent, setTrackElement } = useEventTracker();
|
const { captureProjectStateEvent, setTrackElement } = useEventTracker();
|
||||||
const { updateState } = useProjectState();
|
|
||||||
// states
|
// states
|
||||||
const [loader, setLoader] = useState(false);
|
const [loader, setLoader] = useState(false);
|
||||||
|
|
||||||
|
|
@ -31,13 +29,21 @@ export const StateUpdate: FC<TStateUpdate> = observer((props) => {
|
||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (formData: Partial<IState>) => {
|
const captureEventIfEnabled = (props: EventProps) => {
|
||||||
if (!workspaceSlug || !projectId || !state.id) return { status: "error" };
|
if (shouldTrackEvents) {
|
||||||
|
captureProjectStateEvent(props);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (formData: Partial<IState>) => {
|
||||||
|
if (!state.id) return { status: "error" };
|
||||||
|
|
||||||
|
if (shouldTrackEvents) {
|
||||||
setTrackElement("PROJECT_SETTINGS_STATE_PAGE");
|
setTrackElement("PROJECT_SETTINGS_STATE_PAGE");
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const stateResponse = await updateState(workspaceSlug, projectId, state.id, formData);
|
const stateResponse = await updateStateCallback(state.id, formData);
|
||||||
captureProjectStateEvent({
|
captureEventIfEnabled({
|
||||||
eventName: STATE_UPDATED,
|
eventName: STATE_UPDATED,
|
||||||
payload: {
|
payload: {
|
||||||
...stateResponse,
|
...stateResponse,
|
||||||
|
|
@ -67,7 +73,7 @@ export const StateUpdate: FC<TStateUpdate> = observer((props) => {
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "State could not be updated. Please try again.",
|
message: "State could not be updated. Please try again.",
|
||||||
});
|
});
|
||||||
captureProjectStateEvent({
|
captureEventIfEnabled({
|
||||||
eventName: STATE_UPDATED,
|
eventName: STATE_UPDATED,
|
||||||
payload: {
|
payload: {
|
||||||
...formData,
|
...formData,
|
||||||
|
|
|
||||||
|
|
@ -4,35 +4,38 @@ import { FC, useState, useRef } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { ChevronDown, Plus } from "lucide-react";
|
import { ChevronDown, Plus } from "lucide-react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { IState, TStateGroups } from "@plane/types";
|
import { IState, TStateGroups, TStateOperationsCallbacks } from "@plane/types";
|
||||||
import { StateGroupIcon } from "@plane/ui";
|
import { StateGroupIcon } from "@plane/ui";
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
import { StateList, StateCreate } from "@/components/project-states";
|
import { StateList, StateCreate } from "@/components/project-states";
|
||||||
// hooks
|
|
||||||
import { useUserPermissions } from "@/hooks/store";
|
|
||||||
|
|
||||||
type TGroupItem = {
|
type TGroupItem = {
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
groupKey: TStateGroups;
|
groupKey: TStateGroups;
|
||||||
groupsExpanded: Partial<TStateGroups>[];
|
groupsExpanded: Partial<TStateGroups>[];
|
||||||
handleGroupCollapse: (groupKey: TStateGroups) => void;
|
|
||||||
handleExpand: (groupKey: TStateGroups) => void;
|
|
||||||
groupedStates: Record<string, IState[]>;
|
groupedStates: Record<string, IState[]>;
|
||||||
states: IState[];
|
states: IState[];
|
||||||
|
stateOperationsCallbacks: TStateOperationsCallbacks;
|
||||||
|
isEditable: boolean;
|
||||||
|
shouldTrackEvents: boolean;
|
||||||
|
groupItemClassName?: string;
|
||||||
|
stateItemClassName?: string;
|
||||||
|
handleGroupCollapse: (groupKey: TStateGroups) => void;
|
||||||
|
handleExpand: (groupKey: TStateGroups) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GroupItem: FC<TGroupItem> = observer((props) => {
|
export const GroupItem: FC<TGroupItem> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
workspaceSlug,
|
|
||||||
projectId,
|
|
||||||
groupKey,
|
groupKey,
|
||||||
groupedStates,
|
groupedStates,
|
||||||
states,
|
states,
|
||||||
groupsExpanded,
|
groupsExpanded,
|
||||||
|
isEditable,
|
||||||
|
stateOperationsCallbacks,
|
||||||
|
shouldTrackEvents,
|
||||||
|
groupItemClassName,
|
||||||
|
stateItemClassName,
|
||||||
handleExpand,
|
handleExpand,
|
||||||
handleGroupCollapse,
|
handleGroupCollapse,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
@ -40,18 +43,18 @@ export const GroupItem: FC<TGroupItem> = observer((props) => {
|
||||||
const dropElementRef = useRef<HTMLDivElement | null>(null);
|
const dropElementRef = useRef<HTMLDivElement | null>(null);
|
||||||
// plane hooks
|
// plane hooks
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// store hooks
|
|
||||||
const { allowPermissions } = useUserPermissions();
|
|
||||||
// state
|
// state
|
||||||
const [createState, setCreateState] = useState(false);
|
const [createState, setCreateState] = useState(false);
|
||||||
// derived values
|
// derived values
|
||||||
const currentStateExpanded = groupsExpanded.includes(groupKey);
|
const currentStateExpanded = groupsExpanded.includes(groupKey);
|
||||||
const isEditable = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
|
|
||||||
const shouldShowEmptyState = states.length === 0 && currentStateExpanded && !createState;
|
const shouldShowEmptyState = states.length === 0 && currentStateExpanded && !createState;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="space-y-1 border border-custom-border-200 rounded bg-custom-background-90 transition-all p-2"
|
className={cn(
|
||||||
|
"space-y-1 border border-custom-border-200 rounded bg-custom-background-90 transition-all p-2",
|
||||||
|
groupItemClassName
|
||||||
|
)}
|
||||||
ref={dropElementRef}
|
ref={dropElementRef}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center gap-2">
|
<div className="flex justify-between items-center gap-2">
|
||||||
|
|
@ -76,12 +79,18 @@ export const GroupItem: FC<TGroupItem> = observer((props) => {
|
||||||
<div className="text-base font-medium text-custom-text-200 capitalize px-1">{groupKey}</div>
|
<div className="text-base font-medium text-custom-text-200 capitalize px-1">{groupKey}</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-shrink-0 w-6 h-6 rounded flex justify-center items-center overflow-hidden transition-colors hover:bg-custom-background-80 cursor-pointer text-custom-primary-100/80 hover:text-custom-primary-100",
|
"flex-shrink-0 w-6 h-6 rounded flex justify-center items-center overflow-hidden transition-colors hover:bg-custom-background-80 cursor-pointer text-custom-primary-100/80 hover:text-custom-primary-100",
|
||||||
!isEditable && "cursor-not-allowed text-custom-text-400 hover:text-custom-text-400"
|
(!isEditable || createState) && "cursor-not-allowed text-custom-text-400 hover:text-custom-text-400"
|
||||||
)}
|
)}
|
||||||
onClick={() => !createState && setCreateState(true)}
|
onClick={() => {
|
||||||
disabled={!isEditable}
|
if (!createState) {
|
||||||
|
handleExpand(groupKey);
|
||||||
|
setCreateState(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!isEditable || createState}
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -97,12 +106,13 @@ export const GroupItem: FC<TGroupItem> = observer((props) => {
|
||||||
{currentStateExpanded && (
|
{currentStateExpanded && (
|
||||||
<div id="group-droppable-container">
|
<div id="group-droppable-container">
|
||||||
<StateList
|
<StateList
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
projectId={projectId}
|
|
||||||
groupKey={groupKey}
|
groupKey={groupKey}
|
||||||
groupedStates={groupedStates}
|
groupedStates={groupedStates}
|
||||||
states={states}
|
states={states}
|
||||||
disabled={!isEditable}
|
disabled={!isEditable}
|
||||||
|
stateOperationsCallbacks={stateOperationsCallbacks}
|
||||||
|
shouldTrackEvents={shouldTrackEvents}
|
||||||
|
stateItemClassName={stateItemClassName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -110,10 +120,10 @@ export const GroupItem: FC<TGroupItem> = observer((props) => {
|
||||||
{isEditable && createState && (
|
{isEditable && createState && (
|
||||||
<div className="">
|
<div className="">
|
||||||
<StateCreate
|
<StateCreate
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
projectId={projectId}
|
|
||||||
groupKey={groupKey}
|
groupKey={groupKey}
|
||||||
handleClose={() => setCreateState(false)}
|
handleClose={() => setCreateState(false)}
|
||||||
|
createStateCallback={stateOperationsCallbacks.createState}
|
||||||
|
shouldTrackEvents={shouldTrackEvents}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,32 @@
|
||||||
|
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { observer } from "mobx-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
|
// components
|
||||||
import { GroupItem } from "@/components/project-states";
|
import { GroupItem } from "@/components/project-states";
|
||||||
|
|
||||||
type TGroupList = {
|
type TGroupList = {
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
groupedStates: Record<string, IState[]>;
|
groupedStates: Record<string, IState[]>;
|
||||||
|
stateOperationsCallbacks: TStateOperationsCallbacks;
|
||||||
|
isEditable: boolean;
|
||||||
|
shouldTrackEvents: boolean;
|
||||||
|
groupListClassName?: string;
|
||||||
|
groupItemClassName?: string;
|
||||||
|
stateItemClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GroupList: FC<TGroupList> = observer((props) => {
|
export const GroupList: FC<TGroupList> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, groupedStates } = props;
|
const {
|
||||||
|
groupedStates,
|
||||||
|
stateOperationsCallbacks,
|
||||||
|
isEditable,
|
||||||
|
shouldTrackEvents,
|
||||||
|
groupListClassName,
|
||||||
|
groupItemClassName,
|
||||||
|
stateItemClassName,
|
||||||
|
} = props;
|
||||||
// states
|
// states
|
||||||
const [groupsExpanded, setGroupsExpanded] = useState<Partial<TStateGroups>[]>([
|
const [groupsExpanded, setGroupsExpanded] = useState<Partial<TStateGroups>[]>([
|
||||||
"backlog",
|
"backlog",
|
||||||
|
|
@ -41,21 +55,24 @@ export const GroupList: FC<TGroupList> = observer((props) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className={cn("space-y-5", groupListClassName)}>
|
||||||
{Object.entries(groupedStates).map(([key, value]) => {
|
{Object.entries(groupedStates).map(([key, value]) => {
|
||||||
const groupKey = key as TStateGroups;
|
const groupKey = key as TStateGroups;
|
||||||
const groupStates = value;
|
const groupStates = value;
|
||||||
return (
|
return (
|
||||||
<GroupItem
|
<GroupItem
|
||||||
key={groupKey}
|
key={groupKey}
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
projectId={projectId}
|
|
||||||
groupKey={groupKey}
|
groupKey={groupKey}
|
||||||
states={groupStates}
|
states={groupStates}
|
||||||
groupedStates={groupedStates}
|
groupedStates={groupedStates}
|
||||||
groupsExpanded={groupsExpanded}
|
groupsExpanded={groupsExpanded}
|
||||||
|
stateOperationsCallbacks={stateOperationsCallbacks}
|
||||||
|
isEditable={isEditable}
|
||||||
|
shouldTrackEvents={shouldTrackEvents}
|
||||||
handleGroupCollapse={handleGroupCollapse}
|
handleGroupCollapse={handleGroupCollapse}
|
||||||
handleExpand={handleExpand}
|
handleExpand={handleExpand}
|
||||||
|
groupItemClassName={groupItemClassName}
|
||||||
|
stateItemClassName={stateItemClassName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -3,45 +3,49 @@
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Loader, X } from "lucide-react";
|
import { Loader, X } from "lucide-react";
|
||||||
import { STATE_DELETED } from "@plane/constants";
|
// plane imports
|
||||||
import { IState } from "@plane/types";
|
import { EventProps, STATE_DELETED } from "@plane/constants";
|
||||||
|
import { IState, TStateOperationsCallbacks } from "@plane/types";
|
||||||
import { AlertModalCore, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
import { AlertModalCore, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||||
// constants
|
import { cn } from "@plane/utils";
|
||||||
// helpers
|
|
||||||
import { cn } from "@/helpers/common.helper";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useProjectState } from "@/hooks/store";
|
import { useEventTracker } from "@/hooks/store";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
|
|
||||||
type TStateDelete = {
|
type TStateDelete = {
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
totalStates: number;
|
totalStates: number;
|
||||||
state: IState;
|
state: IState;
|
||||||
|
deleteStateCallback: TStateOperationsCallbacks["deleteState"];
|
||||||
|
shouldTrackEvents: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StateDelete: FC<TStateDelete> = observer((props) => {
|
export const StateDelete: FC<TStateDelete> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, totalStates, state } = props;
|
const { totalStates, state, deleteStateCallback, shouldTrackEvents } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
const { captureProjectStateEvent, setTrackElement } = useEventTracker();
|
const { captureProjectStateEvent, setTrackElement } = useEventTracker();
|
||||||
const { deleteState } = useProjectState();
|
|
||||||
// states
|
// states
|
||||||
const [isDeleteModal, setIsDeleteModal] = useState(false);
|
const [isDeleteModal, setIsDeleteModal] = useState(false);
|
||||||
const [isDelete, setIsDelete] = useState(false);
|
const [isDelete, setIsDelete] = useState(false);
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const isDeleteDisabled = state.default ? true : totalStates === 1 ? true : false;
|
const isDeleteDisabled = state.default ? true : totalStates === 1 ? true : false;
|
||||||
|
|
||||||
const handleDeleteState = async () => {
|
const captureEventIfEnabled = (props: EventProps) => {
|
||||||
if (!workspaceSlug || !projectId || isDeleteDisabled) return;
|
if (shouldTrackEvents) {
|
||||||
|
captureProjectStateEvent(props);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteState = async () => {
|
||||||
|
if (isDeleteDisabled) return;
|
||||||
|
if (shouldTrackEvents) {
|
||||||
setTrackElement("PROJECT_SETTINGS_STATE_PAGE");
|
setTrackElement("PROJECT_SETTINGS_STATE_PAGE");
|
||||||
|
}
|
||||||
setIsDelete(true);
|
setIsDelete(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteState(workspaceSlug, projectId, state.id);
|
await deleteStateCallback(state.id);
|
||||||
captureProjectStateEvent({
|
captureEventIfEnabled({
|
||||||
eventName: STATE_DELETED,
|
eventName: STATE_DELETED,
|
||||||
payload: {
|
payload: {
|
||||||
...state,
|
...state,
|
||||||
|
|
@ -51,7 +55,7 @@ export const StateDelete: FC<TStateDelete> = observer((props) => {
|
||||||
setIsDelete(false);
|
setIsDelete(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorStatus = error as unknown as { status: number; data: { error: string } };
|
const errorStatus = error as unknown as { status: number; data: { error: string } };
|
||||||
captureProjectStateEvent({
|
captureEventIfEnabled({
|
||||||
eventName: STATE_DELETED,
|
eventName: STATE_DELETED,
|
||||||
payload: {
|
payload: {
|
||||||
...state,
|
...state,
|
||||||
|
|
@ -94,6 +98,7 @@ export const StateDelete: FC<TStateDelete> = observer((props) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-shrink-0 w-5 h-5 rounded flex justify-center items-center overflow-hidden transition-colors cursor-pointer focus:outline-none",
|
"flex-shrink-0 w-5 h-5 rounded flex justify-center items-center overflow-hidden transition-colors cursor-pointer focus:outline-none",
|
||||||
isDeleteDisabled
|
isDeleteDisabled
|
||||||
|
|
|
||||||
|
|
@ -2,29 +2,30 @@
|
||||||
|
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// helpers
|
// plane imports
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { TStateOperationsCallbacks } from "@plane/types";
|
||||||
// hooks
|
import { cn } from "@plane/utils";
|
||||||
import { useProjectState } from "@/hooks/store";
|
|
||||||
|
|
||||||
type TStateMarksAsDefault = { workspaceSlug: string; projectId: string; stateId: string; isDefault: boolean };
|
type TStateMarksAsDefault = {
|
||||||
|
stateId: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
markStateAsDefaultCallback: TStateOperationsCallbacks["markStateAsDefault"];
|
||||||
|
};
|
||||||
|
|
||||||
export const StateMarksAsDefault: FC<TStateMarksAsDefault> = observer((props) => {
|
export const StateMarksAsDefault: FC<TStateMarksAsDefault> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, stateId, isDefault } = props;
|
const { stateId, isDefault, markStateAsDefaultCallback } = props;
|
||||||
// hooks
|
|
||||||
const { markStateAsDefault } = useProjectState();
|
|
||||||
// states
|
// states
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const handleMarkAsDefault = async () => {
|
const handleMarkAsDefault = async () => {
|
||||||
if (!workspaceSlug || !projectId || !stateId || isDefault) return;
|
if (!stateId || isDefault) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
await markStateAsDefault(workspaceSlug, projectId, stateId);
|
await markStateAsDefaultCallback(stateId);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} catch (error) {
|
} catch {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC } from "react";
|
import { FC, useMemo } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// components
|
// components
|
||||||
|
import { EUserProjectRoles, EUserPermissionsLevel } from "@plane/constants";
|
||||||
|
import { IState, TStateOperationsCallbacks } from "@plane/types";
|
||||||
import { ProjectStateLoader, GroupList } from "@/components/project-states";
|
import { ProjectStateLoader, GroupList } from "@/components/project-states";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProjectState } from "@/hooks/store";
|
import { useProjectState, useUserPermissions } from "@/hooks/store";
|
||||||
|
|
||||||
type TProjectState = {
|
type TProjectState = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
|
@ -16,20 +18,56 @@ type TProjectState = {
|
||||||
export const ProjectStateRoot: FC<TProjectState> = observer((props) => {
|
export const ProjectStateRoot: FC<TProjectState> = observer((props) => {
|
||||||
const { workspaceSlug, projectId } = props;
|
const { workspaceSlug, projectId } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { groupedProjectStates, fetchProjectStates } = useProjectState();
|
const {
|
||||||
|
groupedProjectStates,
|
||||||
|
fetchProjectStates,
|
||||||
|
createState,
|
||||||
|
moveStatePosition,
|
||||||
|
updateState,
|
||||||
|
deleteState,
|
||||||
|
markStateAsDefault,
|
||||||
|
} = useProjectState();
|
||||||
|
const { allowPermissions } = useUserPermissions();
|
||||||
|
// derived values
|
||||||
|
const isEditable = allowPermissions(
|
||||||
|
[EUserProjectRoles.ADMIN],
|
||||||
|
EUserPermissionsLevel.PROJECT,
|
||||||
|
workspaceSlug,
|
||||||
|
projectId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetching all project states
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null,
|
workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null,
|
||||||
workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId.toString()) : null,
|
workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId.toString()) : null,
|
||||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// State operations callbacks
|
||||||
|
const stateOperationsCallbacks: TStateOperationsCallbacks = useMemo(
|
||||||
|
() => ({
|
||||||
|
createState: async (data: Partial<IState>) => createState(workspaceSlug, projectId, data),
|
||||||
|
updateState: async (stateId: string, data: Partial<IState>) =>
|
||||||
|
updateState(workspaceSlug, projectId, stateId, data),
|
||||||
|
deleteState: async (stateId: string) => deleteState(workspaceSlug, projectId, stateId),
|
||||||
|
moveStatePosition: async (stateId: string, data: Partial<IState>) =>
|
||||||
|
moveStatePosition(workspaceSlug, projectId, stateId, data),
|
||||||
|
markStateAsDefault: async (stateId: string) => markStateAsDefault(workspaceSlug, projectId, stateId),
|
||||||
|
}),
|
||||||
|
[workspaceSlug, projectId, createState, moveStatePosition, updateState, deleteState, markStateAsDefault]
|
||||||
|
);
|
||||||
|
|
||||||
// Loader
|
// Loader
|
||||||
if (!groupedProjectStates) return <ProjectStateLoader />;
|
if (!groupedProjectStates) return <ProjectStateLoader />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-3">
|
<div className="py-3">
|
||||||
<GroupList workspaceSlug={workspaceSlug} projectId={projectId} groupedStates={groupedProjectStates} />
|
<GroupList
|
||||||
|
groupedStates={groupedProjectStates}
|
||||||
|
stateOperationsCallbacks={stateOperationsCallbacks}
|
||||||
|
isEditable={isEditable}
|
||||||
|
shouldTrackEvents
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,31 +2,33 @@ import { SetStateAction } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { GripVertical, Pencil } from "lucide-react";
|
import { GripVertical, Pencil } from "lucide-react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { IState } from "@plane/types";
|
import { IState, TStateOperationsCallbacks } from "@plane/types";
|
||||||
import { StateGroupIcon } from "@plane/ui";
|
import { StateGroupIcon } from "@plane/ui";
|
||||||
// local imports
|
// local imports
|
||||||
import { StateDelete, StateMarksAsDefault } from "./options";
|
import { StateDelete, StateMarksAsDefault } from "./options";
|
||||||
|
|
||||||
export type StateItemTitleProps = {
|
type TBaseStateItemTitleProps = {
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
setUpdateStateModal: (value: SetStateAction<boolean>) => void;
|
|
||||||
stateCount: number;
|
stateCount: number;
|
||||||
disabled: boolean;
|
|
||||||
state: IState;
|
state: IState;
|
||||||
shouldShowDescription?: boolean;
|
shouldShowDescription?: boolean;
|
||||||
|
setUpdateStateModal: (value: SetStateAction<boolean>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StateItemTitle = observer((props: StateItemTitleProps) => {
|
type TEnabledStateItemTitleProps = TBaseStateItemTitleProps & {
|
||||||
const {
|
disabled: false;
|
||||||
workspaceSlug,
|
stateOperationsCallbacks: Pick<TStateOperationsCallbacks, "markStateAsDefault" | "deleteState">;
|
||||||
projectId,
|
shouldTrackEvents: boolean;
|
||||||
stateCount,
|
};
|
||||||
setUpdateStateModal,
|
|
||||||
disabled,
|
type TDisabledStateItemTitleProps = TBaseStateItemTitleProps & {
|
||||||
state,
|
disabled: true;
|
||||||
shouldShowDescription = true,
|
};
|
||||||
} = props;
|
|
||||||
|
export type TStateItemTitleProps = TEnabledStateItemTitleProps | TDisabledStateItemTitleProps;
|
||||||
|
|
||||||
|
export const StateItemTitle = observer((props: TStateItemTitleProps) => {
|
||||||
|
const { stateCount, setUpdateStateModal, disabled, state, shouldShowDescription = true } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 w-full justify-between">
|
<div className="flex items-center gap-2 w-full justify-between">
|
||||||
<div className="flex items-center gap-1 px-1">
|
<div className="flex items-center gap-1 px-1">
|
||||||
|
|
@ -46,19 +48,16 @@ export const StateItemTitle = observer((props: StateItemTitleProps) => {
|
||||||
{shouldShowDescription && <p className="text-xs text-custom-text-200">{state.description}</p>}
|
{shouldShowDescription && <p className="text-xs text-custom-text-200">{state.description}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
<div className="hidden group-hover:flex items-center gap-2">
|
<div className="hidden group-hover:flex items-center gap-2">
|
||||||
{/* state mark as default option */}
|
{/* state mark as default option */}
|
||||||
<div className="flex-shrink-0 text-xs transition-all">
|
<div className="flex-shrink-0 text-xs transition-all">
|
||||||
<StateMarksAsDefault
|
<StateMarksAsDefault
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
projectId={projectId}
|
|
||||||
stateId={state.id}
|
stateId={state.id}
|
||||||
isDefault={state.default ? true : false}
|
isDefault={state.default ? true : false}
|
||||||
|
markStateAsDefaultCallback={props.stateOperationsCallbacks.markStateAsDefault}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* state edit options */}
|
{/* state edit options */}
|
||||||
<div className="flex items-center gap-1 transition-all">
|
<div className="flex items-center gap-1 transition-all">
|
||||||
<button
|
<button
|
||||||
|
|
@ -67,7 +66,12 @@ export const StateItemTitle = observer((props: StateItemTitleProps) => {
|
||||||
>
|
>
|
||||||
<Pencil className="w-3 h-3" />
|
<Pencil className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
<StateDelete workspaceSlug={workspaceSlug} projectId={projectId} totalStates={stateCount} state={state} />
|
<StateDelete
|
||||||
|
totalStates={stateCount}
|
||||||
|
state={state}
|
||||||
|
deleteStateCallback={props.stateOperationsCallbacks.deleteState}
|
||||||
|
shouldTrackEvents={props.shouldTrackEvents}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -7,55 +7,63 @@ import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// Plane
|
// Plane
|
||||||
import { TDraggableData } from "@plane/constants";
|
import { TDraggableData } from "@plane/constants";
|
||||||
import { IState, TStateGroups } from "@plane/types";
|
import { IState, TStateGroups, TStateOperationsCallbacks } from "@plane/types";
|
||||||
import { DropIndicator } from "@plane/ui";
|
import { DropIndicator } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { StateItemTitle, StateUpdate } from "@/components/project-states";
|
import { StateItemTitle, StateUpdate } from "@/components/project-states";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { getCurrentStateSequence } from "@/helpers/state.helper";
|
import { getCurrentStateSequence } from "@/helpers/state.helper";
|
||||||
// hooks
|
|
||||||
import { useProjectState } from "@/hooks/store";
|
|
||||||
|
|
||||||
type TStateItem = {
|
type TStateItem = {
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
groupKey: TStateGroups;
|
groupKey: TStateGroups;
|
||||||
groupedStates: Record<string, IState[]>;
|
groupedStates: Record<string, IState[]>;
|
||||||
totalStates: number;
|
totalStates: number;
|
||||||
state: IState;
|
state: IState;
|
||||||
|
stateOperationsCallbacks: TStateOperationsCallbacks;
|
||||||
|
shouldTrackEvents: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
stateItemClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StateItem: FC<TStateItem> = observer((props) => {
|
export const StateItem: FC<TStateItem> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, groupKey, groupedStates, totalStates, state, disabled = false } = props;
|
const {
|
||||||
// hooks
|
groupKey,
|
||||||
const { moveStatePosition } = useProjectState();
|
groupedStates,
|
||||||
|
totalStates,
|
||||||
|
state,
|
||||||
|
stateOperationsCallbacks,
|
||||||
|
shouldTrackEvents,
|
||||||
|
disabled = false,
|
||||||
|
stateItemClassName,
|
||||||
|
} = props;
|
||||||
|
// ref
|
||||||
|
const draggableElementRef = useRef<HTMLDivElement | null>(null);
|
||||||
// states
|
// states
|
||||||
const [updateStateModal, setUpdateStateModal] = useState(false);
|
const [updateStateModal, setUpdateStateModal] = useState(false);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [isDraggedOver, setIsDraggedOver] = useState(false);
|
||||||
|
const [closestEdge, setClosestEdge] = useState<string | null>(null);
|
||||||
|
// derived values
|
||||||
|
const isDraggable = totalStates === 1 ? false : true;
|
||||||
|
const commonStateItemListProps = {
|
||||||
|
stateCount: totalStates,
|
||||||
|
state: state,
|
||||||
|
setUpdateStateModal: setUpdateStateModal,
|
||||||
|
};
|
||||||
|
|
||||||
const handleStateSequence = useCallback(
|
const handleStateSequence = useCallback(
|
||||||
async (payload: Partial<IState>) => {
|
async (payload: Partial<IState>) => {
|
||||||
try {
|
try {
|
||||||
if (!workspaceSlug || !projectId || !payload.id) return;
|
if (!payload.id) return;
|
||||||
await moveStatePosition(workspaceSlug, projectId, payload.id, payload);
|
await stateOperationsCallbacks.moveStatePosition(payload.id, payload);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("error", error);
|
console.error("error", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[workspaceSlug, projectId, moveStatePosition]
|
[stateOperationsCallbacks]
|
||||||
);
|
);
|
||||||
|
|
||||||
// derived values
|
|
||||||
const isDraggable = totalStates === 1 ? false : true;
|
|
||||||
|
|
||||||
// DND starts
|
|
||||||
// ref
|
|
||||||
const draggableElementRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
// states
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
|
||||||
const [isDraggedOver, setIsDraggedOver] = useState(false);
|
|
||||||
const [closestEdge, setClosestEdge] = useState<string | null>(null);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const elementRef = draggableElementRef.current;
|
const elementRef = draggableElementRef.current;
|
||||||
const initialData: TDraggableData = { groupKey: groupKey, id: state.id };
|
const initialData: TDraggableData = { groupKey: groupKey, id: state.id };
|
||||||
|
|
@ -111,9 +119,9 @@ export const StateItem: FC<TStateItem> = observer((props) => {
|
||||||
if (updateStateModal)
|
if (updateStateModal)
|
||||||
return (
|
return (
|
||||||
<StateUpdate
|
<StateUpdate
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
projectId={projectId}
|
|
||||||
state={state}
|
state={state}
|
||||||
|
updateStateCallback={stateOperationsCallbacks.updateState}
|
||||||
|
shouldTrackEvents={shouldTrackEvents}
|
||||||
handleClose={() => setUpdateStateModal(false)}
|
handleClose={() => setUpdateStateModal(false)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -122,25 +130,29 @@ export const StateItem: FC<TStateItem> = observer((props) => {
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{/* draggable drop top indicator */}
|
{/* draggable drop top indicator */}
|
||||||
<DropIndicator isVisible={isDraggedOver && closestEdge === "top"} />
|
<DropIndicator isVisible={isDraggedOver && closestEdge === "top"} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={draggableElementRef}
|
ref={draggableElementRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative border border-custom-border-100 bg-custom-background-100 py-3 px-3.5 rounded group",
|
"relative border border-custom-border-100 bg-custom-background-100 py-3 px-3.5 rounded group",
|
||||||
isDragging ? `opacity-50` : `opacity-100`,
|
isDragging ? `opacity-50` : `opacity-100`,
|
||||||
totalStates === 1 ? `cursor-auto` : `cursor-grab`
|
totalStates === 1 ? `cursor-auto` : `cursor-grab`,
|
||||||
|
stateItemClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{disabled ? (
|
||||||
|
<StateItemTitle {...commonStateItemListProps} disabled />
|
||||||
|
) : (
|
||||||
<StateItemTitle
|
<StateItemTitle
|
||||||
workspaceSlug={workspaceSlug}
|
{...commonStateItemListProps}
|
||||||
projectId={projectId}
|
disabled={false}
|
||||||
setUpdateStateModal={setUpdateStateModal}
|
stateOperationsCallbacks={{
|
||||||
stateCount={totalStates}
|
markStateAsDefault: stateOperationsCallbacks.markStateAsDefault,
|
||||||
disabled={disabled}
|
deleteState: stateOperationsCallbacks.deleteState,
|
||||||
state={state}
|
}}
|
||||||
|
shouldTrackEvents={shouldTrackEvents}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* draggable drop bottom indicator */}
|
{/* draggable drop bottom indicator */}
|
||||||
<DropIndicator isVisible={isDraggedOver && closestEdge === "bottom"} />
|
<DropIndicator isVisible={isDraggedOver && closestEdge === "bottom"} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|
|
||||||
|
|
@ -2,34 +2,44 @@
|
||||||
|
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { IState, TStateGroups } from "@plane/types";
|
import { IState, TStateGroups, TStateOperationsCallbacks } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { StateItem } from "@/components/project-states";
|
import { StateItem } from "@/components/project-states";
|
||||||
|
|
||||||
type TStateList = {
|
type TStateList = {
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
groupKey: TStateGroups;
|
groupKey: TStateGroups;
|
||||||
groupedStates: Record<string, IState[]>;
|
groupedStates: Record<string, IState[]>;
|
||||||
states: IState[];
|
states: IState[];
|
||||||
|
stateOperationsCallbacks: TStateOperationsCallbacks;
|
||||||
|
shouldTrackEvents: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
stateItemClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StateList: FC<TStateList> = observer((props) => {
|
export const StateList: FC<TStateList> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, groupKey, groupedStates, states, disabled = false } = props;
|
const {
|
||||||
|
groupKey,
|
||||||
|
groupedStates,
|
||||||
|
states,
|
||||||
|
stateOperationsCallbacks,
|
||||||
|
shouldTrackEvents,
|
||||||
|
disabled = false,
|
||||||
|
stateItemClassName,
|
||||||
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{states.map((state: IState) => (
|
{states.map((state: IState) => (
|
||||||
<StateItem
|
<StateItem
|
||||||
key={state?.name}
|
key={state?.name}
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
projectId={projectId}
|
|
||||||
groupKey={groupKey}
|
groupKey={groupKey}
|
||||||
groupedStates={groupedStates}
|
groupedStates={groupedStates}
|
||||||
totalStates={states.length || 0}
|
totalStates={states.length || 0}
|
||||||
state={state}
|
state={state}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
stateOperationsCallbacks={stateOperationsCallbacks}
|
||||||
|
shouldTrackEvents={shouldTrackEvents}
|
||||||
|
stateItemClassName={stateItemClassName}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ type Props = {
|
||||||
setToFavorite?: boolean;
|
setToFavorite?: boolean;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
data?: Partial<TProject>;
|
data?: Partial<TProject>;
|
||||||
|
templateId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum EProjectCreationSteps {
|
enum EProjectCreationSteps {
|
||||||
|
|
@ -27,7 +28,7 @@ enum EProjectCreationSteps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateProjectModal: FC<Props> = (props) => {
|
export const CreateProjectModal: FC<Props> = (props) => {
|
||||||
const { isOpen, onClose, setToFavorite = false, workspaceSlug, data } = props;
|
const { isOpen, onClose, setToFavorite = false, workspaceSlug, data, templateId } = props;
|
||||||
// states
|
// states
|
||||||
const [currentStep, setCurrentStep] = useState<EProjectCreationSteps>(EProjectCreationSteps.CREATE_PROJECT);
|
const [currentStep, setCurrentStep] = useState<EProjectCreationSteps>(EProjectCreationSteps.CREATE_PROJECT);
|
||||||
const [createdProjectId, setCreatedProjectId] = useState<string | null>(null);
|
const [createdProjectId, setCreatedProjectId] = useState<string | null>(null);
|
||||||
|
|
@ -63,6 +64,7 @@ export const CreateProjectModal: FC<Props> = (props) => {
|
||||||
updateCoverImageStatus={handleCoverImageStatusUpdate}
|
updateCoverImageStatus={handleCoverImageStatusUpdate}
|
||||||
handleNextStep={handleNextStep}
|
handleNextStep={handleNextStep}
|
||||||
data={data}
|
data={data}
|
||||||
|
templateId={templateId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{currentStep === EProjectCreationSteps.FEATURE_SELECTION && (
|
{currentStep === EProjectCreationSteps.FEATURE_SELECTION && (
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import { ImagePickerPopover } from "@/components/core";
|
||||||
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
|
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
|
||||||
import { getFileURL } from "@/helpers/file.helper";
|
import { getFileURL } from "@/helpers/file.helper";
|
||||||
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
||||||
|
// plane web imports
|
||||||
|
import { ProjectTemplateSelect } from "@/plane-web/components/projects/create/template-select";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
|
|
@ -39,6 +41,9 @@ const ProjectCreateHeader: React.FC<Props> = (props) => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="absolute left-2.5 top-2.5">
|
||||||
|
<ProjectTemplateSelect handleModalClose={handleClose} />
|
||||||
|
</div>
|
||||||
<div className="absolute right-2 top-2 p-2">
|
<div className="absolute right-2 top-2 p-2">
|
||||||
<button data-posthog="PROJECT_MODAL_CLOSE" type="button" onClick={handleClose} tabIndex={getIndex("close")}>
|
<button data-posthog="PROJECT_MODAL_CLOSE" type="button" onClick={handleClose} tabIndex={getIndex("close")}>
|
||||||
<X className="h-5 w-5 text-white" />
|
<X className="h-5 w-5 text-white" />
|
||||||
|
|
|
||||||
|
|
@ -59,17 +59,13 @@ export const ProjectFeaturesList: FC<Props> = observer((props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{Object.keys(PROJECT_FEATURES_LIST).map((featureSectionKey) => {
|
{Object.entries(PROJECT_FEATURES_LIST).map(([featureSectionKey, feature]) => (
|
||||||
const feature = PROJECT_FEATURES_LIST[featureSectionKey];
|
|
||||||
return (
|
|
||||||
<div key={featureSectionKey} className="">
|
<div key={featureSectionKey} className="">
|
||||||
<div className="flex flex-col justify-center pb-2 border-b border-custom-border-100">
|
<div className="flex flex-col justify-center pb-2 border-b border-custom-border-100">
|
||||||
<h3 className="text-xl font-medium">{t(feature.key)}</h3>
|
<h3 className="text-xl font-medium">{t(feature.key)}</h3>
|
||||||
<h4 className="text-sm leading-5 text-custom-text-200">{t(`${feature.key}_description`)}</h4>
|
<h4 className="text-sm leading-5 text-custom-text-200">{t(`${feature.key}_description`)}</h4>
|
||||||
</div>
|
</div>
|
||||||
{Object.keys(feature.featureList).map((featureItemKey) => {
|
{Object.entries(feature.featureList).map(([featureItemKey, featureItem]) => (
|
||||||
const featureItem = feature.featureList[featureItemKey];
|
|
||||||
return (
|
|
||||||
<div
|
<div
|
||||||
key={featureItemKey}
|
key={featureItemKey}
|
||||||
className="gap-x-8 gap-y-2 border-b border-custom-border-100 bg-custom-background-100 pb-2 pt-4"
|
className="gap-x-8 gap-y-2 border-b border-custom-border-100 bg-custom-background-100 pb-2 pt-4"
|
||||||
|
|
@ -93,7 +89,6 @@ export const ProjectFeaturesList: FC<Props> = observer((props) => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={Boolean(currentProjectDetails?.[featureItem.property as keyof IProject])}
|
value={Boolean(currentProjectDetails?.[featureItem.property as keyof IProject])}
|
||||||
onChange={() => handleSubmit(featureItemKey, featureItem.property)}
|
onChange={() => handleSubmit(featureItemKey, featureItem.property)}
|
||||||
|
|
@ -106,11 +101,9 @@ export const ProjectFeaturesList: FC<Props> = observer((props) => {
|
||||||
featureItem.renderChildren?.(currentProjectDetails, workspaceSlug)}
|
featureItem.renderChildren?.(currentProjectDetails, workspaceSlug)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ export interface IProjectStore {
|
||||||
getPartialProjectById: (projectId: string | undefined | null) => TPartialProject | undefined;
|
getPartialProjectById: (projectId: string | undefined | null) => TPartialProject | undefined;
|
||||||
getProjectIdentifierById: (projectId: string | undefined | null) => string;
|
getProjectIdentifierById: (projectId: string | undefined | null) => string;
|
||||||
getProjectAnalyticsCountById: (projectId: string | undefined | null) => TProjectAnalyticsCount | undefined;
|
getProjectAnalyticsCountById: (projectId: string | undefined | null) => TProjectAnalyticsCount | undefined;
|
||||||
|
getProjectByIdentifier: (projectIdentifier: string) => TProject | undefined;
|
||||||
// collapsible
|
// collapsible
|
||||||
openCollapsibleSection: ProjectOverviewCollapsible[];
|
openCollapsibleSection: ProjectOverviewCollapsible[];
|
||||||
lastCollapsibleAction: ProjectOverviewCollapsible | null;
|
lastCollapsibleAction: ProjectOverviewCollapsible | null;
|
||||||
|
|
@ -45,6 +46,9 @@ export interface IProjectStore {
|
||||||
setLastCollapsibleAction: (section: ProjectOverviewCollapsible) => void;
|
setLastCollapsibleAction: (section: ProjectOverviewCollapsible) => void;
|
||||||
toggleOpenCollapsibleSection: (section: ProjectOverviewCollapsible) => void;
|
toggleOpenCollapsibleSection: (section: ProjectOverviewCollapsible) => void;
|
||||||
|
|
||||||
|
// helper actions
|
||||||
|
processProjectAfterCreation: (workspaceSlug: string, data: TProject) => void;
|
||||||
|
|
||||||
// fetch actions
|
// fetch actions
|
||||||
fetchPartialProjects: (workspaceSlug: string) => Promise<TPartialProject[]>;
|
fetchPartialProjects: (workspaceSlug: string) => Promise<TPartialProject[]>;
|
||||||
fetchProjects: (workspaceSlug: string) => Promise<TProject[]>;
|
fetchProjects: (workspaceSlug: string) => Promise<TProject[]>;
|
||||||
|
|
@ -104,6 +108,8 @@ export class ProjectStore implements IProjectStore {
|
||||||
currentProjectDetails: computed,
|
currentProjectDetails: computed,
|
||||||
joinedProjectIds: computed,
|
joinedProjectIds: computed,
|
||||||
favoriteProjectIds: computed,
|
favoriteProjectIds: computed,
|
||||||
|
// helper actions
|
||||||
|
processProjectAfterCreation: action,
|
||||||
// fetch actions
|
// fetch actions
|
||||||
fetchPartialProjects: action,
|
fetchPartialProjects: action,
|
||||||
fetchProjects: action,
|
fetchProjects: action,
|
||||||
|
|
@ -233,7 +239,10 @@ export class ProjectStore implements IProjectStore {
|
||||||
const projectIds = projects
|
const projectIds = projects
|
||||||
.filter(
|
.filter(
|
||||||
(project) =>
|
(project) =>
|
||||||
project.workspace === currentWorkspace.id && !!project.member_role && project.is_favorite && !project.archived_at
|
project.workspace === currentWorkspace.id &&
|
||||||
|
!!project.member_role &&
|
||||||
|
project.is_favorite &&
|
||||||
|
!project.archived_at
|
||||||
)
|
)
|
||||||
.map((project) => project.id);
|
.map((project) => project.id);
|
||||||
return projectIds;
|
return projectIds;
|
||||||
|
|
@ -256,6 +265,19 @@ export class ProjectStore implements IProjectStore {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description process project after creation
|
||||||
|
* @param workspaceSlug
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
processProjectAfterCreation = (workspaceSlug: string, data: TProject) => {
|
||||||
|
runInAction(() => {
|
||||||
|
set(this.projectMap, [data.id], data);
|
||||||
|
// updating the user project role in workspaceProjectsPermissions
|
||||||
|
set(this.rootStore.user.permission.workspaceProjectsPermissions, [workspaceSlug, data.id], data.member_role);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get Workspace projects partial data using workspace slug
|
* get Workspace projects partial data using workspace slug
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
|
@ -363,6 +385,15 @@ export class ProjectStore implements IProjectStore {
|
||||||
return projectInfo;
|
return projectInfo;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns project details using project identifier
|
||||||
|
* @param projectIdentifier
|
||||||
|
* @returns TProject | undefined
|
||||||
|
*/
|
||||||
|
getProjectByIdentifier = computedFn((projectIdentifier: string) =>
|
||||||
|
Object.values(this.projectMap).find((project) => project.identifier === projectIdentifier)
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns project lite using project id
|
* Returns project lite using project id
|
||||||
* This method is used just for type safety
|
* This method is used just for type safety
|
||||||
|
|
@ -481,15 +512,7 @@ export class ProjectStore implements IProjectStore {
|
||||||
createProject = async (workspaceSlug: string, data: any) => {
|
createProject = async (workspaceSlug: string, data: any) => {
|
||||||
try {
|
try {
|
||||||
const response = await this.projectService.createProject(workspaceSlug, data);
|
const response = await this.projectService.createProject(workspaceSlug, data);
|
||||||
runInAction(() => {
|
this.processProjectAfterCreation(workspaceSlug, response);
|
||||||
set(this.projectMap, [response.id], response);
|
|
||||||
// updating the user project role in workspaceProjectsPermissions
|
|
||||||
set(
|
|
||||||
this.rootStore.user.permission.workspaceProjectsPermissions,
|
|
||||||
[workspaceSlug, response.id],
|
|
||||||
response.member_role
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Failed to create project from project store");
|
console.log("Failed to create project from project store");
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,8 @@
|
||||||
// ui
|
// plane imports
|
||||||
|
import { RANDOM_EMOJI_CODES } from "@plane/constants";
|
||||||
import { LUCIDE_ICONS_LIST } from "@plane/ui";
|
import { LUCIDE_ICONS_LIST } from "@plane/ui";
|
||||||
|
|
||||||
export const getRandomEmoji = () => {
|
export const getRandomEmoji = () => RANDOM_EMOJI_CODES[Math.floor(Math.random() * RANDOM_EMOJI_CODES.length)];
|
||||||
const emojis = [
|
|
||||||
"8986",
|
|
||||||
"9200",
|
|
||||||
"128204",
|
|
||||||
"127773",
|
|
||||||
"127891",
|
|
||||||
"128076",
|
|
||||||
"128077",
|
|
||||||
"128187",
|
|
||||||
"128188",
|
|
||||||
"128512",
|
|
||||||
"128522",
|
|
||||||
"128578",
|
|
||||||
];
|
|
||||||
|
|
||||||
return emojis[Math.floor(Math.random() * emojis.length)];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getRandomIconName = () => LUCIDE_ICONS_LIST[Math.floor(Math.random() * LUCIDE_ICONS_LIST.length)].name;
|
export const getRandomIconName = () => LUCIDE_ICONS_LIST[Math.floor(Math.random() * LUCIDE_ICONS_LIST.length)].name;
|
||||||
|
|
||||||
|
|
@ -45,8 +29,18 @@ export const groupReactions: (reactions: any[], key: string) => { [key: string]:
|
||||||
reactions: any,
|
reactions: any,
|
||||||
key: string
|
key: string
|
||||||
) => {
|
) => {
|
||||||
|
if (!Array.isArray(reactions)) {
|
||||||
|
console.error("Expected an array of reactions, but got:", reactions);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
const groupedReactions = reactions.reduce(
|
const groupedReactions = reactions.reduce(
|
||||||
(acc: any, reaction: any) => {
|
(acc: any, reaction: any) => {
|
||||||
|
if (!reaction || typeof reaction !== "object" || !Object.prototype.hasOwnProperty.call(reaction, key)) {
|
||||||
|
console.warn("Skipping undefined reaction or missing key:", reaction);
|
||||||
|
return acc; // Skip undefined reactions or those without the specified key
|
||||||
|
}
|
||||||
|
|
||||||
if (!acc[reaction[key]]) {
|
if (!acc[reaction[key]]) {
|
||||||
acc[reaction[key]] = [];
|
acc[reaction[key]] = [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue