fix: minor refactoring changes for state dropdowns

This commit is contained in:
sriram veeraghanta 2025-02-24 14:14:24 +05:30
parent da469dac18
commit 952eee8d55
39 changed files with 291 additions and 266 deletions

View file

@ -675,6 +675,16 @@
"disconnecting": "Disconnecting", "disconnecting": "Disconnecting",
"installing": "Installing", "installing": "Installing",
"install": "Install", "install": "Install",
"reset": "Reset",
"live": "Live",
"change_history": "Change History",
"coming_soon": "Coming soon",
"members": "Members",
"you": "You",
"upgrade_cta": {
"higher_subscription": "Upgrade to higher subscription",
"talk_to_sales": "Talk to sales"
},
"category": "Category", "category": "Category",
"categories": "Categories", "categories": "Categories",
"saving": "Saving", "saving": "Saving",

View file

@ -215,6 +215,7 @@
"activity": "Actividad", "activity": "Actividad",
"appearance": "Apariencia", "appearance": "Apariencia",
"notifications": "Notificaciones", "notifications": "Notificaciones",
"connections": "Conexiones",
"workspaces": "Espacios de trabajo", "workspaces": "Espacios de trabajo",
"create_workspace": "Crear espacio de trabajo", "create_workspace": "Crear espacio de trabajo",
"invitations": "Invitaciones", "invitations": "Invitaciones",
@ -291,6 +292,7 @@
"workspace_logo": "Logo del espacio de trabajo", "workspace_logo": "Logo del espacio de trabajo",
"new_issue": "Nuevo elemento de trabajo", "new_issue": "Nuevo elemento de trabajo",
"your_work": "Tu trabajo", "your_work": "Tu trabajo",
"workspace_dashboards": "Paneles de control",
"drafts": "Borradores", "drafts": "Borradores",
"projects": "Proyectos", "projects": "Proyectos",
"views": "Vistas", "views": "Vistas",
@ -845,6 +847,16 @@
"disconnecting": "Desconectando", "disconnecting": "Desconectando",
"installing": "Instalando", "installing": "Instalando",
"install": "Instalar", "install": "Instalar",
"reset": "Reiniciar",
"live": "En vivo",
"change_history": "Historial de cambios",
"coming_soon": "Próximamente",
"members": "Miembros",
"you": "Tú",
"upgrade_cta": {
"higher_subscription": "Mejorar a una suscripción más alta",
"talk_to_sales": "Hablar con ventas"
},
"category": "Categoría", "category": "Categoría",
"categories": "Categorías", "categories": "Categorías",
"saving": "Guardando", "saving": "Guardando",

View file

@ -845,6 +845,16 @@
"disconnecting": "Déconnexion", "disconnecting": "Déconnexion",
"installing": "Installation", "installing": "Installation",
"install": "Installer", "install": "Installer",
"reset": "Réinitialiser",
"live": "En direct",
"change_history": "Historique des modifications",
"coming_soon": "À venir",
"members": "Membres",
"you": "Vous",
"upgrade_cta": {
"higher_subscription": "Passer à une abonnement plus élevé",
"talk_to_sales": "Parler à la vente"
},
"category": "Catégorie", "category": "Catégorie",
"categories": "Catégories", "categories": "Catégories",
"saving": "Enregistrement", "saving": "Enregistrement",

View file

@ -845,6 +845,16 @@
"disconnecting": "切断中", "disconnecting": "切断中",
"installing": "インストール中", "installing": "インストール中",
"install": "インストール", "install": "インストール",
"reset": "リセット",
"live": "ライブ",
"change_history": "変更履歴",
"coming_soon": "近日公開",
"members": "メンバー",
"you": "あなた",
"upgrade_cta": {
"higher_subscription": "高いサブスクリプションにアップグレード",
"talk_to_sales": "セールスに連絡"
},
"category": "カテゴリー", "category": "カテゴリー",
"categories": "カテゴリーズ", "categories": "カテゴリーズ",
"saving": "セービング", "saving": "セービング",

View file

@ -845,6 +845,16 @@
"disconnecting": "正在断开连接", "disconnecting": "正在断开连接",
"installing": "正在安装", "installing": "正在安装",
"install": "安装", "install": "安装",
"reset": "重置",
"live": "实时",
"change_history": "变更历史",
"coming_soon": "即将推出",
"members": "成员",
"you": "你",
"upgrade_cta": {
"higher_subscription": "升级到更高订阅",
"talk_to_sales": "联系销售"
},
"category": "类别", "category": "类别",
"categories": "类别", "categories": "类别",
"saving": "保存中", "saving": "保存中",

View file

