forked from github/plane
63b6150b9c
* fix: Labels reordering inconsistency * fix: Delete child labels * feat: multi-select while grouping labels * refactor: label sorting in mobx computed function * feat: drag & drop label grouping, un-grouping * chore: removed label select modal * fix: moving labels from project store to project label store * fix: typo changes and build tree function added * labels feature * disable dropping group into a group * fix build errors * fix more issues * chore: added combining state UI, fixed scroll issue for label groups * chore: group icon for label groups * fix: group cannot be dropped in another group --------- Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> Co-authored-by: rahulramesha <rahulramesham@gmail.com> Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
229 lines
8.2 KiB
TypeScript
229 lines
8.2 KiB
TypeScript
import React, { useState, useRef } from "react";
|
|
import { useRouter } from "next/router";
|
|
import useSWR from "swr";
|
|
import { observer } from "mobx-react-lite";
|
|
import {
|
|
DragDropContext,
|
|
Draggable,
|
|
DraggableProvided,
|
|
DraggableStateSnapshot,
|
|
DropResult,
|
|
Droppable,
|
|
} from "@hello-pangea/dnd";
|
|
|
|
// store
|
|
import { useMobxStore } from "lib/mobx/store-provider";
|
|
// components
|
|
import { CreateUpdateLabelInline, DeleteLabelModal, ProjectSettingLabelGroup } from "components/labels";
|
|
// ui
|
|
import { Button, Loader } from "@plane/ui";
|
|
import { EmptyState } from "components/common";
|
|
// images
|
|
import emptyLabel from "public/empty-state/label.svg";
|
|
// types
|
|
import { IIssueLabel } from "types";
|
|
//component
|
|
import { ProjectSettingLabelItem } from "./project-setting-label-item";
|
|
import useDraggableInPortal from "hooks/use-draggable-portal";
|
|
|
|
const LABELS_ROOT = "labels.root";
|
|
|
|
export const ProjectSettingsLabelList: React.FC = observer(() => {
|
|
// router
|
|
const router = useRouter();
|
|
const { workspaceSlug, projectId } = router.query;
|
|
|
|
const renderDraggable = useDraggableInPortal();
|
|
|
|
// store
|
|
const {
|
|
projectLabel: { fetchProjectLabels, projectLabels, updateLabelPosition, projectLabelsTree },
|
|
} = useMobxStore();
|
|
// states
|
|
const [showLabelForm, setLabelForm] = useState(false);
|
|
const [isUpdating, setIsUpdating] = useState(false);
|
|
const [selectDeleteLabel, setSelectDeleteLabel] = useState<IIssueLabel | null>(null);
|
|
const [isDraggingGroup, setIsDraggingGroup] = useState(false);
|
|
// ref
|
|
const scrollToRef = useRef<HTMLFormElement>(null);
|
|
|
|
// api call to fetch project details
|
|
useSWR(
|
|
workspaceSlug && projectId ? "PROJECT_LABELS" : null,
|
|
workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) : null
|
|
);
|
|
|
|
const newLabel = () => {
|
|
setIsUpdating(false);
|
|
setLabelForm(true);
|
|
};
|
|
|
|
const onDragEnd = (result: DropResult) => {
|
|
const { combine, draggableId, destination, source } = result;
|
|
|
|
// return if dropped outside the DragDropContext
|
|
if (!combine && !destination) return;
|
|
|
|
const childLabel = draggableId.split(".")[2];
|
|
let parentLabel: string | undefined | null = destination?.droppableId?.split(".")[3];
|
|
const index = destination?.index || 0;
|
|
|
|
const prevParentLabel: string | undefined | null = source?.droppableId?.split(".")[3];
|
|
const prevIndex = source?.index;
|
|
|
|
if (combine && combine.draggableId) parentLabel = combine?.draggableId?.split(".")[2];
|
|
|
|
if (destination?.droppableId === LABELS_ROOT) parentLabel = null;
|
|
|
|
if (result.reason == "DROP" && childLabel != parentLabel) {
|
|
updateLabelPosition(
|
|
workspaceSlug?.toString()!,
|
|
projectId?.toString()!,
|
|
childLabel,
|
|
parentLabel,
|
|
index,
|
|
prevParentLabel == parentLabel,
|
|
prevIndex
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<DeleteLabelModal
|
|
isOpen={!!selectDeleteLabel}
|
|
data={selectDeleteLabel ?? null}
|
|
onClose={() => setSelectDeleteLabel(null)}
|
|
/>
|
|
|
|
<div className="flex items-center py-3.5 border-b border-custom-border-100 justify-between">
|
|
<h3 className="text-xl font-medium">Labels</h3>
|
|
<Button variant="primary" onClick={newLabel} size="sm">
|
|
Add label
|
|
</Button>
|
|
</div>
|
|
<div className="w-full">
|
|
{showLabelForm && (
|
|
<div className="w-full rounded border border-custom-border-200 px-3.5 py-2">
|
|
<CreateUpdateLabelInline
|
|
labelForm={showLabelForm}
|
|
setLabelForm={setLabelForm}
|
|
isUpdating={isUpdating}
|
|
ref={scrollToRef}
|
|
onClose={() => {
|
|
setLabelForm(false);
|
|
setIsUpdating(false);
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
{/* labels */}
|
|
<>
|
|
{projectLabelsTree && (
|
|
<DragDropContext
|
|
onDragEnd={onDragEnd}
|
|
autoScrollerOptions={{
|
|
startFromPercentage: 1,
|
|
disabled: false,
|
|
maxScrollAtPercentage: 0,
|
|
maxPixelScroll: 2,
|
|
}}
|
|
>
|
|
<Droppable
|
|
droppableId={LABELS_ROOT}
|
|
isCombineEnabled={!isDraggingGroup}
|
|
ignoreContainerClipping={true}
|
|
isDropDisabled={isUpdating}
|
|
>
|
|
{(droppableProvided, droppableSnapshot) => (
|
|
<div className={`mt-3`} ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
|
|
{projectLabelsTree.map((label, index) => {
|
|
if (label.children && label.children.length) {
|
|
return (
|
|
<Draggable
|
|
key={`label.draggable.${label.id}`}
|
|
draggableId={`label.draggable.${label.id}.group`}
|
|
index={index}
|
|
isDragDisabled={isUpdating}
|
|
>
|
|
{(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => {
|
|
const isGroup = droppableSnapshot.draggingFromThisWith?.split(".")[3] === "group";
|
|
setIsDraggingGroup(isGroup);
|
|
|
|
return (
|
|
<div ref={provided.innerRef} {...provided.draggableProps} className="mt-3">
|
|
<ProjectSettingLabelGroup
|
|
key={label.id}
|
|
label={label}
|
|
labelChildren={label.children || []}
|
|
isDropDisabled={isGroup}
|
|
dragHandleProps={provided.dragHandleProps!}
|
|
handleLabelDelete={(label: IIssueLabel) => setSelectDeleteLabel(label)}
|
|
draggableSnapshot={snapshot}
|
|
isUpdating={isUpdating}
|
|
setIsUpdating={setIsUpdating}
|
|
/>
|
|
</div>
|
|
);
|
|
}}
|
|
</Draggable>
|
|
);
|
|
}
|
|
return (
|
|
<Draggable
|
|
key={`label.draggable.${label.id}`}
|
|
draggableId={`label.draggable.${label.id}`}
|
|
index={index}
|
|
isDragDisabled={isUpdating}
|
|
>
|
|
{renderDraggable((provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
|
|
<div ref={provided.innerRef} {...provided.draggableProps} className="mt-3">
|
|
<ProjectSettingLabelItem
|
|
dragHandleProps={provided.dragHandleProps!}
|
|
draggableSnapshot={snapshot}
|
|
label={label}
|
|
setIsUpdating={setIsUpdating}
|
|
handleLabelDelete={(label) => setSelectDeleteLabel(label)}
|
|
isChild={false}
|
|
/>
|
|
</div>
|
|
))}
|
|
</Draggable>
|
|
);
|
|
})}
|
|
{droppableProvided.placeholder}
|
|
</div>
|
|
)}
|
|
</Droppable>
|
|
</DragDropContext>
|
|
)}
|
|
</>
|
|
|
|
{/* loading state */}
|
|
{!projectLabels && (
|
|
<Loader className="space-y-5">
|
|
<Loader.Item height="42px" />
|
|
<Loader.Item height="42px" />
|
|
<Loader.Item height="42px" />
|
|
<Loader.Item height="42px" />
|
|
</Loader>
|
|
)}
|
|
|
|
{/* empty state */}
|
|
{projectLabels && projectLabels.length === 0 && (
|
|
<EmptyState
|
|
title="No labels yet"
|
|
description="Create labels to help organize and filter issues in you project"
|
|
image={emptyLabel}
|
|
primaryButton={{
|
|
text: "Add label",
|
|
onClick: () => newLabel(),
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
});
|