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",
"installing": "Installing",
"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",
"categories": "Categories",
"saving": "Saving",

View file

@ -215,6 +215,7 @@
"activity": "Actividad",
"appearance": "Apariencia",
"notifications": "Notificaciones",
"connections": "Conexiones",
"workspaces": "Espacios de trabajo",
"create_workspace": "Crear espacio de trabajo",
"invitations": "Invitaciones",
@ -291,6 +292,7 @@
"workspace_logo": "Logo del espacio de trabajo",
"new_issue": "Nuevo elemento de trabajo",
"your_work": "Tu trabajo",
"workspace_dashboards": "Paneles de control",
"drafts": "Borradores",
"projects": "Proyectos",
"views": "Vistas",
@ -845,6 +847,16 @@
"disconnecting": "Desconectando",
"installing": "Instalando",
"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",
"categories": "Categorías",
"saving": "Guardando",

View file

@ -845,6 +845,16 @@
"disconnecting": "Déconnexion",
"installing": "Installation",
"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",
"categories": "Catégories",
"saving": "Enregistrement",

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from "react";
import { Tooltip2 } from "@blueprintjs/popover2";
import React, { useEffect, useRef, useState } from "react";
// helpers
import { cn } from "../../helpers";
@ -23,7 +23,6 @@ export type TPosition =
interface ITooltipProps {
tooltipHeading?: string;
tooltipContent: string | React.ReactNode;
jsxContent?: string | React.ReactNode;
position?: TPosition;
children: JSX.Element;
disabled?: boolean;
@ -39,14 +38,13 @@ export const Tooltip: React.FC<ITooltipProps> = ({
tooltipContent,
position = "top",
children,
jsxContent,
disabled = false,
className = "",
openDelay = 200,
closeDelay,
isMobile = false,
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 [shouldRender, setShouldRender] = useState(renderByDefault);
@ -81,22 +79,18 @@ export const Tooltip: React.FC<ITooltipProps> = ({
hoverOpenDelay={openDelay}
hoverCloseDelay={closeDelay}
content={
jsxContent ? (
<>{jsxContent}</>
) : (
<div
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",
{
hidden: isMobile,
},
className
)}
>
{tooltipHeading && <h5 className="font-medium text-custom-text-100">{tooltipHeading}</h5>}
{tooltipContent}
</div>
)
<div
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",
{
hidden: isMobile,
},
className
)}
>
{tooltipHeading && <h5 className="font-medium text-custom-text-100">{tooltipHeading}</h5>}
{tooltipContent}
</div>
}
position={position}
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-item-child";
export * from "./state-transition-count";
export * from "./use-workflow-drag-n-drop";
export * from "./workflow-disabled-message";
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;
selectedValue: string | null | undefined;
className?: string;
isForWorkItemCreation?: boolean;
};
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";
export const useWorkFlowFDragNDrop = (
@ -6,6 +7,7 @@ export const useWorkFlowFDragNDrop = (
) => ({
workflowDisabledSource: undefined,
isWorkflowDropDisabled: false,
getIsWorkflowWorkItemCreationDisabled: (groupId: string, subGroupId?: string) => false,
handleWorkFlowState: (
sourceGroupId: string,
destinationGroupId: string,

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
type Props = {
parentStateId: 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";
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 "./issue-types";
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";
import { Command } from "cmdk";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// hooks
import { Check } from "lucide-react";
// plane imports
import { TIssue } from "@plane/types";
import { Spinner, StateGroupIcon } from "@plane/ui";
import { useProjectState, useIssueDetail } from "@/hooks/store";
// ui
// icons
// types
// store hooks
import { useIssueDetail } from "@/hooks/store";
// plane web imports
import { ChangeWorkItemStateList } from "@/plane-web/components/command-palette/actions/work-item-actions";
type Props = { closePalette: () => void; issue: TIssue };
@ -20,10 +17,9 @@ export const ChangeIssueState: React.FC<Props> = observer((props) => {
const { workspaceSlug } = useParams();
// store hooks
const { updateIssue } = useIssueDetail();
const { getProjectStates } = useProjectState();
// derived values
const projectId = issue?.project_id;
const projectStates = getProjectStates(projectId);
const currentStateId = issue?.state_id;
const submitChanges = async (formData: Partial<TIssue>) => {
if (!workspaceSlug || !projectId || !issue) return;
@ -40,24 +36,10 @@ export const ChangeIssueState: React.FC<Props> = observer((props) => {
};
return (
<>
{projectStates ? (
projectStates.length > 0 ? (
projectStates.map((state) => (
<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 />
)}
</>
<ChangeWorkItemStateList
projectId={projectId}
currentStateId={currentStateId}
handleStateChange={handleIssueState}
/>
);
});

View file

@ -29,15 +29,17 @@ export const ActivityBlockComponent: FC<TActivityBlockComponent> = (props) => {
if (!activity) return <></>;
return (
<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`
}`}
>
<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" />}
</div>
<div className="w-full truncate text-custom-text-200">
<User activity={activity} customUserName={customUserName} /> {children}
<div className="w-full text-custom-text-200">
<div className="line-clamp-2">
<User activity={activity} customUserName={customUserName} /> {children}
</div>
<div className="mt-1">
<Tooltip
isMobile={isMobile}

View file

@ -35,6 +35,7 @@ type Props = TDropdownProps & {
renderByDefault?: boolean;
stateIds?: string[];
filterAvailableStateIds?: boolean;
isForWorkItemCreation?: boolean;
};
export const StateDropdown: React.FC<Props> = observer((props) => {
@ -59,6 +60,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
renderByDefault = true,
stateIds,
filterAvailableStateIds = true,
isForWorkItemCreation = false,
} = props;
// states
const [query, setQuery] = useState("");
@ -98,7 +100,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
content: (
<div className="flex items-center gap-2">
<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>
),
}));
@ -182,7 +184,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
/>
)}
{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 && (
<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}
selectedValue={value}
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";
// Plane
// plane imports
import { ISSUE_ORDER_BY_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssueOrderByOptions } from "@plane/types";
// helpers
import { cn } from "@/helpers/common.helper";
// Plane-web
import { WorkFlowDisabledMessage } from "@/plane-web/components/workflow";
// plane web imports
import { WorkFlowDisabledOverlay } from "@/plane-web/components/workflow";
type Props = {
dragColumnOrientation: "justify-start" | "justify-center" | "justify-end";
@ -30,9 +31,10 @@ export const GroupDragOverlay = (props: Props) => {
isDraggingOverColumn,
isEpic = false,
} = props;
// hooks
const { t } = useTranslation();
// refs
const messageContainerRef = useRef<HTMLDivElement>(null);
const shouldOverlayBeVisible = isDraggingOverColumn && canOverlayBeVisible;
const readableOrderBy = t(
@ -41,27 +43,28 @@ export const GroupDragOverlay = (props: Props) => {
return (
<div
ref={messageContainerRef}
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,
"bg-red-200/60": workflowDisabledSource && isDropDisabled,
},
{ hidden: !shouldOverlayBeVisible }
)}
>
{workflowDisabledSource ? (
<WorkFlowDisabledMessage parentStateId={workflowDisabledSource} className="my-2" />
<WorkFlowDisabledOverlay
messageContainerRef={messageContainerRef}
workflowDisabledSource={workflowDisabledSource}
shouldOverlayBeVisible={shouldOverlayBeVisible}
/>
) : (
<div
className={cn(
"p-3 my-8 flex flex-col rounded items-center",
{
"text-custom-text-200": shouldOverlayBeVisible,
},
{
"text-custom-text-error": isDropDisabled,
}
)}
className={cn("p-3 my-8 flex flex-col rounded items-center", {
"text-custom-text-200": shouldOverlayBeVisible,
"text-custom-text-error": isDropDisabled,
})}
>
{dropErrorMessage ? (
<div className="flex items-center">

View file

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

View file

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

View file

@ -1,6 +1,5 @@
import { MutableRefObject } from "react";
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import {
GroupByColumnTypes,
IGroupByColumn,
@ -18,6 +17,7 @@ import { Row } from "@plane/ui";
// hooks
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
// components
import { useWorkFlowFDragNDrop } from "@/plane-web/components/workflow";
import { TRenderQuickActions } from "../list/list-view-types";
import { getGroupByColumns, isWorkspaceLevel, GroupDropLocation } from "../utils";
import { KanBan } from "./default";
@ -53,35 +53,40 @@ const visibilitySubGroupByGroupCount = (subGroupIssueCount: number, showEmptyGro
};
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = observer(
({ getGroupIssueCount, sub_group_by, group_by, list, collapsedGroups, handleCollapsedGroups, showEmptyGroup }) => (
<div className="relative flex h-max min-h-full w-full items-center gap-4">
{list &&
list.length > 0 &&
list.map((_list: IGroupByColumn) => {
const groupCount = getGroupIssueCount(_list?.id, undefined, false) ?? 0;
({ getGroupIssueCount, sub_group_by, group_by, list, collapsedGroups, handleCollapsedGroups, showEmptyGroup }) => {
const { getIsWorkflowWorkItemCreationDisabled } = useWorkFlowFDragNDrop(group_by, sub_group_by);
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup);
return (
<div className="relative flex h-max min-h-full w-full items-center gap-4">
{list &&
list.length > 0 &&
list.map((_list: IGroupByColumn) => {
const groupCount = getGroupIssueCount(_list?.id, undefined, false) ?? 0;
if (subGroupByVisibilityToggle === false) return <></>;
const { t } = useTranslation();
return (
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={_list.id}
icon={_list.icon}
title={_list.name}
count={groupCount}
collapsedGroups={collapsedGroups}
handleCollapsedGroups={handleCollapsedGroups}
issuePayload={_list.payload}
/>{" "}
</div>
);
})}
</div>
)
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup);
if (subGroupByVisibilityToggle === false) return <></>;
return (
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={_list.id}
icon={_list.icon}
title={_list.name}
count={groupCount}
collapsedGroups={collapsedGroups}
handleCollapsedGroups={handleCollapsedGroups}
issuePayload={_list.payload}
disableIssueCreation={getIsWorkflowWorkItemCreationDisabled(_list.id)}
/>
</div>
);
})}
</div>
);
}
);
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
@ -156,7 +161,6 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
{list &&
list.length > 0 &&
list.map((_list: IGroupByColumn, subGroupIndex) => {
const { t } = useTranslation();
const issueCount = getGroupIssueCount(undefined, _list.id, true) ?? 0;
const subGroupByVisibilityToggle = visibilitySubGroupBy(_list, issueCount);
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 { 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 nextPageResults = getPaginationData(group.id, undefined)?.nextPageResults;
@ -267,7 +269,9 @@ export const ListGroup = observer((props: Props) => {
count={groupIssueCount}
issuePayload={group.payload}
canEditProperties={canEditProperties}
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy || isCompletedCycle}
disableIssueCreation={
disableIssueCreation || isGroupByCreatedBy || isCompletedCycle || isWorkflowIssueCreationDisabled
}
addIssuesToView={addIssuesToView}
selectionHelpers={selectionHelpers}
handleCollapsedGroups={handleCollapsedGroups}
@ -305,18 +309,22 @@ export const ListGroup = observer((props: Props) => {
{shouldLoadMore && (group_by ? <>{loadMore}</> : <ListLoaderItemRow ref={setIntersectionElement} />)}
{enableIssueQuickAdd && !disableIssueCreation && !isGroupByCreatedBy && !isCompletedCycle && (
<div className="sticky bottom-0 z-[1] w-full flex-shrink-0">
<QuickAddIssueRoot
layout={EIssueLayoutTypes.LIST}
QuickAddButton={ListQuickAddIssueButton}
prePopulatedData={prePopulateQuickAddData(group_by, group.id)}
containerClassName="border-b border-t border-custom-border-200 bg-custom-background-100 "
quickAddCallback={quickAddCallback}
isEpic={isEpic}
/>
</div>
)}
{enableIssueQuickAdd &&
!disableIssueCreation &&
!isGroupByCreatedBy &&
!isCompletedCycle &&
!isWorkflowIssueCreationDisabled && (
<div className="sticky bottom-0 z-[1] w-full flex-shrink-0">
<QuickAddIssueRoot
layout={EIssueLayoutTypes.LIST}
QuickAddButton={ListQuickAddIssueButton}
prePopulatedData={prePopulateQuickAddData(group_by, group.id)}
containerClassName="border-b border-t border-custom-border-200 bg-custom-background-100 "
quickAddCallback={quickAddCallback}
isEpic={isEpic}
/>
</div>
)}
</div>
)}
</div>

View file

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

View file

@ -32,7 +32,7 @@ export const StateMarksAsDefault: FC<TStateMarksAsDefault> = observer((props) =>
return (
<button
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"
)}
disabled={isDefault || isLoading}

View file

@ -16,16 +16,11 @@ type TProjectState = {
export const ProjectStateRoot: FC<TProjectState> = observer((props) => {
const { workspaceSlug, projectId } = props;
// hooks
const { groupedProjectStates, fetchProjectStates, fetchProjectStateTransitions } = useProjectState();
const { groupedProjectStates, fetchProjectStates } = useProjectState();
useSWR(
workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId
? () => {
fetchProjectStates(workspaceSlug.toString(), projectId.toString());
fetchProjectStateTransitions(workspaceSlug.toString(), projectId.toString());
}
: null,
workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);

View file

@ -1,13 +1,10 @@
import { SetStateAction } from "react";
import { observer } from "mobx-react";
import { GripVertical, Pencil } from "lucide-react";
// Plane
// plane imports
import { IState } from "@plane/types";
import { StateGroupIcon } from "@plane/ui";
// Plane-web
import { StateTransitionCount } from "@/plane-web/components/workflow";
import { IStateWorkFlow } from "@/plane-web/types";
//
// local imports
import { StateDelete, StateMarksAsDefault } from "./options";
export type StateItemTitleProps = {
@ -17,31 +14,37 @@ export type StateItemTitleProps = {
stateCount: number;
disabled: boolean;
state: IState;
currentTransitionMap?: IStateWorkFlow;
shouldShowDescription?: boolean;
};
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 (
<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 */}
{!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" />
</div>
)}
{/* state icon */}
<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>
{/* state title and description */}
<div className="text-sm px-2 min-h-5">
<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>
{/* Transition count */}
<StateTransitionCount currentTransitionMap={currentTransitionMap} />
</div>
{!disabled && (

View file

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

View file

@ -1,5 +1,8 @@
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) => {
const { fetchWorkspaceLabels } = useLabel();
@ -37,4 +40,7 @@ export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | u
workspaceSlug ? () => getWorkspaceEstimates(workspaceSlug.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetch extended issue properties
useWorkspaceIssuePropertiesExtended(workspaceSlug);
};

View file

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

View file

@ -6,7 +6,6 @@ import { computedFn } from "mobx-utils";
import { STATE_GROUPS } from "@plane/constants";
import { IState } from "@plane/types";
// helpers
import { convertStringArrayToBooleanObject } from "@/helpers/array.helper";
import { sortStates } from "@/helpers/state.helper";
// plane web
import { syncIssuesWithDeletedStates } from "@/local-db/utils/load-workspace";
@ -25,10 +24,6 @@ export interface IStateStore {
// computed actions
getStateById: (stateId: 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
fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise<IState[]>;
fetchWorkspaceStates: (workspaceSlug: string) => Promise<IState[]>;
@ -48,8 +43,6 @@ export interface IStateStore {
stateId: string,
payload: Partial<IState>
) => Promise<void>;
//Dummy method
fetchProjectStateTransitions: (workspaceSlug: string, projectId: string) => void;
}
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));
});
/**
* 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
* @param workspaceSlug
@ -292,14 +271,11 @@ export class StateStore implements IStateStore {
});
// updating using api
await this.stateService.patchState(workspaceSlug, projectId, stateId, payload);
} catch (err) {
} catch {
// reverting back to old state group if api fails
runInAction(() => {
this.stateMap = originalStates;
});
}
};
// Dummy method
fetchProjectStateTransitions = (workspaceSlug: string, projectId: string) => {};
}

View file

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

View file

@ -37,3 +37,13 @@
[cmdk-item][aria-selected="true"] {
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;
}