@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from "react";
import { Tooltip2 } from "@blueprintjs/popover2"; import { Tooltip2 } from "@blueprintjs/popover2";
import React, { useEffect, useRef, useState } from "react";
// helpers // helpers
import { cn } from "../../helpers"; import { cn } from "../../helpers";
@ -23,7 +23,6 @@ export type TPosition =
interface ITooltipProps { interface ITooltipProps {
tooltipHeading?: string; tooltipHeading?: string;
tooltipContent: string | React.ReactNode; tooltipContent: string | React.ReactNode;
jsxContent?: string | React.ReactNode;
position?: TPosition; position?: TPosition;
children: JSX.Element; children: JSX.Element;
disabled?: boolean; disabled?: boolean;
@ -39,14 +38,13 @@ export const Tooltip: React.FC<ITooltipProps> = ({
tooltipContent, tooltipContent,
position = "top", position = "top",
children, children,
jsxContent,
disabled = false, disabled = false,
className = "", className = "",
openDelay = 200, openDelay = 200,
closeDelay, closeDelay,
isMobile = false, isMobile = false,
renderByDefault = true, //FIXME: tooltip should always render on hover and not by default, this is a temporary fix renderByDefault = true, //FIXME: tooltip should always render on hover and not by default, this is a temporary fix
}: ITooltipProps) => { }) => {
const toolTipRef = useRef<HTMLDivElement | null>(null); const toolTipRef = useRef<HTMLDivElement | null>(null);
const [shouldRender, setShouldRender] = useState(renderByDefault); const [shouldRender, setShouldRender] = useState(renderByDefault);
@ -81,9 +79,6 @@ export const Tooltip: React.FC<ITooltipProps> = ({
hoverOpenDelay={openDelay} hoverOpenDelay={openDelay}
hoverCloseDelay={closeDelay} hoverCloseDelay={closeDelay}
content={ content={
jsxContent ? (
<>{jsxContent}</>
) : (
<div <div
className={cn( className={cn(
"relative block z-50 max-w-xs gap-1 overflow-hidden break-words rounded-md bg-custom-background-100 p-2 text-xs text-custom-text-200 shadow-md", "relative block z-50 max-w-xs gap-1 overflow-hidden break-words rounded-md bg-custom-background-100 p-2 text-xs text-custom-text-200 shadow-md",
@ -96,7 +91,6 @@ export const Tooltip: React.FC<ITooltipProps> = ({
{tooltipHeading && <h5 className="font-medium text-custom-text-100">{tooltipHeading}</h5>} {tooltipHeading && <h5 className="font-medium text-custom-text-100">{tooltipHeading}</h5>}
{tooltipContent} {tooltipContent}
</div> </div>
)
} }
position={position} position={position}
renderTarget={({ renderTarget={({

View file

@ -0,0 +1 @@
export * from "./work-item-actions";

View file

@ -0,0 +1,43 @@
import { Command } from "cmdk";
import { observer } from "mobx-react";
import { Check } from "lucide-react";
// plane imports
import { Spinner, StateGroupIcon } from "@plane/ui";
// store hooks
import { useProjectState } from "@/hooks/store";
export type TChangeWorkItemStateListProps = {
projectId: string | null;
currentStateId: string | null;
handleStateChange: (stateId: string) => void;
};
export const ChangeWorkItemStateList = observer((props: TChangeWorkItemStateListProps) => {
const { projectId, currentStateId, handleStateChange } = props;
// store hooks
const { getProjectStates } = useProjectState();
// derived values
const projectStates = getProjectStates(projectId);
return (
<>
{projectStates ? (
projectStates.length > 0 ? (
projectStates.map((state) => (
<Command.Item key={state.id} onSelect={() => handleStateChange(state.id)} className="focus:outline-none">
<div className="flex items-center space-x-3">
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
<p>{state.name}</p>
</div>
<div>{state.id === currentStateId && <Check className="h-3 w-3" />}</div>
</Command.Item>
))
) : (
<div className="text-center">No states found</div>
)
) : (
<Spinner />
)}
</>
);
});

View file

@ -0,0 +1 @@
export * from "./change-state-list";

View file

@ -1,20 +0,0 @@
import { Plus } from "lucide-react";
// plane utils
import { cn } from "@plane/utils";
type Props = {
workspaceSlug: string;
projectId: string;
parentStateId: string;
onTransitionAdd?: () => void;
};
export const AddStateTransition = (props: Props) => (
<div className={cn("flex w-full px-3 h-6 items-center justify-start gap-2 text-sm bg-custom-background-90")}>
<>
<Plus className="h-4 w-4" color="#8591AD" />
<span className="text-custom-text-400 font-medium"> Add Transition</span>
<div className="text-white bg-custom-background-80 font-semibold px-2 rounded-lg">Pro</div>
</>
</div>
);

View file

@ -1,6 +1,5 @@
export * from "./state-option"; export * from "./state-option";
export * from "./state-item-child";
export * from "./state-transition-count";
export * from "./use-workflow-drag-n-drop"; export * from "./use-workflow-drag-n-drop";
export * from "./workflow-disabled-message"; export * from "./workflow-disabled-message";
export * from "./workflow-group-tree"; export * from "./workflow-group-tree";
export * from "./workflow-disabled-overlay";

View file

@ -1,39 +0,0 @@
import { SetStateAction } from "react";
import { observer } from "mobx-react";
// Plane
import { DISPLAY_WORKFLOW_PRO_CTA } from "@plane/constants";
import { IState } from "@plane/types";
// components
import { StateItemTitle } from "@/components/project-states/state-item-title";
// constants
//
import { AddStateTransition } from "./add-state-transition";
export type StateItemChildProps = {
workspaceSlug: string;
projectId: string;
stateCount: number;
disabled: boolean;
state: IState;
setUpdateStateModal: (value: SetStateAction<boolean>) => void;
};
export const StateItemChild = observer((props: StateItemChildProps) => {
const { workspaceSlug, projectId, stateCount, setUpdateStateModal, disabled, state } = props;
return (
<div className="flex flex-col w-full items-center justify-between">
<StateItemTitle
workspaceSlug={workspaceSlug}
projectId={projectId}
setUpdateStateModal={setUpdateStateModal}
stateCount={stateCount}
disabled={disabled}
state={state}
/>
{DISPLAY_WORKFLOW_PRO_CTA && (
<AddStateTransition workspaceSlug={workspaceSlug} projectId={projectId} parentStateId={state.id} />
)}
</div>
);
});

View file

@ -12,6 +12,7 @@ type Props = {
filterAvailableStateIds: boolean; filterAvailableStateIds: boolean;
selectedValue: string | null | undefined; selectedValue: string | null | undefined;
className?: string; className?: string;
isForWorkItemCreation?: boolean;
}; };
export const StateOption = observer((props: Props) => { export const StateOption = observer((props: Props) => {

View file

@ -1,7 +0,0 @@
import { IStateWorkFlow } from "@/plane-web/types";
type Props = {
currentTransitionMap?: IStateWorkFlow;
};
export const StateTransitionCount = (props: Props) => <></>;

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { TIssueGroupByOptions } from "@plane/types"; import { TIssueGroupByOptions } from "@plane/types";
export const useWorkFlowFDragNDrop = ( export const useWorkFlowFDragNDrop = (
@ -6,6 +7,7 @@ export const useWorkFlowFDragNDrop = (
) => ({ ) => ({
workflowDisabledSource: undefined, workflowDisabledSource: undefined,
isWorkflowDropDisabled: false, isWorkflowDropDisabled: false,
getIsWorkflowWorkItemCreationDisabled: (groupId: string, subGroupId?: string) => false,
handleWorkFlowState: ( handleWorkFlowState: (
sourceGroupId: string, sourceGroupId: string,
destinationGroupId: string, destinationGroupId: string,

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
type Props = { type Props = {
parentStateId: string; parentStateId: string;
className?: string; className?: string;

View file

@ -0,0 +1,10 @@
import { observer } from "mobx-react";
export type TWorkflowDisabledOverlayProps = {
messageContainerRef: React.RefObject<HTMLDivElement>;
workflowDisabledSource: string;
shouldOverlayBeVisible: boolean;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const WorkFlowDisabledOverlay = observer((props: TWorkflowDisabledOverlayProps) => <></>);

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { TIssueGroupByOptions } from "@plane/types"; import { TIssueGroupByOptions } from "@plane/types";
type Props = { type Props = {

View file

@ -0,0 +1,2 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const useWorkspaceIssuePropertiesExtended = (workspaceSlug: string | string[] | undefined) => {};

View file

@ -1,4 +1,3 @@
export * from "./projects"; export * from "./projects";
export * from "./issue-types"; export * from "./issue-types";
export * from "./gantt-chart"; export * from "./gantt-chart";
export * from "./state.d";

View file

@ -1,8 +0,0 @@
export interface IStateTransition {
transition_state_id: string;
actors: string[];
}
export interface IStateWorkFlow {
[transitionId: string]: IStateTransition;
}

View file

@ -1,16 +1,13 @@
"use client"; "use client";
import { Command } from "cmdk";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// hooks // plane imports
import { Check } from "lucide-react";
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
import { Spinner, StateGroupIcon } from "@plane/ui"; // store hooks
import { useProjectState, useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
// ui // plane web imports
// icons import { ChangeWorkItemStateList } from "@/plane-web/components/command-palette/actions/work-item-actions";
// types
type Props = { closePalette: () => void; issue: TIssue }; type Props = { closePalette: () => void; issue: TIssue };
@ -20,10 +17,9 @@ export const ChangeIssueState: React.FC<Props> = observer((props) => {
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
// store hooks // store hooks
const { updateIssue } = useIssueDetail(); const { updateIssue } = useIssueDetail();
const { getProjectStates } = useProjectState();
// derived values // derived values
const projectId = issue?.project_id; const projectId = issue?.project_id;
const projectStates = getProjectStates(projectId); const currentStateId = issue?.state_id;
const submitChanges = async (formData: Partial<TIssue>) => { const submitChanges = async (formData: Partial<TIssue>) => {
if (!workspaceSlug || !projectId || !issue) return; if (!workspaceSlug || !projectId || !issue) return;
@ -40,24 +36,10 @@ export const ChangeIssueState: React.FC<Props> = observer((props) => {
}; };
return ( return (
<> <ChangeWorkItemStateList
{projectStates ? ( projectId={projectId}
projectStates.length > 0 ? ( currentStateId={currentStateId}
projectStates.map((state) => ( handleStateChange={handleIssueState}
<Command.Item key={state.id} onSelect={() => handleIssueState(state.id)} className="focus:outline-none"> />
<div className="flex items-center space-x-3">
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
<p>{state.name}</p>
</div>
<div>{state.id === issue.state_id && <Check className="h-3 w-3" />}</div>
</Command.Item>
))
) : (
<div className="text-center">No states found</div>
)
) : (
<Spinner />
)}
</>
); );
}); });

View file

@ -29,15 +29,17 @@ export const ActivityBlockComponent: FC<TActivityBlockComponent> = (props) => {
if (!activity) return <></>; if (!activity) return <></>;
return ( return (
<div <div
className={`relative flex items-center gap-2 text-xs ${ className={`relative flex items-start gap-2 text-xs ${
ends === "top" ? `pb-3` : ends === "bottom" ? `pt-3` : `py-3` ends === "top" ? `pb-3` : ends === "bottom" ? `pt-3` : `py-3`
}`} }`}
> >
<div className="flex-shrink-0 ring-6 w-7 h-7 rounded-full overflow-hidden flex justify-center items-start mt-0.5 z-[4] text-custom-text-200"> <div className="flex-shrink-0 ring-6 w-7 h-7 rounded-full overflow-hidden flex justify-center items-start mt-0.5 z-[4] text-custom-text-200">
{icon ? icon : <Network className="w-3.5 h-3.5" />} {icon ? icon : <Network className="w-3.5 h-3.5" />}
</div> </div>
<div className="w-full truncate text-custom-text-200"> <div className="w-full text-custom-text-200">
<div className="line-clamp-2">
<User activity={activity} customUserName={customUserName} /> {children} <User activity={activity} customUserName={customUserName} /> {children}
</div>
<div className="mt-1"> <div className="mt-1">
<Tooltip <Tooltip
isMobile={isMobile} isMobile={isMobile}

View file

@ -35,6 +35,7 @@ type Props = TDropdownProps & {
renderByDefault?: boolean; renderByDefault?: boolean;
stateIds?: string[]; stateIds?: string[];
filterAvailableStateIds?: boolean; filterAvailableStateIds?: boolean;
isForWorkItemCreation?: boolean;
}; };
export const StateDropdown: React.FC<Props> = observer((props) => { export const StateDropdown: React.FC<Props> = observer((props) => {
@ -59,6 +60,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
renderByDefault = true, renderByDefault = true,
stateIds, stateIds,
filterAvailableStateIds = true, filterAvailableStateIds = true,
isForWorkItemCreation = false,
} = props; } = props;
// states // states
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@ -98,7 +100,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
content: ( content: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<StateGroupIcon stateGroup={state?.group ?? "backlog"} color={state?.color} className="h-3 w-3 flex-shrink-0" /> <StateGroupIcon stateGroup={state?.group ?? "backlog"} color={state?.color} className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">{state?.name}</span> <span className="flex-grow truncate text-left">{state?.name}</span>
</div> </div>
), ),
})); }));
@ -182,7 +184,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
/> />
)} )}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate">{selectedState?.name ?? t("state")}</span> <span className="flex-grow truncate text-left">{selectedState?.name ?? t("state")}</span>
)} )}
{dropdownArrow && ( {dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" /> <ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
@ -239,6 +241,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
filterAvailableStateIds={filterAvailableStateIds} filterAvailableStateIds={filterAvailableStateIds}
selectedValue={value} selectedValue={value}
className="flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5" className="flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5"
isForWorkItemCreation={isForWorkItemCreation}
/> />
)) ))
) : ( ) : (

View file

@ -1,12 +1,13 @@
import { useRef } from "react";
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
// Plane // plane imports
import { ISSUE_ORDER_BY_OPTIONS } from "@plane/constants"; import { ISSUE_ORDER_BY_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { TIssueOrderByOptions } from "@plane/types"; import { TIssueOrderByOptions } from "@plane/types";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
// Plane-web // plane web imports
import { WorkFlowDisabledMessage } from "@/plane-web/components/workflow"; import { WorkFlowDisabledOverlay } from "@/plane-web/components/workflow";
type Props = { type Props = {
dragColumnOrientation: "justify-start" | "justify-center" | "justify-end"; dragColumnOrientation: "justify-start" | "justify-center" | "justify-end";
@ -30,9 +31,10 @@ export const GroupDragOverlay = (props: Props) => {
isDraggingOverColumn, isDraggingOverColumn,
isEpic = false, isEpic = false,
} = props; } = props;
// hooks // hooks
const { t } = useTranslation(); const { t } = useTranslation();
// refs
const messageContainerRef = useRef<HTMLDivElement>(null);
const shouldOverlayBeVisible = isDraggingOverColumn && canOverlayBeVisible; const shouldOverlayBeVisible = isDraggingOverColumn && canOverlayBeVisible;
const readableOrderBy = t( const readableOrderBy = t(
@ -41,27 +43,28 @@ export const GroupDragOverlay = (props: Props) => {
return ( return (
<div <div
ref={messageContainerRef}
className={cn( className={cn(
`absolute top-0 left-0 h-full w-full items-center text-sm font-medium text-custom-text-300 rounded bg-custom-background-overlay ${dragColumnOrientation}`, `absolute top-0 left-0 h-full w-full items-center text-sm font-medium text-custom-text-300 rounded bg-custom-background-80/85 ${dragColumnOrientation}`,
{ {
"flex flex-col border-[1px] border-custom-border-300 z-[2]": shouldOverlayBeVisible, "flex flex-col border-[1px] border-custom-border-300 z-[2]": shouldOverlayBeVisible,
"bg-red-200/60": workflowDisabledSource && isDropDisabled,
}, },
{ hidden: !shouldOverlayBeVisible } { hidden: !shouldOverlayBeVisible }
)} )}
> >
{workflowDisabledSource ? ( {workflowDisabledSource ? (
<WorkFlowDisabledMessage parentStateId={workflowDisabledSource} className="my-2" /> <WorkFlowDisabledOverlay
messageContainerRef={messageContainerRef}
workflowDisabledSource={workflowDisabledSource}
shouldOverlayBeVisible={shouldOverlayBeVisible}
/>
) : ( ) : (
<div <div
className={cn( className={cn("p-3 my-8 flex flex-col rounded items-center", {
"p-3 my-8 flex flex-col rounded items-center",
{
"text-custom-text-200": shouldOverlayBeVisible, "text-custom-text-200": shouldOverlayBeVisible,
},
{
"text-custom-text-error": isDropDisabled, "text-custom-text-error": isDropDisabled,
} })}
)}
> >
{dropErrorMessage ? ( {dropErrorMessage ? (
<div className="flex items-center"> <div className="flex items-center">

View file

@ -24,6 +24,7 @@ import { useKanbanView } from "@/hooks/store";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
// types // types
// parent components // parent components
import { useWorkFlowFDragNDrop } from "@/plane-web/components/workflow";
import { TRenderQuickActions } from "../list/list-view-types"; import { TRenderQuickActions } from "../list/list-view-types";
import { getGroupByColumns, isWorkspaceLevel, GroupDropLocation, getApproximateCardHeight } from "../utils"; import { getGroupByColumns, isWorkspaceLevel, GroupDropLocation, getApproximateCardHeight } from "../utils";
// components // components
@ -98,6 +99,9 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
const issueKanBanView = useKanbanView(); const issueKanBanView = useKanbanView();
// derived values // derived values
const isDragDisabled = !issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by); const isDragDisabled = !issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by);
const { getIsWorkflowWorkItemCreationDisabled } = useWorkFlowFDragNDrop(group_by, sub_group_by);
const list = getGroupByColumns({ const list = getGroupByColumns({
groupBy: group_by as GroupByColumnTypes, groupBy: group_by as GroupByColumnTypes,
includeNone: true, includeNone: true,
@ -167,7 +171,11 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
title={subList.name} title={subList.name}
count={getGroupIssueCount(subList.id, undefined, false) ?? 0} count={getGroupIssueCount(subList.id, undefined, false) ?? 0}
issuePayload={subList.payload} issuePayload={subList.payload}
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy} disableIssueCreation={
disableIssueCreation ||
isGroupByCreatedBy ||
getIsWorkflowWorkItemCreationDisabled(subList.id, sub_group_id)
}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
collapsedGroups={collapsedGroups} collapsedGroups={collapsedGroups}
handleCollapsedGroups={handleCollapsedGroups} handleCollapsedGroups={handleCollapsedGroups}

View file

@ -113,10 +113,8 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
); );
const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false); const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false);
const { workflowDisabledSource, isWorkflowDropDisabled, handleWorkFlowState } = useWorkFlowFDragNDrop( const { workflowDisabledSource, isWorkflowDropDisabled, handleWorkFlowState, getIsWorkflowWorkItemCreationDisabled } =
group_by, useWorkFlowFDragNDrop(group_by, sub_group_by);
sub_group_by
);
// Enable Kanban Columns as Drop Targets // Enable Kanban Columns as Drop Targets
useEffect(() => { useEffect(() => {
@ -306,7 +304,9 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
{shouldLoadMore && (isSubGroup ? <>{loadMore}</> : <KanbanIssueBlockLoader ref={setIntersectionElement} />)} {shouldLoadMore && (isSubGroup ? <>{loadMore}</> : <KanbanIssueBlockLoader ref={setIntersectionElement} />)}
{enableQuickIssueCreate && !disableIssueCreation && ( {enableQuickIssueCreate &&
!disableIssueCreation &&
!getIsWorkflowWorkItemCreationDisabled(groupId, sub_group_id) && (
<div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0"> <div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0">
<QuickAddIssueRoot <QuickAddIssueRoot
layout={EIssueLayoutTypes.KANBAN} layout={EIssueLayoutTypes.KANBAN}

View file

@ -1,6 +1,5 @@
import { MutableRefObject } from "react"; import { MutableRefObject } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import { import {
GroupByColumnTypes, GroupByColumnTypes,
IGroupByColumn, IGroupByColumn,
@ -18,6 +17,7 @@ import { Row } from "@plane/ui";
// hooks // hooks
import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
// components // components
import { useWorkFlowFDragNDrop } from "@/plane-web/components/workflow";
import { TRenderQuickActions } from "../list/list-view-types"; import { TRenderQuickActions } from "../list/list-view-types";
import { getGroupByColumns, isWorkspaceLevel, GroupDropLocation } from "../utils"; import { getGroupByColumns, isWorkspaceLevel, GroupDropLocation } from "../utils";
import { KanBan } from "./default"; import { KanBan } from "./default";
@ -53,7 +53,10 @@ const visibilitySubGroupByGroupCount = (subGroupIssueCount: number, showEmptyGro
}; };
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = observer( const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = observer(
({ getGroupIssueCount, sub_group_by, group_by, list, collapsedGroups, handleCollapsedGroups, showEmptyGroup }) => ( ({ getGroupIssueCount, sub_group_by, group_by, list, collapsedGroups, handleCollapsedGroups, showEmptyGroup }) => {
const { getIsWorkflowWorkItemCreationDisabled } = useWorkFlowFDragNDrop(group_by, sub_group_by);
return (
<div className="relative flex h-max min-h-full w-full items-center gap-4"> <div className="relative flex h-max min-h-full w-full items-center gap-4">
{list && {list &&
list.length > 0 && list.length > 0 &&
@ -63,7 +66,7 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = observer(
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup); const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup);
if (subGroupByVisibilityToggle === false) return <></>; if (subGroupByVisibilityToggle === false) return <></>;
const { t } = useTranslation();
return ( return (
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col"> <div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
<HeaderGroupByCard <HeaderGroupByCard
@ -76,12 +79,14 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = observer(
collapsedGroups={collapsedGroups} collapsedGroups={collapsedGroups}
handleCollapsedGroups={handleCollapsedGroups} handleCollapsedGroups={handleCollapsedGroups}
issuePayload={_list.payload} issuePayload={_list.payload}
/>{" "} disableIssueCreation={getIsWorkflowWorkItemCreationDisabled(_list.id)}
/>
</div> </div>
); );
})} })}
</div> </div>
) );
}
); );
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
@ -156,7 +161,6 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
{list && {list &&
list.length > 0 && list.length > 0 &&
list.map((_list: IGroupByColumn, subGroupIndex) => { list.map((_list: IGroupByColumn, subGroupIndex) => {
const { t } = useTranslation();
const issueCount = getGroupIssueCount(undefined, _list.id, true) ?? 0; const issueCount = getGroupIssueCount(undefined, _list.id, true) ?? 0;
const subGroupByVisibilityToggle = visibilitySubGroupBy(_list, issueCount); const subGroupByVisibilityToggle = visibilitySubGroupBy(_list, issueCount);
if (subGroupByVisibilityToggle.showGroup === false) return <></>; if (subGroupByVisibilityToggle.showGroup === false) return <></>;

View file

@ -110,7 +110,9 @@ export const ListGroup = observer((props: Props) => {
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null); const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null);
const { workflowDisabledSource, isWorkflowDropDisabled, handleWorkFlowState } = useWorkFlowFDragNDrop(group_by); const { workflowDisabledSource, isWorkflowDropDisabled, handleWorkFlowState, getIsWorkflowWorkItemCreationDisabled } =
useWorkFlowFDragNDrop(group_by);
const isWorkflowIssueCreationDisabled = getIsWorkflowWorkItemCreationDisabled(group.id);
const groupIssueCount = getGroupIssueCount(group.id, undefined, false) ?? 0; const groupIssueCount = getGroupIssueCount(group.id, undefined, false) ?? 0;
const nextPageResults = getPaginationData(group.id, undefined)?.nextPageResults; const nextPageResults = getPaginationData(group.id, undefined)?.nextPageResults;
@ -267,7 +269,9 @@ export const ListGroup = observer((props: Props) => {
count={groupIssueCount} count={groupIssueCount}
issuePayload={group.payload} issuePayload={group.payload}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy || isCompletedCycle} disableIssueCreation={
disableIssueCreation || isGroupByCreatedBy || isCompletedCycle || isWorkflowIssueCreationDisabled
}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
selectionHelpers={selectionHelpers} selectionHelpers={selectionHelpers}
handleCollapsedGroups={handleCollapsedGroups} handleCollapsedGroups={handleCollapsedGroups}
@ -305,7 +309,11 @@ export const ListGroup = observer((props: Props) => {
{shouldLoadMore && (group_by ? <>{loadMore}</> : <ListLoaderItemRow ref={setIntersectionElement} />)} {shouldLoadMore && (group_by ? <>{loadMore}</> : <ListLoaderItemRow ref={setIntersectionElement} />)}
{enableIssueQuickAdd && !disableIssueCreation && !isGroupByCreatedBy && !isCompletedCycle && ( {enableIssueQuickAdd &&
!disableIssueCreation &&
!isGroupByCreatedBy &&
!isCompletedCycle &&
!isWorkflowIssueCreationDisabled && (
<div className="sticky bottom-0 z-[1] w-full flex-shrink-0"> <div className="sticky bottom-0 z-[1] w-full flex-shrink-0">
<QuickAddIssueRoot <QuickAddIssueRoot
layout={EIssueLayoutTypes.LIST} layout={EIssueLayoutTypes.LIST}

View file

@ -99,6 +99,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
projectId={projectId ?? undefined} projectId={projectId ?? undefined}
buttonVariant="border-with-text" buttonVariant="border-with-text"
tabIndex={getIndex("state_id")} tabIndex={getIndex("state_id")}
isForWorkItemCreation={!id}
/> />
</div> </div>
)} )}

View file

@ -32,7 +32,7 @@ export const StateMarksAsDefault: FC<TStateMarksAsDefault> = observer((props) =>
return ( return (
<button <button
className={cn( className={cn(
"text-sm whitespace-nowrap transition-colors", "text-xs whitespace-nowrap transition-colors",
isDefault ? "text-custom-text-300" : "text-custom-text-200 hover:text-custom-text-100" isDefault ? "text-custom-text-300" : "text-custom-text-200 hover:text-custom-text-100"
)} )}
disabled={isDefault || isLoading} disabled={isDefault || isLoading}

View file

@ -16,16 +16,11 @@ 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, fetchProjectStateTransitions } = useProjectState(); const { groupedProjectStates, fetchProjectStates } = useProjectState();
useSWR( useSWR(
workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null, workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId.toString()) : null,
? () => {
fetchProjectStates(workspaceSlug.toString(), projectId.toString());
fetchProjectStateTransitions(workspaceSlug.toString(), projectId.toString());
}
: null,
{ revalidateIfStale: false, revalidateOnFocus: false } { revalidateIfStale: false, revalidateOnFocus: false }
); );

View file

@ -1,13 +1,10 @@
import { SetStateAction } from "react"; 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 // plane imports
import { IState } from "@plane/types"; import { IState } from "@plane/types";
import { StateGroupIcon } from "@plane/ui"; import { StateGroupIcon } from "@plane/ui";
// Plane-web // local imports
import { StateTransitionCount } from "@/plane-web/components/workflow";
import { IStateWorkFlow } from "@/plane-web/types";
//
import { StateDelete, StateMarksAsDefault } from "./options"; import { StateDelete, StateMarksAsDefault } from "./options";
export type StateItemTitleProps = { export type StateItemTitleProps = {
@ -17,31 +14,37 @@ export type StateItemTitleProps = {
stateCount: number; stateCount: number;
disabled: boolean; disabled: boolean;
state: IState; state: IState;
currentTransitionMap?: IStateWorkFlow; shouldShowDescription?: boolean;
}; };
export const StateItemTitle = observer((props: StateItemTitleProps) => { export const StateItemTitle = observer((props: StateItemTitleProps) => {
const { workspaceSlug, projectId, stateCount, setUpdateStateModal, disabled, state, currentTransitionMap } = props; const {
workspaceSlug,
projectId,
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-2"> <div className="flex items-center gap-1 px-1">
{/* draggable indicator */} {/* draggable indicator */}
{!disabled && stateCount != 1 && ( {!disabled && stateCount != 1 && (
<div className="flex-shrink-0 w-3 h-3 rounded-sm absolute left-0 hidden group-hover:flex justify-center items-center transition-colors bg-custom-background-90 cursor-pointer text-custom-text-200 hover:text-custom-text-100"> <div className="flex-shrink-0 w-3 h-3 rounded-sm absolute -left-1.5 hidden group-hover:flex justify-center items-center transition-colors bg-custom-background-90 cursor-pointer text-custom-text-200 hover:text-custom-text-100">
<GripVertical className="w-3 h-3" /> <GripVertical className="w-3 h-3" />
</div> </div>
)} )}
{/* state icon */} {/* state icon */}
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" /> <StateGroupIcon stateGroup={state.group} color={state.color} className={"size-3.5"} />
</div> </div>
{/* state title and description */} {/* state title and description */}
<div className="text-sm px-2 min-h-5"> <div className="text-sm px-2 min-h-5">
<h6 className="text-sm font-medium">{state.name}</h6> <h6 className="text-sm font-medium">{state.name}</h6>
<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>
{/* Transition count */}
<StateTransitionCount currentTransitionMap={currentTransitionMap} />
</div> </div>
{!disabled && ( {!disabled && (

View file

@ -10,14 +10,12 @@ import { TDraggableData } from "@plane/constants";
import { IState, TStateGroups } from "@plane/types"; import { IState, TStateGroups } from "@plane/types";
import { DropIndicator } from "@plane/ui"; import { DropIndicator } from "@plane/ui";
// components // components
import { 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 // hooks
import { useProjectState } from "@/hooks/store"; import { useProjectState } from "@/hooks/store";
// Plane-web
import { StateItemChild } from "@/plane-web/components/workflow";
type TStateItem = { type TStateItem = {
workspaceSlug: string; workspaceSlug: string;
@ -133,7 +131,7 @@ export const StateItem: FC<TStateItem> = observer((props) => {
totalStates === 1 ? `cursor-auto` : `cursor-grab` totalStates === 1 ? `cursor-auto` : `cursor-grab`
)} )}
> >
<StateItemChild <StateItemTitle
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
setUpdateStateModal={setUpdateStateModal} setUpdateStateModal={setUpdateStateModal}

View file

@ -1,5 +1,8 @@
import useSWR from "swr"; import useSWR from "swr";
import { useCycle, useProjectEstimates, useLabel, useModule, useProjectState } from "./store"; // plane web imports
import { useWorkspaceIssuePropertiesExtended } from "@/plane-web/hooks/use-workspace-issue-properties-extended";
// plane imports
import { useCycle, useProjectEstimates, useLabel, useModule } from "./store";
export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | undefined) => { export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | undefined) => {
const { fetchWorkspaceLabels } = useLabel(); const { fetchWorkspaceLabels } = useLabel();
@ -37,4 +40,7 @@ export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | u
workspaceSlug ? () => getWorkspaceEstimates(workspaceSlug.toString()) : null, workspaceSlug ? () => getWorkspaceEstimates(workspaceSlug.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false } { revalidateIfStale: false, revalidateOnFocus: false }
); );
// fetch extended issue properties
useWorkspaceIssuePropertiesExtended(workspaceSlug);
}; };

View file

@ -54,7 +54,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
const { const {
project: { fetchProjectMembers }, project: { fetchProjectMembers },
} = useMember(); } = useMember();
const { fetchProjectStates, fetchProjectStateTransitions } = useProjectState(); const { fetchProjectStates } = useProjectState();
const { fetchProjectLabels } = useLabel(); const { fetchProjectLabels } = useLabel();
const { getProjectEstimates } = useProjectEstimates(); const { getProjectEstimates } = useProjectEstimates();
@ -118,12 +118,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
// fetching project states // fetching project states
useSWR( useSWR(
workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null, workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId.toString()) : null,
? () => {
fetchProjectStates(workspaceSlug.toString(), projectId.toString());
fetchProjectStateTransitions(workspaceSlug.toString(), projectId.toString());
}
: null,
{ revalidateIfStale: false, revalidateOnFocus: false } { revalidateIfStale: false, revalidateOnFocus: false }
); );
// fetching project estimates // fetching project estimates

View file

@ -6,7 +6,6 @@ import { computedFn } from "mobx-utils";
import { STATE_GROUPS } from "@plane/constants"; import { STATE_GROUPS } from "@plane/constants";
import { IState } from "@plane/types"; import { IState } from "@plane/types";
// helpers // helpers
import { convertStringArrayToBooleanObject } from "@/helpers/array.helper";
import { sortStates } from "@/helpers/state.helper"; import { sortStates } from "@/helpers/state.helper";
// plane web // plane web
import { syncIssuesWithDeletedStates } from "@/local-db/utils/load-workspace"; import { syncIssuesWithDeletedStates } from "@/local-db/utils/load-workspace";
@ -25,10 +24,6 @@ export interface IStateStore {
// computed actions // computed actions
getStateById: (stateId: string | null | undefined) => IState | undefined; getStateById: (stateId: string | null | undefined) => IState | undefined;
getProjectStates: (projectId: string | null | undefined) => IState[] | undefined; getProjectStates: (projectId: string | null | undefined) => IState[] | undefined;
getAvailableProjectStateIdMap: (
projectId: string | null | undefined,
currStateId: string | null | undefined
) => { [key: string]: boolean };
// fetch actions // fetch actions
fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise<IState[]>; fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise<IState[]>;
fetchWorkspaceStates: (workspaceSlug: string) => Promise<IState[]>; fetchWorkspaceStates: (workspaceSlug: string) => Promise<IState[]>;
@ -48,8 +43,6 @@ export interface IStateStore {
stateId: string, stateId: string,
payload: Partial<IState> payload: Partial<IState>
) => Promise<void>; ) => Promise<void>;
//Dummy method
fetchProjectStateTransitions: (workspaceSlug: string, projectId: string) => void;
} }
export class StateStore implements IStateStore { export class StateStore implements IStateStore {
@ -143,20 +136,6 @@ export class StateStore implements IStateStore {
return sortStates(Object.values(this.stateMap).filter((state) => state.project_id === projectId)); return sortStates(Object.values(this.stateMap).filter((state) => state.project_id === projectId));
}); });
/**
* Returns an object linking state permissions as boolean values
* @param projectId
*/
getAvailableProjectStateIdMap = computedFn(
(projectId: string | null | undefined, currStateId: string | null | undefined) => {
const projectStates = this.getProjectStates(projectId);
if (!projectStates) return {};
return convertStringArrayToBooleanObject(projectStates.map((projectState) => projectState.id));
}
);
/** /**
* fetches the stateMap of a project * fetches the stateMap of a project
* @param workspaceSlug * @param workspaceSlug
@ -292,14 +271,11 @@ export class StateStore implements IStateStore {
}); });
// updating using api // updating using api
await this.stateService.patchState(workspaceSlug, projectId, stateId, payload); await this.stateService.patchState(workspaceSlug, projectId, stateId, payload);
} catch (err) { } catch {
// reverting back to old state group if api fails // reverting back to old state group if api fails
runInAction(() => { runInAction(() => {
this.stateMap = originalStates; this.stateMap = originalStates;
}); });
} }
}; };
// Dummy method
fetchProjectStateTransitions = (workspaceSlug: string, projectId: string) => {};
} }

View file

@ -1,3 +1,2 @@
export * from "./projects"; export * from "./projects";
export * from "./issue-types"; export * from "./issue-types";
export * from "ce/types/state.d";

View file

@ -37,3 +37,13 @@
[cmdk-item][aria-selected="true"] { [cmdk-item][aria-selected="true"] {
background-color: rgba(var(--color-background-80)); background-color: rgba(var(--color-background-80));
} }
[cmdk-item][aria-disabled="true"], [cmdk-item][data-disabled="true"] {
cursor: not-allowed;
opacity: 0.5;
background-color: transparent;
}
[cmdk-item][aria-disabled="true"]:hover, [cmdk-item][data-disabled="true"]:hover {
background-color: transparent;
}