chore: update issue-states settings ui (#6338)

This commit is contained in:
Vamsi Krishna 2025-01-07 17:16:42 +05:30 committed by GitHub
parent 6914dc9f42
commit f4c4848a0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 135 additions and 67 deletions

View file

@ -3,7 +3,7 @@
import { FormEvent, FC, useEffect, useState, useMemo } from "react";
import { TwitterPicker } from "react-color";
import { IState } from "@plane/types";
import { Button, Popover, Input } from "@plane/ui";
import { Button, Popover, Input, TextArea } from "@plane/ui";
type TStateForm = {
data: Partial<IState>;
@ -59,47 +59,49 @@ export const StateForm: FC<TStateForm> = (props) => {
);
return (
<form onSubmit={formSubmit} className="relative flex items-center gap-2">
<form onSubmit={formSubmit} className="relative flex space-x-2 bg-custom-background-100 p-3 rounded">
{/* color */}
<div className="flex-shrink-0">
<div className="flex-shrink-0 h-full mt-2">
<Popover button={PopoverButton} panelClassName="mt-4 -ml-3">
<TwitterPicker color={formData?.color} onChange={(value) => handleFormData("color", value.hex)} />
</Popover>
</div>
{/* title */}
<Input
id="name"
type="text"
name="name"
placeholder="Name"
value={formData?.name}
onChange={(e) => handleFormData("name", e.target.value)}
hasError={(errors && Boolean(errors.name)) || false}
className="w-full"
maxLength={100}
autoFocus
/>
<div className="w-full space-y-2">
{/* title */}
<Input
id="name"
type="text"
name="name"
placeholder="Name"
value={formData?.name}
onChange={(e) => handleFormData("name", e.target.value)}
hasError={(errors && Boolean(errors.name)) || false}
className="w-full"
maxLength={100}
autoFocus
/>
{/* description */}
<Input
id="description"
type="text"
name="description"
placeholder="Description"
value={formData?.description}
onChange={(e) => handleFormData("description", e.target.value)}
hasError={(errors && Boolean(errors.description)) || false}
className="w-full"
/>
{/* description */}
<TextArea
id="description"
name="description"
placeholder="Describe this state for your members."
value={formData?.description}
onChange={(e) => handleFormData("description", e.target.value)}
hasError={(errors && Boolean(errors.description)) || false}
className="w-full text-sm min-h-14 resize-none"
/>
<Button type="button" variant="neutral-primary" size="sm" disabled={buttonDisabled} onClick={onCancel}>
Cancel
</Button>
<Button type="submit" variant="primary" size="sm" disabled={buttonDisabled}>
{buttonTitle}
</Button>
<div className="flex space-x-2 items-center">
<Button type="submit" variant="primary" size="sm" disabled={buttonDisabled}>
{buttonTitle}
</Button>
<Button type="button" variant="neutral-primary" size="sm" disabled={buttonDisabled} onClick={onCancel}>
Cancel
</Button>
</div>
</div>
</form>
);
};

View file

@ -1,64 +1,108 @@
"use client";
import { FC, useState } from "react";
import { FC, useState, useRef } from "react";
import { observer } from "mobx-react";
import { Plus } from "lucide-react";
import { ChevronDown, Plus } from "lucide-react";
import { IState, TStateGroups } from "@plane/types";
// components
import { StateGroupIcon } from "@plane/ui";
import { cn } from "@plane/utils";
import { StateList, StateCreate } from "@/components/project-states";
// hooks
import { useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
type TGroupItem = {
workspaceSlug: string;
projectId: string;
groupKey: TStateGroups;
groupsExpanded: Partial<TStateGroups>[];
handleGroupCollapse: (groupKey: TStateGroups) => void;
handleExpand: (groupKey: TStateGroups) => void;
groupedStates: Record<string, IState[]>;
states: IState[];
};
export const GroupItem: FC<TGroupItem> = observer((props) => {
const { workspaceSlug, projectId, groupKey, groupedStates, states } = props;
const {
workspaceSlug,
projectId,
groupKey,
groupedStates,
states,
groupsExpanded,
handleExpand,
handleGroupCollapse,
} = props;
// store hooks
const { allowPermissions } = useUserPermissions();
// state
const [createState, setCreateState] = useState(false);
// derived values
const currentStateExpanded = groupsExpanded.includes(groupKey);
// refs
const dropElementRef = useRef<HTMLDivElement | null>(null);
const isEditable = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
return (
<div className="space-y-3">
<div className="flex justify-between items-center">
<div className="text-base font-medium text-custom-text-200 capitalize">{groupKey}</div>
{isEditable && (
<div
className="space-y-1 border border-custom-border-200 rounded bg-custom-background-90 transition-all p-2"
ref={dropElementRef}
>
<div className="flex justify-between items-center gap-2">
<div
className="w-full flex items-center cursor-pointer py-1"
onClick={() => (!currentStateExpanded ? handleExpand(groupKey) : handleGroupCollapse(groupKey))}
>
<div
className="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"
onClick={() => !createState && setCreateState(true)}
className={cn(
"flex-shrink-0 w-5 h-5 rounded flex justify-center items-center overflow-hidden transition-all",
{
"rotate-0": currentStateExpanded,
"-rotate-90": !currentStateExpanded,
}
)}
>
<Plus className="w-4 h-4" />
<ChevronDown className="w-4 h-4" />
</div>
)}
<div className="flex-shrink-0 w-6 h-6 rounded flex justify-center items-center overflow-hidden">
<StateGroupIcon stateGroup={groupKey} height="16px" width="16px" />
</div>
<div className="text-base font-medium text-custom-text-200 capitalize px-1">{groupKey}</div>
</div>
<div
className="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"
onClick={() => !createState && setCreateState(true)}
>
<Plus className="w-4 h-4" />
</div>
</div>
{isEditable && createState && (
<StateCreate
workspaceSlug={workspaceSlug}
projectId={projectId}
groupKey={groupKey}
handleClose={() => setCreateState(false)}
/>
{groupedStates[groupKey].length > 0 && currentStateExpanded && (
<div id="group-droppable-container">
<StateList
workspaceSlug={workspaceSlug}
projectId={projectId}
groupKey={groupKey}
groupedStates={groupedStates}
states={states}
disabled={!isEditable}
/>
</div>
)}
<div id="group-droppable-container">
<StateList
workspaceSlug={workspaceSlug}
projectId={projectId}
groupKey={groupKey}
groupedStates={groupedStates}
states={states}
disabled={!isEditable}
/>
</div>
{isEditable && createState && (
<div className="">
<StateCreate
workspaceSlug={workspaceSlug}
projectId={projectId}
groupKey={groupKey}
handleClose={() => setCreateState(false)}
/>
</div>
)}
</div>
);
});

View file

@ -1,6 +1,6 @@
"use client";
import { FC } from "react";
import { FC, useState } from "react";
import { observer } from "mobx-react";
import { IState, TStateGroups } from "@plane/types";
// components
@ -14,7 +14,26 @@ type TGroupList = {
export const GroupList: FC<TGroupList> = observer((props) => {
const { workspaceSlug, projectId, groupedStates } = props;
// states
const [groupsExpanded, setGroupsExpanded] = useState<Partial<TStateGroups>[]>([]);
const handleGroupCollapse = (groupKey: TStateGroups) => {
setGroupsExpanded((prev) => {
if (prev.includes(groupKey)) {
return prev.filter((key) => key !== groupKey);
}
return prev;
});
};
const handleExpand = (groupKey: TStateGroups) => {
setGroupsExpanded((prev) => {
if (prev.includes(groupKey)) {
return prev;
}
return [...prev, groupKey];
});
};
return (
<div className="space-y-5">
{Object.entries(groupedStates).map(([key, value]) => {
@ -28,6 +47,9 @@ export const GroupList: FC<TGroupList> = observer((props) => {
groupKey={groupKey}
states={groupStates}
groupedStates={groupedStates}
groupsExpanded={groupsExpanded}
handleGroupCollapse={handleGroupCollapse}
handleExpand={handleExpand}
/>
);
})}

View file

@ -23,7 +23,7 @@ export type StateItemTitleProps = {
export const StateItemTitle = observer((props: StateItemTitleProps) => {
const { workspaceSlug, projectId, stateCount, setUpdateStateModal, disabled, state, currentTransitionMap } = props;
return (
<div className="py-4 px-2 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">
{/* draggable indicator */}
{!disabled && stateCount != 1 && (

View file

@ -107,7 +107,7 @@ export const StateItem: FC<TStateItem> = observer((props) => {
})
);
}
}, [draggableElementRef, state, groupKey, isDraggable, groupedStates, handleStateSequence]);
}, [draggableElementRef, state, groupKey, isDraggable, groupedStates, handleStateSequence, disabled]);
// DND ends
if (updateStateModal)
@ -128,7 +128,7 @@ export const StateItem: FC<TStateItem> = observer((props) => {
<div
ref={draggableElementRef}
className={cn(
"relative border border-custom-border-100 rounded group",
"relative border border-custom-border-100 bg-custom-background-100 py-3 px-3.5 rounded group",
isDragging ? `opacity-50` : `opacity-100`,
totalStates === 1 ? `cursor-auto` : `cursor-grab`
)}