forked from github/plane
Compare commits
9 Commits
preview
...
dev/label_
Author | SHA1 | Date | |
---|---|---|---|
|
f560032fbf | ||
|
cb341f19c6 | ||
|
0722f1f624 | ||
|
adcc9e75e8 | ||
|
963eeb0a8c | ||
|
37df8684d7 | ||
|
0468e066ff | ||
|
e9c3a0642e | ||
|
f8ab0aa72b |
@ -53,7 +53,11 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||
|
||||
const activityDetails: {
|
||||
[key: string]: {
|
||||
message: (activity: IIssueActivity, showIssue: boolean) => React.ReactNode;
|
||||
message: (
|
||||
activity: IIssueActivity,
|
||||
showIssue: boolean,
|
||||
backgroundColor?: string
|
||||
) => React.ReactNode;
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
} = {
|
||||
@ -253,7 +257,7 @@ const activityDetails: {
|
||||
icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
labels: {
|
||||
message: (activity, showIssue) => {
|
||||
message: (activity, showIssue, backgroundColor = "#000000") => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
@ -262,7 +266,7 @@ const activityDetails: {
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: "#000000",
|
||||
backgroundColor,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@ -532,14 +536,26 @@ export const ActivityIcon = ({ activity }: { activity: IIssueActivity }) => (
|
||||
<>{activityDetails[activity.field as keyof typeof activityDetails]?.icon}</>
|
||||
);
|
||||
|
||||
export const ActivityMessage = ({
|
||||
activity,
|
||||
showIssue = false,
|
||||
}: {
|
||||
type ActivityMessageProps = {
|
||||
activity: IIssueActivity;
|
||||
showIssue?: boolean;
|
||||
}) => (
|
||||
<>
|
||||
{activityDetails[activity.field as keyof typeof activityDetails]?.message(activity, showIssue)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
export const ActivityMessage = observer(({ activity, showIssue = false }: ActivityMessageProps) => {
|
||||
const {
|
||||
label: { getLabelById },
|
||||
} = useMobxStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
{activityDetails[activity.field as keyof typeof activityDetails]?.message(
|
||||
activity,
|
||||
showIssue,
|
||||
activity.field === "labels" ? getLabelById(activity?.new_identifier!)?.color : undefined
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -10,7 +10,7 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// helpers
|
||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IIssueFilterOptions, IIssueLabels, IState, IUserLite, TStateGroups } from "types";
|
||||
import { IIssueFilterOptions, IState, IUserLite, TStateGroups, IIssueLabels } from "types";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
|
@ -1,18 +1,21 @@
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useProjects from "hooks/use-projects";
|
||||
// component
|
||||
import { Avatar, Icon } from "components/ui";
|
||||
// icons
|
||||
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
@ -20,7 +23,7 @@ import { renderEmoji } from "helpers/emoji.helper";
|
||||
// types
|
||||
import { IIssueViewProps, IState } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
currentState?: IState | null;
|
||||
@ -32,162 +35,164 @@ type Props = {
|
||||
viewProps: IIssueViewProps;
|
||||
};
|
||||
|
||||
export const BoardHeader: React.FC<Props> = ({
|
||||
currentState,
|
||||
groupTitle,
|
||||
addIssueToGroup,
|
||||
isCollapsed,
|
||||
setIsCollapsed,
|
||||
disableUserActions,
|
||||
viewProps,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
export const BoardHeader: React.FC<Props> = observer(
|
||||
({
|
||||
currentState,
|
||||
groupTitle,
|
||||
addIssueToGroup,
|
||||
isCollapsed,
|
||||
setIsCollapsed,
|
||||
disableUserActions,
|
||||
viewProps,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { groupedIssues, groupByProperty: selectedGroup } = viewProps;
|
||||
const { groupedIssues, groupByProperty: selectedGroup } = viewProps;
|
||||
|
||||
const { data: issueLabels } = useSWR(
|
||||
workspaceSlug && projectId && selectedGroup === "labels"
|
||||
? PROJECT_ISSUE_LABELS(projectId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && selectedGroup === "labels"
|
||||
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
const { label: labelStore } = useMobxStore();
|
||||
const { labels, loadLabels } = labelStore;
|
||||
|
||||
const { data: members } = useSWR(
|
||||
workspaceSlug && projectId && selectedGroup === "created_by"
|
||||
? PROJECT_MEMBERS(projectId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && selectedGroup === "created_by"
|
||||
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
const { data: members } = useSWR(
|
||||
workspaceSlug && projectId && selectedGroup === "created_by"
|
||||
? PROJECT_MEMBERS(projectId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && selectedGroup === "created_by"
|
||||
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { projects } = useProjects();
|
||||
const { projects } = useProjects();
|
||||
|
||||
const getGroupTitle = () => {
|
||||
let title = addSpaceIfCamelCase(groupTitle);
|
||||
useEffect(() => {
|
||||
if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId.toString());
|
||||
}, [workspaceSlug, projectId, loadLabels]);
|
||||
|
||||
switch (selectedGroup) {
|
||||
case "state":
|
||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
||||
break;
|
||||
case "labels":
|
||||
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
||||
break;
|
||||
case "project":
|
||||
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||
break;
|
||||
case "created_by":
|
||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
||||
title = member?.display_name ?? "";
|
||||
break;
|
||||
}
|
||||
const getGroupTitle = () => {
|
||||
let title = addSpaceIfCamelCase(groupTitle);
|
||||
|
||||
return title;
|
||||
};
|
||||
switch (selectedGroup) {
|
||||
case "state":
|
||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
||||
break;
|
||||
case "labels":
|
||||
title = labels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
||||
break;
|
||||
case "project":
|
||||
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||
break;
|
||||
case "created_by":
|
||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
||||
title = member?.display_name ?? "";
|
||||
break;
|
||||
}
|
||||
|
||||
const getGroupIcon = () => {
|
||||
let icon;
|
||||
return title;
|
||||
};
|
||||
|
||||
switch (selectedGroup) {
|
||||
case "state":
|
||||
icon =
|
||||
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
|
||||
break;
|
||||
case "state_detail.group":
|
||||
icon = getStateGroupIcon(groupTitle as any, "16", "16");
|
||||
break;
|
||||
case "priority":
|
||||
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||
break;
|
||||
case "project":
|
||||
const project = projects?.find((p) => p.id === groupTitle);
|
||||
icon =
|
||||
project &&
|
||||
(project.emoji !== null
|
||||
? renderEmoji(project.emoji)
|
||||
: project.icon_prop !== null
|
||||
? renderEmoji(project.icon_prop)
|
||||
: null);
|
||||
break;
|
||||
case "labels":
|
||||
const labelColor =
|
||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||
icon = (
|
||||
<span
|
||||
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
||||
style={{ backgroundColor: labelColor }}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "created_by":
|
||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
||||
icon = <Avatar user={member} height="24px" width="24px" fontSize="12px" />;
|
||||
const getGroupIcon = () => {
|
||||
let icon;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return icon;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-between px-1 ${
|
||||
!isCollapsed ? "flex-col rounded-md bg-custom-background-90" : ""
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-center ${isCollapsed ? "gap-1" : "flex-col gap-2"}`}>
|
||||
<div
|
||||
className={`flex cursor-pointer items-center gap-x-2 max-w-[316px] ${
|
||||
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center">{getGroupIcon()}</span>
|
||||
<h2
|
||||
className={`text-lg font-semibold truncate ${
|
||||
selectedGroup === "created_by" ? "" : "capitalize"
|
||||
}`}
|
||||
style={{
|
||||
writingMode: isCollapsed ? "horizontal-tb" : "vertical-rl",
|
||||
}}
|
||||
>
|
||||
{getGroupTitle()}
|
||||
</h2>
|
||||
<span className={`${isCollapsed ? "ml-0.5" : ""} py-1 text-center text-sm`}>
|
||||
{groupedIssues?.[groupTitle].length ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
setIsCollapsed((prevData) => !prevData);
|
||||
}}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<Icon
|
||||
iconName="close_fullscreen"
|
||||
className="text-base font-medium text-custom-text-900"
|
||||
switch (selectedGroup) {
|
||||
case "state":
|
||||
icon =
|
||||
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
|
||||
break;
|
||||
case "state_detail.group":
|
||||
icon = getStateGroupIcon(groupTitle as any, "16", "16");
|
||||
break;
|
||||
case "priority":
|
||||
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||
break;
|
||||
case "project":
|
||||
const project = projects?.find((p) => p.id === groupTitle);
|
||||
icon =
|
||||
project &&
|
||||
(project.emoji !== null
|
||||
? renderEmoji(project.emoji)
|
||||
: project.icon_prop !== null
|
||||
? renderEmoji(project.icon_prop)
|
||||
: null);
|
||||
break;
|
||||
case "labels":
|
||||
const labelColor = labels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||
icon = (
|
||||
<span
|
||||
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
||||
style={{ backgroundColor: labelColor }}
|
||||
/>
|
||||
) : (
|
||||
<Icon iconName="open_in_full" className="text-base font-medium text-custom-text-900" />
|
||||
)}
|
||||
</button>
|
||||
{!disableUserActions && selectedGroup !== "created_by" && (
|
||||
);
|
||||
break;
|
||||
case "created_by":
|
||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
||||
icon = <Avatar user={member} height="24px" width="24px" fontSize="12px" />;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return icon;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-between px-1 ${
|
||||
!isCollapsed ? "flex-col rounded-md bg-custom-background-90" : ""
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-center ${isCollapsed ? "gap-1" : "flex-col gap-2"}`}>
|
||||
<div
|
||||
className={`flex cursor-pointer items-center gap-x-2 max-w-[316px] ${
|
||||
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center">{getGroupIcon()}</span>
|
||||
<h2
|
||||
className={`text-lg font-semibold truncate ${
|
||||
selectedGroup === "created_by" ? "" : "capitalize"
|
||||
}`}
|
||||
style={{
|
||||
writingMode: isCollapsed ? "horizontal-tb" : "vertical-rl",
|
||||
}}
|
||||
>
|
||||
{getGroupTitle()}
|
||||
</h2>
|
||||
<span className={`${isCollapsed ? "ml-0.5" : ""} py-1 text-center text-sm`}>
|
||||
{groupedIssues?.[groupTitle].length ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
|
||||
onClick={addIssueToGroup}
|
||||
onClick={() => {
|
||||
setIsCollapsed((prevData) => !prevData);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
{isCollapsed ? (
|
||||
<Icon
|
||||
iconName="close_fullscreen"
|
||||
className="text-base font-medium text-custom-text-900"
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
iconName="open_in_full"
|
||||
className="text-base font-medium text-custom-text-900"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{!disableUserActions && selectedGroup !== "created_by" && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
|
||||
onClick={addIssueToGroup}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -1,9 +1,13 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useState, useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import { DropResult } from "react-beautiful-dnd";
|
||||
// services
|
||||
@ -37,7 +41,6 @@ import {
|
||||
MODULE_DETAILS,
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
PROJECT_ISSUE_LABELS,
|
||||
STATES_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
@ -46,10 +49,8 @@ type Props = {
|
||||
disableUserActions?: boolean;
|
||||
};
|
||||
|
||||
export const IssuesView: React.FC<Props> = ({
|
||||
openIssuesListModal,
|
||||
disableUserActions = false,
|
||||
}) => {
|
||||
export const IssuesView: React.FC<Props> = observer((props) => {
|
||||
const { openIssuesListModal, disableUserActions = false } = props;
|
||||
// create issue modal
|
||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
||||
const [createViewModal, setCreateViewModal] = useState<any>(null);
|
||||
@ -75,6 +76,9 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
const { label: labelStore } = useMobxStore();
|
||||
const { labels, loadLabels } = labelStore;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
@ -99,15 +103,12 @@ export const IssuesView: React.FC<Props> = ({
|
||||
);
|
||||
const states = getStatesList(stateGroups);
|
||||
|
||||
const { data: labels } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString());
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceSlug && projectId) loadLabels(workspaceSlug as string, projectId as string);
|
||||
}, [loadLabels, projectId, workspaceSlug]);
|
||||
|
||||
const handleDeleteIssue = useCallback(
|
||||
(issue: IIssue) => {
|
||||
setDeleteIssueModal(true);
|
||||
@ -564,4 +565,4 @@ export const IssuesView: React.FC<Props> = ({
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,11 +1,16 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// headless ui
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useProjects from "hooks/use-projects";
|
||||
@ -20,16 +25,9 @@ import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// types
|
||||
import {
|
||||
ICurrentUserResponse,
|
||||
IIssue,
|
||||
IIssueLabels,
|
||||
IIssueViewProps,
|
||||
IState,
|
||||
UserAuth,
|
||||
} from "types";
|
||||
import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
currentState?: IState | null;
|
||||
@ -44,18 +42,20 @@ type Props = {
|
||||
viewProps: IIssueViewProps;
|
||||
};
|
||||
|
||||
export const SingleList: React.FC<Props> = ({
|
||||
currentState,
|
||||
groupTitle,
|
||||
addIssueToGroup,
|
||||
handleIssueAction,
|
||||
openIssuesListModal,
|
||||
removeIssue,
|
||||
disableUserActions,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
}) => {
|
||||
export const SingleList: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
currentState,
|
||||
groupTitle,
|
||||
addIssueToGroup,
|
||||
handleIssueAction,
|
||||
openIssuesListModal,
|
||||
removeIssue,
|
||||
disableUserActions,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
} = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
@ -65,12 +65,8 @@ export const SingleList: React.FC<Props> = ({
|
||||
|
||||
const { groupByProperty: selectedGroup, groupedIssues } = viewProps;
|
||||
|
||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
const { label: labelStore } = useMobxStore();
|
||||
const { labels, loadLabels } = labelStore;
|
||||
|
||||
const { data: members } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||
@ -81,6 +77,10 @@ export const SingleList: React.FC<Props> = ({
|
||||
|
||||
const { projects } = useProjects();
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId.toString());
|
||||
}, [workspaceSlug, projectId, loadLabels]);
|
||||
|
||||
const getGroupTitle = () => {
|
||||
let title = addSpaceIfCamelCase(groupTitle);
|
||||
|
||||
@ -89,7 +89,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
||||
break;
|
||||
case "labels":
|
||||
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
||||
title = labels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
||||
break;
|
||||
case "project":
|
||||
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||
@ -128,8 +128,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
: null);
|
||||
break;
|
||||
case "labels":
|
||||
const labelColor =
|
||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||
const labelColor = labels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||
icon = (
|
||||
<span
|
||||
className="h-3 w-3 flex-shrink-0 rounded-full"
|
||||
@ -252,4 +251,4 @@ export const SingleList: React.FC<Props> = ({
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,13 +1,12 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// headless ui
|
||||
import { Combobox, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import issuesServices from "services/issues.service";
|
||||
// ui
|
||||
import { IssueLabelsList } from "components/ui";
|
||||
// icons
|
||||
@ -20,8 +19,6 @@ import {
|
||||
} from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import type { IIssueLabels } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
@ -30,179 +27,184 @@ type Props = {
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange, projectId }) => {
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
export const IssueLabelSelect: React.FC<Props> = observer(
|
||||
({ setIsOpen, value, onChange, projectId }) => {
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||
projectId ? PROJECT_ISSUE_LABELS(projectId) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesServices.getIssueLabels(workspaceSlug as string, projectId)
|
||||
: null
|
||||
);
|
||||
const { label: labelStore } = useMobxStore();
|
||||
const {
|
||||
isLabelsLoading: isLoading,
|
||||
labels,
|
||||
loadLabels,
|
||||
getLabelChildren,
|
||||
getFilteredLabels,
|
||||
} = labelStore;
|
||||
|
||||
const filteredOptions =
|
||||
query === ""
|
||||
? issueLabels
|
||||
: issueLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase()));
|
||||
useEffect(() => {
|
||||
if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId);
|
||||
}, [workspaceSlug, projectId, loadLabels]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
value={value}
|
||||
onChange={(val) => onChange(val)}
|
||||
className="relative flex-shrink-0"
|
||||
multiple
|
||||
>
|
||||
{({ open }: any) => (
|
||||
<>
|
||||
<Combobox.Button className="flex cursor-pointer items-center text-xs">
|
||||
{value && value.length > 0 ? (
|
||||
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs">
|
||||
<IssueLabelsList
|
||||
labels={value.map((v) => issueLabels?.find((l) => l.id === v)?.color) ?? []}
|
||||
length={3}
|
||||
showLength={true}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center justify-center gap-2 px-2.5 py-1 text-xs rounded-md border border-custom-border-200 shadow-sm duration-200 hover:bg-custom-background-80">
|
||||
<TagIcon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
<span className=" text-custom-text-200">Label</span>
|
||||
</span>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
const filteredOptions = getFilteredLabels(query);
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Combobox.Options
|
||||
className={`absolute z-10 mt-1 max-h-52 min-w-[8rem] overflow-auto rounded-md border-none
|
||||
bg-custom-background-90 px-2 py-2 text-xs shadow-md focus:outline-none`}
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
value={value}
|
||||
onChange={(val) => onChange(val)}
|
||||
className="relative flex-shrink-0"
|
||||
multiple
|
||||
>
|
||||
{({ open }: any) => (
|
||||
<>
|
||||
<Combobox.Button className="flex cursor-pointer items-center rounded-md border border-custom-border-200 text-xs shadow-sm duration-200 hover:bg-custom-background-80">
|
||||
{value && value.length > 0 ? (
|
||||
<span className="flex items-center justify-center gap-2 px-3 py-1 text-xs">
|
||||
<IssueLabelsList
|
||||
labels={value.map((v) => labels?.find((l) => l.id === v)?.color) ?? []}
|
||||
length={3}
|
||||
showLength={true}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center justify-center gap-2 px-2.5 py-1 text-xs">
|
||||
<TagIcon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
<span className=" text-custom-text-200">Label</span>
|
||||
</span>
|
||||
)}
|
||||
</Combobox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded-sm border-[0.6px] border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3 w-3 text-custom-text-200" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 focus:outline-none"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search for label..."
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="py-1.5">
|
||||
{issueLabels && filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((label) => {
|
||||
const children = issueLabels?.filter((l) => l.parent === label.id);
|
||||
<Combobox.Options
|
||||
className={`absolute z-10 mt-1 max-h-52 min-w-[8rem] overflow-auto rounded-md border-none
|
||||
bg-custom-background-90 px-2 py-2 text-xs shadow-md focus:outline-none`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded-sm border-[0.6px] border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3 w-3 text-custom-text-200" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 focus:outline-none"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search for label..."
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="py-1.5">
|
||||
{!isLoading && filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((label) => {
|
||||
const children = getLabelChildren(label.id);
|
||||
|
||||
if (children.length === 0) {
|
||||
if (!label.parent)
|
||||
return (
|
||||
<Combobox.Option
|
||||
key={label.id}
|
||||
className={({ active }) =>
|
||||
`${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200`
|
||||
}
|
||||
value={label.id}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<div className="flex w-full justify-between gap-2 rounded">
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<span
|
||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color,
|
||||
}}
|
||||
/>
|
||||
<span>{label.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center rounded p-1">
|
||||
<CheckIcon
|
||||
className={`h-3 w-3 ${
|
||||
selected ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
);
|
||||
} else
|
||||
return (
|
||||
<div className="border-y border-custom-border-200">
|
||||
<div className="flex select-none items-center gap-2 truncate p-2 text-custom-text-100">
|
||||
<RectangleGroupIcon className="h-3 w-3" /> {label.name}
|
||||
</div>
|
||||
<div>
|
||||
{children.map((child) => (
|
||||
<Combobox.Option
|
||||
key={child.id}
|
||||
className={({ active }) =>
|
||||
`${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200`
|
||||
}
|
||||
value={child.id}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<div className="flex w-full justify-between gap-2 rounded">
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<span
|
||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: child?.color,
|
||||
}}
|
||||
/>
|
||||
<span>{child.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center rounded p-1">
|
||||
<CheckIcon
|
||||
className={`h-3 w-3 ${
|
||||
selected ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
if (children.length === 0) {
|
||||
if (!label.parent)
|
||||
return (
|
||||
<Combobox.Option
|
||||
key={label.id}
|
||||
className={({ active }) =>
|
||||
`${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200`
|
||||
}
|
||||
value={label.id}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<div className="flex w-full justify-between gap-2 rounded">
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<span
|
||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color,
|
||||
}}
|
||||
/>
|
||||
<span>{label.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
<div className="flex items-center justify-center rounded p-1">
|
||||
<CheckIcon
|
||||
className={`h-3 w-3 ${
|
||||
selected ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
);
|
||||
} else
|
||||
return (
|
||||
<div className="border-y border-custom-border-200">
|
||||
<div className="flex select-none items-center gap-2 truncate p-2 text-custom-text-100">
|
||||
<RectangleGroupIcon className="h-3 w-3" /> {label.name}
|
||||
</div>
|
||||
<div>
|
||||
{children.map((child) => (
|
||||
<Combobox.Option
|
||||
key={child.id}
|
||||
className={({ active }) =>
|
||||
`${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200`
|
||||
}
|
||||
value={child.id}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<div className="flex w-full justify-between gap-2 rounded">
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<span
|
||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: child?.color,
|
||||
}}
|
||||
/>
|
||||
<span>{child.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center rounded p-1">
|
||||
<CheckIcon
|
||||
className={`h-3 w-3 ${
|
||||
selected ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="px-2 text-xs text-custom-text-200">No labels found</p>
|
||||
)
|
||||
) : (
|
||||
<p className="px-2 text-xs text-custom-text-200">No labels found</p>
|
||||
)
|
||||
) : (
|
||||
<p className="px-2 text-xs text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full select-none items-center rounded py-2 px-1 hover:bg-custom-background-80"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-1 text-custom-text-200">
|
||||
<PlusIcon className="h-4 w-4" aria-hidden="true" />
|
||||
<span>Create New Label</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
<p className="px-2 text-xs text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full select-none items-center rounded py-2 px-1 hover:bg-custom-background-80"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-1 text-custom-text-200">
|
||||
<PlusIcon className="h-4 w-4" aria-hidden="true" />
|
||||
<span>Create New Label</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -2,7 +2,9 @@ import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, UseFormWatch, useForm } from "react-hook-form";
|
||||
@ -10,8 +12,6 @@ import { Controller, UseFormWatch, useForm } from "react-hook-form";
|
||||
import { TwitterPicker } from "react-color";
|
||||
// headless ui
|
||||
import { Listbox, Popover, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
// ui
|
||||
@ -25,9 +25,7 @@ import {
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IIssue, IIssueLabels } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
import { IIssue, LabelForm } from "types";
|
||||
|
||||
type Props = {
|
||||
issueDetails: IIssue | undefined;
|
||||
@ -38,19 +36,13 @@ type Props = {
|
||||
uneditable: boolean;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IIssueLabels> = {
|
||||
const defaultValues: Partial<LabelForm> = {
|
||||
name: "",
|
||||
color: "#ff0000",
|
||||
};
|
||||
|
||||
export const SidebarLabelSelect: React.FC<Props> = ({
|
||||
issueDetails,
|
||||
issueControl,
|
||||
watchIssue,
|
||||
submitChanges,
|
||||
isNotAllowed,
|
||||
uneditable,
|
||||
}) => {
|
||||
export const SidebarLabelSelect: React.FC<Props> = observer((props) => {
|
||||
const { issueDetails, issueControl, watchIssue, submitChanges, isNotAllowed, uneditable } = props;
|
||||
const [createLabelForm, setCreateLabelForm] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
@ -64,33 +56,38 @@ export const SidebarLabelSelect: React.FC<Props> = ({
|
||||
watch,
|
||||
control: labelControl,
|
||||
setFocus,
|
||||
} = useForm<Partial<IIssueLabels>>({
|
||||
} = useForm<LabelForm>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const { data: issueLabels, mutate: issueLabelMutate } = useSWR<IIssueLabels[]>(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
const { label: labelStore } = useMobxStore();
|
||||
const {
|
||||
labels,
|
||||
loadLabels,
|
||||
createLabel,
|
||||
getLabelById,
|
||||
getLabelChildren,
|
||||
isLabelsLoading: isLoading,
|
||||
} = labelStore;
|
||||
|
||||
const handleNewLabel = async (formData: Partial<IIssueLabels>) => {
|
||||
if (!workspaceSlug || !projectId || isSubmitting) return;
|
||||
useEffect(() => {
|
||||
if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId.toString());
|
||||
}, [workspaceSlug, projectId, loadLabels]);
|
||||
|
||||
await issuesService
|
||||
.createIssueLabel(workspaceSlug as string, projectId as string, formData, user)
|
||||
.then((res) => {
|
||||
const handleNewLabel = async (formData: LabelForm) => {
|
||||
if (!workspaceSlug || !projectId || isSubmitting || !user) return;
|
||||
|
||||
await createLabel(workspaceSlug.toString(), projectId.toString(), formData, user).then(
|
||||
(res: any) => {
|
||||
reset(defaultValues);
|
||||
|
||||
issueLabelMutate((prevData: any) => [...(prevData ?? []), res], false);
|
||||
|
||||
submitChanges({ labels_list: [...(issueDetails?.labels ?? []), res.id] });
|
||||
|
||||
setCreateLabelForm(false);
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -110,7 +107,7 @@ export const SidebarLabelSelect: React.FC<Props> = ({
|
||||
<div className="basis-1/2">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{watchIssue("labels_list")?.map((labelId) => {
|
||||
const label = issueLabels?.find((l) => l.id === labelId);
|
||||
const label = getLabelById(labelId);
|
||||
|
||||
if (label)
|
||||
return (
|
||||
@ -168,12 +165,10 @@ export const SidebarLabelSelect: React.FC<Props> = ({
|
||||
>
|
||||
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-28 w-40 overflow-auto rounded-md bg-custom-background-80 py-1 text-xs shadow-lg border border-custom-border-100 focus:outline-none">
|
||||
<div className="py-1">
|
||||
{issueLabels ? (
|
||||
issueLabels.length > 0 ? (
|
||||
issueLabels.map((label: IIssueLabels) => {
|
||||
const children = issueLabels?.filter(
|
||||
(l) => l.parent === label.id
|
||||
);
|
||||
{!isLoading ? (
|
||||
labels.length > 0 ? (
|
||||
labels.map((label) => {
|
||||
const children = getLabelChildren(label.id);
|
||||
|
||||
if (children.length === 0) {
|
||||
if (!label.parent)
|
||||
@ -346,4 +341,4 @@ export const SidebarLabelSelect: React.FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// component
|
||||
import { CreateLabelModal } from "components/labels";
|
||||
// ui
|
||||
@ -13,9 +13,7 @@ import { CustomSearchSelect, Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { PlusIcon, TagIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, IIssueLabels } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
import { ICurrentUserResponse, IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
@ -28,29 +26,31 @@ type Props = {
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const ViewLabelSelect: React.FC<Props> = ({
|
||||
issue,
|
||||
partialUpdateIssue,
|
||||
position = "left",
|
||||
selfPositioned = false,
|
||||
tooltipPosition = "top",
|
||||
user,
|
||||
isNotAllowed,
|
||||
customButton = false,
|
||||
}) => {
|
||||
export const ViewLabelSelect: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
issue,
|
||||
partialUpdateIssue,
|
||||
position = "left",
|
||||
selfPositioned = false,
|
||||
tooltipPosition = "top",
|
||||
user,
|
||||
isNotAllowed,
|
||||
customButton = false,
|
||||
} = props;
|
||||
|
||||
const [labelModal, setLabelModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||
projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
const { label: labelStore } = useMobxStore();
|
||||
const { labels, loadLabels, getLabelById } = labelStore;
|
||||
|
||||
const options = issueLabels?.map((label) => ({
|
||||
useEffect(() => {
|
||||
if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId.toString());
|
||||
}, [workspaceSlug, projectId, loadLabels]);
|
||||
|
||||
const options = labels?.map((label) => ({
|
||||
value: label.id,
|
||||
query: label.name,
|
||||
content: (
|
||||
@ -74,7 +74,7 @@ export const ViewLabelSelect: React.FC<Props> = ({
|
||||
issue.labels.length > 0
|
||||
? issue.labels
|
||||
.map((labelId) => {
|
||||
const label = issueLabels?.find((l) => l.id === labelId);
|
||||
const label = getLabelById(labelId);
|
||||
|
||||
return label?.name ?? "";
|
||||
})
|
||||
@ -90,7 +90,7 @@ export const ViewLabelSelect: React.FC<Props> = ({
|
||||
{issue.labels.length > 0 ? (
|
||||
<>
|
||||
{issue.labels.slice(0, 4).map((labelId, index) => {
|
||||
const label = issueLabels?.find((l) => l.id === labelId);
|
||||
const label = getLabelById(labelId);
|
||||
|
||||
return (
|
||||
<div className={`flex h-4 w-4 rounded-full ${index ? "-ml-3.5" : ""}`}>
|
||||
@ -154,4 +154,4 @@ export const ViewLabelSelect: React.FC<Props> = ({
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -2,7 +2,9 @@ import React, { useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
@ -10,16 +12,13 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { TwitterPicker } from "react-color";
|
||||
// headless ui
|
||||
import { Dialog, Popover, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// ui
|
||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import type { ICurrentUserResponse, IIssueLabels, IState } from "types";
|
||||
import type { ICurrentUserResponse, IIssueLabels, LabelForm } from "types";
|
||||
// constants
|
||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label";
|
||||
|
||||
// types
|
||||
@ -31,179 +30,172 @@ type Props = {
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IState> = {
|
||||
const defaultValues: Partial<LabelForm> = {
|
||||
name: "",
|
||||
color: "rgb(var(--color-text-200))",
|
||||
};
|
||||
|
||||
export const CreateLabelModal: React.FC<Props> = ({
|
||||
isOpen,
|
||||
projectId,
|
||||
handleClose,
|
||||
user,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
export const CreateLabelModal: React.FC<Props> = observer(
|
||||
({ isOpen, projectId, handleClose, user, onSuccess }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
watch,
|
||||
control,
|
||||
reset,
|
||||
setValue,
|
||||
} = useForm<IIssueLabels>({
|
||||
defaultValues,
|
||||
});
|
||||
const { label: labelStore } = useMobxStore();
|
||||
const { createLabel } = labelStore;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) setValue("color", getRandomLabelColor());
|
||||
}, [setValue, isOpen]);
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
watch,
|
||||
control,
|
||||
reset,
|
||||
setValue,
|
||||
} = useForm<IIssueLabels>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
handleClose();
|
||||
reset(defaultValues);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (isOpen) setValue("color", getRandomLabelColor());
|
||||
}, [setValue, isOpen]);
|
||||
|
||||
const onSubmit = async (formData: IIssueLabels) => {
|
||||
if (!workspaceSlug) return;
|
||||
const onClose = () => {
|
||||
handleClose();
|
||||
reset(defaultValues);
|
||||
};
|
||||
|
||||
await issuesService
|
||||
.createIssueLabel(workspaceSlug as string, projectId as string, formData, user)
|
||||
.then((res) => {
|
||||
mutate<IIssueLabels[]>(
|
||||
PROJECT_ISSUE_LABELS(projectId),
|
||||
(prevData) => [res, ...(prevData ?? [])],
|
||||
false
|
||||
);
|
||||
onClose();
|
||||
if (onSuccess) onSuccess(res);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
};
|
||||
const onSubmit = async (formData: LabelForm) => {
|
||||
if (!workspaceSlug || !user) return;
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-30" onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
await createLabel(workspaceSlug.toString(), projectId as string, formData, user)
|
||||
.then((response: any) => {
|
||||
onClose();
|
||||
if (onSuccess) onSuccess(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
};
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-90 px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-custom-text-100"
|
||||
>
|
||||
Create Label
|
||||
</Dialog.Title>
|
||||
<div className="mt-8 flex items-center gap-2">
|
||||
<Popover className="relative">
|
||||
{({ open, close }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`group inline-flex items-center rounded-sm py-2 text-base font-medium hover:text-custom-text-100 focus:outline-none ${
|
||||
open ? "text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
{watch("color") && watch("color") !== "" && (
|
||||
<span
|
||||
className="ml-2 h-5 w-5 rounded"
|
||||
style={{
|
||||
backgroundColor: watch("color") ?? "black",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ChevronDownIcon
|
||||
className={`ml-2 h-5 w-5 group-hover:text-custom-text-200 ${
|
||||
open ? "text-gray-600" : "text-gray-400"
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-30" onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-90 px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-custom-text-100"
|
||||
>
|
||||
Create Label
|
||||
</Dialog.Title>
|
||||
<div className="mt-8 flex items-center gap-2">
|
||||
<Popover className="relative">
|
||||
{({ open, close }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`group inline-flex items-center rounded-sm py-2 text-base font-medium hover:text-custom-text-100 focus:outline-none ${
|
||||
open ? "text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="fixed left-5 z-50 mt-3 w-screen max-w-xs transform px-2 sm:px-0">
|
||||
<Controller
|
||||
name="color"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TwitterPicker
|
||||
color={value}
|
||||
colors={LABEL_COLOR_OPTIONS}
|
||||
onChange={(value) => {
|
||||
onChange(value.hex);
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{watch("color") && watch("color") !== "" && (
|
||||
<span
|
||||
className="ml-2 h-5 w-5 rounded"
|
||||
style={{
|
||||
backgroundColor: watch("color") ?? "black",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ChevronDownIcon
|
||||
className={`ml-2 h-5 w-5 group-hover:text-custom-text-200 ${
|
||||
open ? "text-gray-600" : "text-gray-400"
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
<div className="flex w-full flex-col gap-0.5 justify-center">
|
||||
<Input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Label title"
|
||||
autoComplete="off"
|
||||
error={errors.name}
|
||||
register={register}
|
||||
width="full"
|
||||
validations={{
|
||||
required: "Label title is required",
|
||||
}}
|
||||
/>
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="fixed left-5 z-50 mt-3 w-screen max-w-xs transform px-2 sm:px-0">
|
||||
<Controller
|
||||
name="color"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TwitterPicker
|
||||
color={value}
|
||||
colors={LABEL_COLOR_OPTIONS}
|
||||
onChange={(value) => {
|
||||
onChange(value.hex);
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
<div className="flex w-full flex-col gap-0.5 justify-center">
|
||||
<Input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Label title"
|
||||
autoComplete="off"
|
||||
error={errors.name}
|
||||
register={register}
|
||||
width="full"
|
||||
validations={{
|
||||
required: "Label title is required",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
|
||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Creating Label..." : "Create Label"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
|
||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Creating Label..." : "Create Label"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -2,7 +2,9 @@ import React, { forwardRef, useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||
@ -12,8 +14,6 @@ import useUserAuth from "hooks/use-user-auth";
|
||||
import { TwitterPicker } from "react-color";
|
||||
// headless ui
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// ui
|
||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// icons
|
||||
@ -21,7 +21,6 @@ import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IIssueLabels } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label";
|
||||
|
||||
type Props = {
|
||||
@ -37,13 +36,16 @@ const defaultValues: Partial<IIssueLabels> = {
|
||||
color: "rgb(var(--color-text-200))",
|
||||
};
|
||||
|
||||
export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
|
||||
function CreateUpdateLabelInline(props, ref) {
|
||||
export const CreateUpdateLabelInline = observer(
|
||||
forwardRef<HTMLDivElement, Props>(function CreateUpdateLabelInline(props, ref) {
|
||||
const { labelForm, setLabelForm, isUpdating, labelToUpdate, onClose } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { label: labelStore } = useMobxStore();
|
||||
const { createLabel, updateLabel } = labelStore;
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
const {
|
||||
@ -65,41 +67,27 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
|
||||
};
|
||||
|
||||
const handleLabelCreate: SubmitHandler<IIssueLabels> = async (formData) => {
|
||||
if (!workspaceSlug || !projectId || isSubmitting) return;
|
||||
if (!workspaceSlug || !projectId || isSubmitting || !user) return;
|
||||
|
||||
await issuesService
|
||||
.createIssueLabel(workspaceSlug as string, projectId as string, formData, user)
|
||||
.then((res) => {
|
||||
mutate<IIssueLabels[]>(
|
||||
PROJECT_ISSUE_LABELS(projectId as string),
|
||||
(prevData) => [res, ...(prevData ?? [])],
|
||||
false
|
||||
);
|
||||
await createLabel(workspaceSlug.toString(), projectId.toString(), formData, user).finally(
|
||||
() => {
|
||||
handleClose();
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleLabelUpdate: SubmitHandler<IIssueLabels> = async (formData) => {
|
||||
if (!workspaceSlug || !projectId || isSubmitting) return;
|
||||
if (!workspaceSlug || !projectId || isSubmitting || !user) return;
|
||||
|
||||
await issuesService
|
||||
.patchIssueLabel(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
labelToUpdate?.id ?? "",
|
||||
formData,
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
reset(defaultValues);
|
||||
mutate<IIssueLabels[]>(
|
||||
PROJECT_ISSUE_LABELS(projectId as string),
|
||||
(prevData) =>
|
||||
prevData?.map((p) => (p.id === labelToUpdate?.id ? { ...p, ...formData } : p)),
|
||||
false
|
||||
);
|
||||
handleClose();
|
||||
});
|
||||
await updateLabel(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
labelToUpdate?.id ?? "",
|
||||
formData,
|
||||
user
|
||||
).finally(() => {
|
||||
handleClose();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -212,5 +200,5 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -2,22 +2,20 @@ import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { DangerButton, SecondaryButton } from "components/ui";
|
||||
// types
|
||||
import type { ICurrentUserResponse, IIssueLabels } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@ -26,12 +24,15 @@ type Props = {
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
export const DeleteLabelModal: React.FC<Props> = ({ isOpen, onClose, data, user }) => {
|
||||
export const DeleteLabelModal: React.FC<Props> = observer(({ isOpen, onClose, data, user }) => {
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { label: labelStore } = useMobxStore();
|
||||
const { deleteLabel } = labelStore;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleClose = () => {
|
||||
@ -40,28 +41,21 @@ export const DeleteLabelModal: React.FC<Props> = ({ isOpen, onClose, data, user
|
||||
};
|
||||
|
||||
const handleDeletion = async () => {
|
||||
if (!workspaceSlug || !projectId || !data) return;
|
||||
if (!workspaceSlug || !projectId || !data || !user) return;
|
||||
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
mutate<IIssueLabels[]>(
|
||||
PROJECT_ISSUE_LABELS(projectId.toString()),
|
||||
(prevData) => (prevData ?? []).filter((p) => p.id !== data.id),
|
||||
false
|
||||
);
|
||||
|
||||
await issuesService
|
||||
.deleteIssueLabel(workspaceSlug.toString(), projectId.toString(), data.id, user)
|
||||
.then(() => handleClose())
|
||||
await deleteLabel(workspaceSlug.toString(), projectId.toString(), data.id, user)
|
||||
.catch(() => {
|
||||
setIsDeleteLoading(false);
|
||||
|
||||
mutate(PROJECT_ISSUE_LABELS(projectId.toString()));
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Label could not be deleted. Please try again.",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
handleClose();
|
||||
setIsDeleteLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
@ -130,4 +124,4 @@ export const DeleteLabelModal: React.FC<Props> = ({ isOpen, onClose, data, user
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -2,18 +2,16 @@ import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// headless ui
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { RectangleStackIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssueLabels } from "types";
|
||||
// constants
|
||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@ -22,158 +20,141 @@ type Props = {
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
export const LabelsListModal: React.FC<Props> = ({ isOpen, handleClose, parent, user }) => {
|
||||
const [query, setQuery] = useState("");
|
||||
export const LabelsListModal: React.FC<Props> = observer(
|
||||
({ isOpen, handleClose, parent, user }) => {
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: issueLabels, mutate } = useSWR<IIssueLabels[]>(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
const { label: labelStore } = useMobxStore();
|
||||
const { updateLabel, getLabelChildren, getFilteredLabels } = labelStore;
|
||||
|
||||
const filteredLabels: IIssueLabels[] =
|
||||
query === ""
|
||||
? issueLabels ?? []
|
||||
: issueLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())) ?? [];
|
||||
const filteredLabels = getFilteredLabels(query);
|
||||
|
||||
const handleModalClose = () => {
|
||||
handleClose();
|
||||
setQuery("");
|
||||
};
|
||||
const handleModalClose = () => {
|
||||
handleClose();
|
||||
setQuery("");
|
||||
};
|
||||
|
||||
const addChildLabel = async (label: IIssueLabels) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const addChildLabel = async (label: IIssueLabels) => {
|
||||
if (!workspaceSlug || !projectId || !user) return;
|
||||
|
||||
mutate(
|
||||
(prevData: any) =>
|
||||
prevData?.map((l: any) => {
|
||||
if (l.id === label.id) return { ...l, parent: parent?.id ?? "" };
|
||||
|
||||
return l;
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
await issuesService
|
||||
.patchIssueLabel(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
updateLabel(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
label.id,
|
||||
{
|
||||
parent: parent?.id ?? "",
|
||||
},
|
||||
user
|
||||
)
|
||||
.then(() => mutate());
|
||||
};
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleModalClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleModalClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-custom-border-200 bg-custom-background-100 shadow-2xl transition-all">
|
||||
<Combobox>
|
||||
<div className="relative m-1">
|
||||
<MagnifyingGlassIcon
|
||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-custom-text-100 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Combobox.Input
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-custom-text-100 outline-none focus:ring-0 sm:text-sm"
|
||||
placeholder="Search..."
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<Combobox.Options static className="max-h-80 scroll-py-2 overflow-y-auto">
|
||||
{filteredLabels.length > 0 && (
|
||||
<li className="p-2">
|
||||
{query === "" && (
|
||||
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-custom-text-100">
|
||||
Labels
|
||||
</h2>
|
||||
)}
|
||||
<ul className="text-sm text-gray-700">
|
||||
{filteredLabels.map((label) => {
|
||||
const children = issueLabels?.filter((l) => l.parent === label.id);
|
||||
|
||||
if (
|
||||
(label.parent === "" || label.parent === null) && // issue does not have any other parent
|
||||
label.id !== parent?.id && // issue is not itself
|
||||
children?.length === 0 // issue doesn't have any othe children
|
||||
)
|
||||
return (
|
||||
<Combobox.Option
|
||||
key={label.id}
|
||||
value={{
|
||||
name: label.name,
|
||||
}}
|
||||
className={({ active }) =>
|
||||
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
|
||||
active ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||
}`
|
||||
}
|
||||
onClick={() => {
|
||||
addChildLabel(label);
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color !== "" ? label.color : "#000000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</Combobox.Option>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
</Combobox.Options>
|
||||
|
||||
{query !== "" && filteredLabels.length === 0 && (
|
||||
<div className="py-14 px-6 text-center sm:px-14">
|
||||
<RectangleStackIcon
|
||||
className="mx-auto h-6 w-6 text-custom-text-100 text-opacity-40"
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-custom-border-200 bg-custom-background-100 shadow-2xl transition-all">
|
||||
<Combobox>
|
||||
<div className="relative m-1">
|
||||
<MagnifyingGlassIcon
|
||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-custom-text-100 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="mt-4 text-sm text-custom-text-100">
|
||||
We couldn{"'"}t find any label with that term. Please try again.
|
||||
</p>
|
||||
<Combobox.Input
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-custom-text-100 outline-none focus:ring-0 sm:text-sm"
|
||||
placeholder="Search..."
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Combobox>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
|
||||
<Combobox.Options static className="max-h-80 scroll-py-2 overflow-y-auto">
|
||||
{filteredLabels.length > 0 && (
|
||||
<li className="p-2">
|
||||
{query === "" && (
|
||||
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-custom-text-100">
|
||||
Labels
|
||||
</h2>
|
||||
)}
|
||||
<ul className="text-sm text-gray-700">
|
||||
{filteredLabels.map((label) => {
|
||||
const children = getLabelChildren(label.id);
|
||||
|
||||
if (
|
||||
(label.parent === "" || label.parent === null) && // issue does not have any other parent
|
||||
label.id !== parent?.id && // issue is not itself
|
||||
children?.length === 0 // issue doesn't have any othe children
|
||||
)
|
||||
return (
|
||||
<Combobox.Option
|
||||
key={label.id}
|
||||
value={{
|
||||
name: label.name,
|
||||
}}
|
||||
className={({ active }) =>
|
||||
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
|
||||
active ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||
}`
|
||||
}
|
||||
onClick={() => {
|
||||
addChildLabel(label);
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color !== "" ? label.color : "#000000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</Combobox.Option>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
</Combobox.Options>
|
||||
|
||||
{query !== "" && filteredLabels.length === 0 && (
|
||||
<div className="py-14 px-6 text-center sm:px-14">
|
||||
<RectangleStackIcon
|
||||
className="mx-auto h-6 w-6 text-custom-text-100 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="mt-4 text-sm text-custom-text-100">
|
||||
We couldn{"'"}t find any label with that term. Please try again.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Combobox>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -2,12 +2,12 @@ import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// headless ui
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// ui
|
||||
import { CustomMenu } from "components/ui";
|
||||
// icons
|
||||
@ -21,158 +21,141 @@ import {
|
||||
} from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssueLabels } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
label: IIssueLabels;
|
||||
labelChildren: IIssueLabels[];
|
||||
addLabelToGroup: (parentLabel: IIssueLabels) => void;
|
||||
editLabel: (label: IIssueLabels) => void;
|
||||
handleLabelDelete: () => void;
|
||||
handleLabelDelete: (label: IIssueLabels) => void;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
export const SingleLabelGroup: React.FC<Props> = ({
|
||||
label,
|
||||
labelChildren,
|
||||
addLabelToGroup,
|
||||
editLabel,
|
||||
handleLabelDelete,
|
||||
user,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
export const SingleLabelGroup: React.FC<Props> = observer(
|
||||
({ label, labelChildren, addLabelToGroup, editLabel, handleLabelDelete, user }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const removeFromGroup = (label: IIssueLabels) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const { label: labelStore } = useMobxStore();
|
||||
const { updateLabel } = labelStore;
|
||||
|
||||
mutate<IIssueLabels[]>(
|
||||
PROJECT_ISSUE_LABELS(projectId as string),
|
||||
(prevData) =>
|
||||
prevData?.map((l) => {
|
||||
if (l.id === label.id) return { ...l, parent: null };
|
||||
const removeFromGroup = (label: IIssueLabels) => {
|
||||
if (!workspaceSlug || !projectId || !user) return;
|
||||
|
||||
return l;
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
issuesService
|
||||
.patchIssueLabel(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
updateLabel(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
label.id,
|
||||
{
|
||||
parent: null,
|
||||
},
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
mutate(PROJECT_ISSUE_LABELS(projectId as string));
|
||||
});
|
||||
};
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Disclosure
|
||||
as="div"
|
||||
className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5 text-custom-text-100"
|
||||
defaultOpen
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className="flex cursor-pointer items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>
|
||||
<RectangleGroupIcon className="h-4 w-4" />
|
||||
</span>
|
||||
<h6>{label.name}</h6>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
<span>Add more labels</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => editLabel(label)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span>Edit label</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleLabelDelete}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete label</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
<Disclosure.Button>
|
||||
return (
|
||||
<Disclosure
|
||||
as="div"
|
||||
className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5 text-custom-text-100"
|
||||
defaultOpen
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className="flex cursor-pointer items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>
|
||||
<ChevronDownIcon
|
||||
className={`h-4 w-4 text-custom-text-100 ${!open ? "rotate-90 transform" : ""}`}
|
||||
/>
|
||||
<RectangleGroupIcon className="h-4 w-4" />
|
||||
</span>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
</div>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="mt-3 ml-6 space-y-3">
|
||||
{labelChildren.map((child) => (
|
||||
<div
|
||||
key={child.id}
|
||||
className="group flex items-center justify-between rounded-md border border-custom-border-200 p-2 text-sm"
|
||||
>
|
||||
<h5 className="flex items-center gap-3">
|
||||
<span
|
||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor:
|
||||
child.color && child.color !== "" ? child.color : "#000000",
|
||||
}}
|
||||
/>
|
||||
{child.name}
|
||||
</h5>
|
||||
<div className="pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100">
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem onClick={() => removeFromGroup(child)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
<span>Remove from group</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => editLabel(child)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span>Edit label</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleLabelDelete}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete label</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<h6>{label.name}</h6>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
<div className="flex items-center gap-2">
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
<span>Add more labels</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => editLabel(label)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span>Edit label</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => handleLabelDelete(label)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete label</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
<Disclosure.Button>
|
||||
<span>
|
||||
<ChevronDownIcon
|
||||
className={`h-4 w-4 text-custom-text-100 ${
|
||||
!open ? "rotate-90 transform" : ""
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
</div>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="mt-3 ml-6 space-y-3">
|
||||
{labelChildren.map((child) => (
|
||||
<div
|
||||
key={child.id}
|
||||
className="group flex items-center justify-between rounded-md border border-custom-border-200 p-2 text-sm"
|
||||
>
|
||||
<h5 className="flex items-center gap-3">
|
||||
<span
|
||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor:
|
||||
child.color && child.color !== "" ? child.color : "#000000",
|
||||
}}
|
||||
/>
|
||||
{child.name}
|
||||
</h5>
|
||||
<div className="pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100">
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem onClick={() => removeFromGroup(child)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
<span>Remove from group</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => editLabel(child)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span>Edit label</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => handleLabelDelete(child)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete label</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -4,6 +4,10 @@ import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
// services
|
||||
@ -20,9 +24,8 @@ import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// types
|
||||
import { IQuery, IView } from "types";
|
||||
import issuesService from "services/issues.service";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, STATES_LIST } from "constants/fetch-keys";
|
||||
import { STATES_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
handleFormSubmit: (values: IView) => Promise<void>;
|
||||
@ -37,13 +40,9 @@ const defaultValues: Partial<IView> = {
|
||||
description: "",
|
||||
};
|
||||
|
||||
export const ViewForm: React.FC<Props> = ({
|
||||
handleFormSubmit,
|
||||
handleClose,
|
||||
status,
|
||||
data,
|
||||
preLoadedData,
|
||||
}) => {
|
||||
export const ViewForm: React.FC<Props> = observer((props) => {
|
||||
const { handleFormSubmit, handleClose, status, data, preLoadedData } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
@ -69,16 +68,16 @@ export const ViewForm: React.FC<Props> = ({
|
||||
);
|
||||
const states = getStatesList(stateGroups);
|
||||
|
||||
const { data: labels } = useSWR(
|
||||
workspaceSlug && projectId && (filters?.labels ?? []).length > 0
|
||||
? PROJECT_ISSUE_LABELS(projectId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && (filters?.labels ?? []).length > 0
|
||||
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
const { label: labelStore } = useMobxStore();
|
||||
const { labels, loadLabels } = labelStore;
|
||||
|
||||
const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString());
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceSlug && projectId && (filters?.labels ?? []).length > 0)
|
||||
loadLabels(workspaceSlug.toString(), projectId.toString());
|
||||
}, [workspaceSlug, projectId, loadLabels, filters]);
|
||||
|
||||
const handleCreateUpdateView = async (formData: IView) => {
|
||||
await handleFormSubmit(formData);
|
||||
|
||||
@ -211,4 +210,4 @@ export const ViewForm: React.FC<Props> = ({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// services
|
||||
import stateService from "services/state.service";
|
||||
import projectService from "services/project.service";
|
||||
import issuesService from "services/issues.service";
|
||||
// components
|
||||
import { DueDateFilterModal } from "components/core";
|
||||
// ui
|
||||
@ -20,7 +23,7 @@ import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssueFilterOptions, IQuery } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
||||
import { PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { PRIORITIES } from "constants/project";
|
||||
import { DUE_DATES } from "constants/due-dates";
|
||||
@ -32,17 +35,17 @@ type Props = {
|
||||
height?: "sm" | "md" | "rg" | "lg";
|
||||
};
|
||||
|
||||
export const SelectFilters: React.FC<Props> = ({
|
||||
filters,
|
||||
onSelect,
|
||||
direction = "right",
|
||||
height = "md",
|
||||
}) => {
|
||||
export const SelectFilters: React.FC<Props> = observer((props) => {
|
||||
const { filters, onSelect, direction = "right", height = "md" } = props;
|
||||
|
||||
const [isDueDateFilterModalOpen, setIsDueDateFilterModalOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { label: labelStore } = useMobxStore();
|
||||
const { labels, loadLabels } = labelStore;
|
||||
|
||||
const { data: states } = useSWR(
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
@ -58,12 +61,9 @@ export const SelectFilters: React.FC<Props> = ({
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: issueLabels } = useSWR(
|
||||
projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId.toString())
|
||||
: null
|
||||
);
|
||||
useEffect(() => {
|
||||
if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId.toString());
|
||||
}, [workspaceSlug, projectId, loadLabels]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -160,9 +160,9 @@ export const SelectFilters: React.FC<Props> = ({
|
||||
{
|
||||
id: "labels",
|
||||
label: "Labels",
|
||||
value: issueLabels,
|
||||
value: labels,
|
||||
hasChildren: true,
|
||||
children: issueLabels?.map((label) => ({
|
||||
children: labels?.map((label) => ({
|
||||
id: label.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
@ -216,4 +216,4 @@ export const SelectFilters: React.FC<Props> = ({
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -4,6 +4,10 @@ import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
@ -16,7 +20,6 @@ import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
import pagesService from "services/pages.service";
|
||||
import issuesService from "services/issues.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useUser from "hooks/use-user";
|
||||
@ -56,13 +59,12 @@ import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
import { IIssueLabels, IPage, IPageBlock, IProjectMember } from "types";
|
||||
import { IPage, IPageBlock, IProjectMember } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
PAGE_BLOCKS_LIST,
|
||||
PAGE_DETAILS,
|
||||
PROJECT_DETAILS,
|
||||
PROJECT_ISSUE_LABELS,
|
||||
USER_PROJECT_VIEW,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
@ -80,6 +82,9 @@ const SinglePage: NextPage = () => {
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const { label: labelStore } = useMobxStore();
|
||||
const { labels, loadLabels } = labelStore;
|
||||
|
||||
const { handleSubmit, reset, watch, setValue } = useForm<IPage>({
|
||||
defaultValues: { name: "" },
|
||||
});
|
||||
@ -115,13 +120,6 @@ const SinglePage: NextPage = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: labels } = useSWR<IIssueLabels[]>(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: memberDetails } = useSWR(
|
||||
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
|
||||
workspaceSlug && projectId
|
||||
@ -129,6 +127,10 @@ const SinglePage: NextPage = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId.toString());
|
||||
}, [workspaceSlug, projectId, loadLabels]);
|
||||
|
||||
const updatePage = async (formData: IPage) => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
|
||||
@ -691,4 +693,4 @@ const SinglePage: NextPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SinglePage;
|
||||
export default observer(SinglePage);
|
||||
|
@ -1,14 +1,17 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// hooks
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
import issuesService from "services/issues.service";
|
||||
// layouts
|
||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||
// components
|
||||
@ -31,7 +34,7 @@ import emptyLabel from "public/empty-state/label.svg";
|
||||
import { IIssueLabels } from "types";
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
import { PROJECT_DETAILS } from "constants/fetch-keys";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
|
||||
@ -53,6 +56,9 @@ const LabelsSettings: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { label } = useMobxStore();
|
||||
const { labels, isLabelsLoading: isLoading, getLabelChildren, loadLabels } = label;
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
const scrollToRef = useRef<HTMLDivElement>(null);
|
||||
@ -64,12 +70,9 @@ const LabelsSettings: NextPage = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: issueLabels } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
useEffect(() => {
|
||||
if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId.toString());
|
||||
}, [loadLabels, projectId, workspaceSlug]);
|
||||
|
||||
const newLabel = () => {
|
||||
setIsUpdating(false);
|
||||
@ -142,10 +145,10 @@ const LabelsSettings: NextPage = () => {
|
||||
/>
|
||||
)}
|
||||
<>
|
||||
{issueLabels ? (
|
||||
issueLabels.length > 0 ? (
|
||||
issueLabels.map((label) => {
|
||||
const children = issueLabels?.filter((l) => l.parent === label.id);
|
||||
{!isLoading ? (
|
||||
labels.length > 0 ? (
|
||||
labels.map((label) => {
|
||||
const children = getLabelChildren(label.id);
|
||||
|
||||
if (children && children.length === 0) {
|
||||
if (!label.parent)
|
||||
@ -176,7 +179,7 @@ const LabelsSettings: NextPage = () => {
|
||||
behavior: "smooth",
|
||||
});
|
||||
}}
|
||||
handleLabelDelete={() => setSelectDeleteLabel(label)}
|
||||
handleLabelDelete={setSelectDeleteLabel}
|
||||
user={user}
|
||||
/>
|
||||
);
|
||||
@ -210,4 +213,4 @@ const LabelsSettings: NextPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default LabelsSettings;
|
||||
export default observer(LabelsSettings);
|
||||
|
169
apps/app/store/label.ts
Normal file
169
apps/app/store/label.ts
Normal file
@ -0,0 +1,169 @@
|
||||
// mobx
|
||||
import { action, observable, runInAction, makeAutoObservable } from "mobx";
|
||||
|
||||
// services
|
||||
import issueService from "services/issues.service";
|
||||
|
||||
// types
|
||||
import type { IIssueLabels, ICurrentUserResponse, LabelForm } from "types";
|
||||
|
||||
class LabelStore {
|
||||
labels: IIssueLabels[] = [];
|
||||
isLabelsLoading: boolean = false;
|
||||
rootStore: any | null = null;
|
||||
|
||||
constructor(_rootStore: any | null = null) {
|
||||
makeAutoObservable(this, {
|
||||
labels: observable.ref,
|
||||
loadLabels: action,
|
||||
isLabelsLoading: observable,
|
||||
createLabel: action,
|
||||
updateLabel: action,
|
||||
deleteLabel: action,
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Fetch all labels of a project and hydrate labels field
|
||||
*/
|
||||
|
||||
loadLabels = async (workspaceSlug: string, projectId: string) => {
|
||||
this.isLabelsLoading = this.labels.length === 0;
|
||||
try {
|
||||
const labelsResponse: IIssueLabels[] = await issueService.getIssueLabels(
|
||||
workspaceSlug,
|
||||
projectId
|
||||
);
|
||||
|
||||
const _labels = [...(labelsResponse || [])].map((label) => ({
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
description: label.description,
|
||||
color: label.color,
|
||||
parent: label.parent,
|
||||
}));
|
||||
|
||||
runInAction(() => {
|
||||
this.labels = _labels;
|
||||
this.isLabelsLoading = false;
|
||||
});
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.isLabelsLoading = false;
|
||||
});
|
||||
console.error("Fetching labels error", error);
|
||||
}
|
||||
};
|
||||
|
||||
getLabelById = (labelId: string) => this.labels.find((label) => label.id === labelId);
|
||||
|
||||
getLabelChildren = (labelId: string) => this.labels.filter((label) => label.parent === labelId);
|
||||
|
||||
/**
|
||||
* For provided query, this function returns all labels that contain query in their name from the labels store.
|
||||
* @param query - query string
|
||||
* @returns {IIssueLabels[]} array of labels that contain query in their name
|
||||
* @example
|
||||
* getFilteredLabels("labe") // [{ id: "1", name: "label1", description: "", color: "", parent: null }]
|
||||
*/
|
||||
getFilteredLabels = (query: string): IIssueLabels[] =>
|
||||
this.labels.filter((label) => label.name.includes(query));
|
||||
|
||||
createLabel = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
labelForm: LabelForm,
|
||||
user: ICurrentUserResponse
|
||||
) => {
|
||||
try {
|
||||
const labelResponse: IIssueLabels = await issueService.createIssueLabel(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
labelForm,
|
||||
user
|
||||
);
|
||||
|
||||
const _label = [
|
||||
...this.labels,
|
||||
{
|
||||
id: labelResponse.id,
|
||||
name: labelResponse.name,
|
||||
description: labelResponse.description,
|
||||
color: labelResponse.color,
|
||||
parent: labelResponse.parent,
|
||||
},
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
runInAction(() => {
|
||||
this.labels = _label;
|
||||
});
|
||||
return labelResponse;
|
||||
} catch (error) {
|
||||
console.error("Creating label error", error);
|
||||
return error;
|
||||
}
|
||||
};
|
||||
|
||||
updateLabel = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
labelId: string,
|
||||
labelForm: Partial<LabelForm>,
|
||||
user: ICurrentUserResponse
|
||||
) => {
|
||||
try {
|
||||
const labelResponse: IIssueLabels = await issueService.patchIssueLabel(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
labelId,
|
||||
labelForm,
|
||||
user
|
||||
);
|
||||
|
||||
const _labels = [...this.labels]
|
||||
.map((label) => {
|
||||
if (label.id === labelId) {
|
||||
return {
|
||||
id: labelResponse.id,
|
||||
name: labelResponse.name,
|
||||
description: labelResponse.description,
|
||||
color: labelResponse.color,
|
||||
parent: labelResponse.parent,
|
||||
};
|
||||
}
|
||||
return label;
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
runInAction(() => {
|
||||
this.labels = _labels;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Updating label error", error);
|
||||
return error;
|
||||
}
|
||||
};
|
||||
|
||||
deleteLabel = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
labelId: string,
|
||||
user: ICurrentUserResponse
|
||||
) => {
|
||||
try {
|
||||
issueService.deleteIssueLabel(workspaceSlug, projectId, labelId, user);
|
||||
|
||||
const _labels = [...this.labels].filter((label) => label.id !== labelId);
|
||||
|
||||
runInAction(() => {
|
||||
this.labels = _labels;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Deleting label error", error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default LabelStore;
|
@ -3,6 +3,7 @@ import { enableStaticRendering } from "mobx-react-lite";
|
||||
// store imports
|
||||
import UserStore from "./user";
|
||||
import ThemeStore from "./theme";
|
||||
import LabelStore from "./label";
|
||||
import ProjectPublishStore, { IProjectPublishStore } from "./project-publish";
|
||||
|
||||
enableStaticRendering(typeof window === "undefined");
|
||||
@ -10,11 +11,13 @@ enableStaticRendering(typeof window === "undefined");
|
||||
export class RootStore {
|
||||
user;
|
||||
theme;
|
||||
label: LabelStore;
|
||||
projectPublish: IProjectPublishStore;
|
||||
|
||||
constructor() {
|
||||
this.user = new UserStore(this);
|
||||
this.theme = new ThemeStore(this);
|
||||
this.label = new LabelStore(this);
|
||||
this.projectPublish = new ProjectPublishStore(this);
|
||||
}
|
||||
}
|
||||
|
2
apps/app/types/index.d.ts
vendored
2
apps/app/types/index.d.ts
vendored
@ -18,7 +18,7 @@ export * from "./calendar";
|
||||
export * from "./notifications";
|
||||
export * from "./waitlist";
|
||||
export * from "./reaction";
|
||||
|
||||
export * from "./labels";
|
||||
|
||||
export type NestedKeyOf<ObjectType extends object> = {
|
||||
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
|
||||
|
16
apps/app/types/issues.d.ts
vendored
16
apps/app/types/issues.d.ts
vendored
@ -150,22 +150,6 @@ export type IssuePriorities = {
|
||||
user: string;
|
||||
};
|
||||
|
||||
export interface IIssueLabels {
|
||||
id: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
name: string;
|
||||
description: string;
|
||||
color: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
project: string;
|
||||
project_detail: IProjectLite;
|
||||
workspace: string;
|
||||
workspace_detail: IWorkspaceLite;
|
||||
parent: string | null;
|
||||
}
|
||||
|
||||
export interface IIssueActivity {
|
||||
actor: string;
|
||||
actor_detail: IUserLite;
|
||||
|
22
apps/app/types/labels.d.ts
vendored
Normal file
22
apps/app/types/labels.d.ts
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
export interface IIssueLabels {
|
||||
id: string;
|
||||
created_at?: Date;
|
||||
updated_at?: Date;
|
||||
name: string;
|
||||
description: string;
|
||||
color: string;
|
||||
created_by?: string;
|
||||
updated_by?: string;
|
||||
project?: string;
|
||||
project_detail?: IProjectLite;
|
||||
workspace?: string;
|
||||
workspace_detail?: IWorkspaceLite;
|
||||
parent: string | null;
|
||||
}
|
||||
|
||||
export interface LabelForm {
|
||||
name: string;
|
||||
description: string;
|
||||
color: string;
|
||||
parent: string | null;
|
||||
}
|
Loading…
Reference in New Issue
Block a user