chore: update issue-states settings ui (#6338)
This commit is contained in:
parent
6914dc9f42
commit
f4c4848a0d
5 changed files with 135 additions and 67 deletions
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue