Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-2226]chore: updated 'Issue states' settings ui #6338

Merged
merged 1 commit into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 36 additions & 34 deletions web/core/components/project-states/create-update/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down Expand Up @@ -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
/>

{/* 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"
/>
<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
/>

<Button type="button" variant="neutral-primary" size="sm" disabled={buttonDisabled} onClick={onCancel}>
Cancel
</Button>
{/* 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="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>
);
};
102 changes: 73 additions & 29 deletions web/core/components/project-states/group-item.tsx
Original file line number Diff line number Diff line change
@@ -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>
Comment on lines +54 to +80
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add ARIA attributes for accessibility.

The expandable section should have proper ARIA attributes for better accessibility.

 <div
   className="flex justify-between items-center gap-2"
+  role="button"
+  aria-expanded={currentStateExpanded}
+  aria-controls={`group-${groupKey}-content`}
 >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<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
className="flex justify-between items-center gap-2"
role="button"
aria-expanded={currentStateExpanded}
aria-controls={`group-${groupKey}-content`}
>
<div
className="w-full flex items-center cursor-pointer py-1"
onClick={() => (!currentStateExpanded ? handleExpand(groupKey) : handleGroupCollapse(groupKey))}
>
<div
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,
}
)}
>
<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>
);
});
24 changes: 23 additions & 1 deletion web/core/components/project-states/group-list.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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]) => {
Expand All @@ -28,6 +47,9 @@ export const GroupList: FC<TGroupList> = observer((props) => {
groupKey={groupKey}
states={groupStates}
groupedStates={groupedStates}
groupsExpanded={groupsExpanded}
handleGroupCollapse={handleGroupCollapse}
handleExpand={handleExpand}
/>
);
})}
Expand Down
2 changes: 1 addition & 1 deletion web/core/components/project-states/state-item-title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 && (
Expand Down
4 changes: 2 additions & 2 deletions web/core/components/project-states/state-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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`
)}
Expand Down
Loading