Compare commits

...

9 Commits

Author SHA1 Message Date
Dakshesh Jain
f560032fbf fix: merge conflict 2023-08-18 17:55:36 +05:30
Dakshesh Jain
cb341f19c6 fix: reverted to old setup 2023-08-18 17:52:03 +05:30
Dakshesh Jain
0722f1f624 dev: new implementation of label storee 2023-08-18 12:39:08 +05:30
Dakshesh Jain
adcc9e75e8 fix: reverted my changes on a file 2023-08-17 13:59:39 +05:30
Dakshesh Jain
963eeb0a8c refactor: only using runInAction for assignment
refactor: - using single types instead of lite version, - isLoading logic for label store
2023-08-17 13:57:21 +05:30
Dakshesh Jain
37df8684d7 Merge branch 'develop' of https://github.com/makeplane/plane into dev/label_store 2023-08-17 13:37:01 +05:30
Dakshesh Jain
0468e066ff style: displaying label color on issue activity 2023-08-16 17:58:25 +05:30
Dakshesh Jain
e9c3a0642e refactor: using label store to get labels 2023-08-16 17:49:51 +05:30
Dakshesh Jain
f8ab0aa72b dev: label store implementation 2023-08-16 16:53:39 +05:30
22 changed files with 1156 additions and 1018 deletions

View File

@ -53,7 +53,11 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
const activityDetails: { const activityDetails: {
[key: string]: { [key: string]: {
message: (activity: IIssueActivity, showIssue: boolean) => React.ReactNode; message: (
activity: IIssueActivity,
showIssue: boolean,
backgroundColor?: string
) => React.ReactNode;
icon: React.ReactNode; icon: React.ReactNode;
}; };
} = { } = {
@ -253,7 +257,7 @@ const activityDetails: {
icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />,
}, },
labels: { labels: {
message: (activity, showIssue) => { message: (activity, showIssue, backgroundColor = "#000000") => {
if (activity.old_value === "") if (activity.old_value === "")
return ( return (
<> <>
@ -262,7 +266,7 @@ const activityDetails: {
<span <span
className="h-1.5 w-1.5 rounded-full" className="h-1.5 w-1.5 rounded-full"
style={{ style={{
backgroundColor: "#000000", backgroundColor,
}} }}
aria-hidden="true" aria-hidden="true"
/> />
@ -532,14 +536,26 @@ export const ActivityIcon = ({ activity }: { activity: IIssueActivity }) => (
<>{activityDetails[activity.field as keyof typeof activityDetails]?.icon}</> <>{activityDetails[activity.field as keyof typeof activityDetails]?.icon}</>
); );
export const ActivityMessage = ({ type ActivityMessageProps = {
activity,
showIssue = false,
}: {
activity: IIssueActivity; activity: IIssueActivity;
showIssue?: boolean; 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
)}
</>
);
});

View File

@ -10,7 +10,7 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// helpers // helpers
import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types // types
import { IIssueFilterOptions, IIssueLabels, IState, IUserLite, TStateGroups } from "types"; import { IIssueFilterOptions, IState, IUserLite, TStateGroups, IIssueLabels } from "types";
// constants // constants
import { STATE_GROUP_COLORS } from "constants/state"; import { STATE_GROUP_COLORS } from "constants/state";

View File

@ -1,18 +1,21 @@
import React from "react"; import React, { useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// services // services
import issuesService from "services/issues.service";
import projectService from "services/project.service"; import projectService from "services/project.service";
// hooks // hooks
import useProjects from "hooks/use-projects"; import useProjects from "hooks/use-projects";
// component // component
import { Avatar, Icon } from "components/ui"; import { Avatar, Icon } from "components/ui";
// icons // icons
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon, getStateGroupIcon } from "components/icons"; import { getPriorityIcon, getStateGroupIcon } from "components/icons";
// helpers // helpers
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
@ -20,7 +23,7 @@ import { renderEmoji } from "helpers/emoji.helper";
// types // types
import { IIssueViewProps, IState } from "types"; import { IIssueViewProps, IState } from "types";
// fetch-keys // fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys"; import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = { type Props = {
currentState?: IState | null; currentState?: IState | null;
@ -32,162 +35,164 @@ type Props = {
viewProps: IIssueViewProps; viewProps: IIssueViewProps;
}; };
export const BoardHeader: React.FC<Props> = ({ export const BoardHeader: React.FC<Props> = observer(
currentState, ({
groupTitle, currentState,
addIssueToGroup, groupTitle,
isCollapsed, addIssueToGroup,
setIsCollapsed, isCollapsed,
disableUserActions, setIsCollapsed,
viewProps, disableUserActions,
}) => { viewProps,
const router = useRouter(); }) => {
const { workspaceSlug, projectId } = router.query; const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { groupedIssues, groupByProperty: selectedGroup } = viewProps; const { groupedIssues, groupByProperty: selectedGroup } = viewProps;
const { data: issueLabels } = useSWR( const { label: labelStore } = useMobxStore();
workspaceSlug && projectId && selectedGroup === "labels" const { labels, loadLabels } = labelStore;
? PROJECT_ISSUE_LABELS(projectId.toString())
: null,
workspaceSlug && projectId && selectedGroup === "labels"
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
: null
);
const { data: members } = useSWR( const { data: members } = useSWR(
workspaceSlug && projectId && selectedGroup === "created_by" workspaceSlug && projectId && selectedGroup === "created_by"
? PROJECT_MEMBERS(projectId.toString()) ? PROJECT_MEMBERS(projectId.toString())
: null, : null,
workspaceSlug && projectId && selectedGroup === "created_by" workspaceSlug && projectId && selectedGroup === "created_by"
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString()) ? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
: null : null
); );
const { projects } = useProjects(); const { projects } = useProjects();
const getGroupTitle = () => { useEffect(() => {
let title = addSpaceIfCamelCase(groupTitle); if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId.toString());
}, [workspaceSlug, projectId, loadLabels]);
switch (selectedGroup) { const getGroupTitle = () => {
case "state": let title = addSpaceIfCamelCase(groupTitle);
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;
}
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 = () => { return title;
let icon; };
switch (selectedGroup) { const getGroupIcon = () => {
case "state": let icon;
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" />;
break; switch (selectedGroup) {
} case "state":
icon =
return icon; currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
}; break;
case "state_detail.group":
return ( icon = getStateGroupIcon(groupTitle as any, "16", "16");
<div break;
className={`flex items-center justify-between px-1 ${ case "priority":
!isCollapsed ? "flex-col rounded-md bg-custom-background-90" : "" icon = getPriorityIcon(groupTitle, "text-lg");
}`} break;
> case "project":
<div className={`flex items-center ${isCollapsed ? "gap-1" : "flex-col gap-2"}`}> const project = projects?.find((p) => p.id === groupTitle);
<div icon =
className={`flex cursor-pointer items-center gap-x-2 max-w-[316px] ${ project &&
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : "" (project.emoji !== null
}`} ? renderEmoji(project.emoji)
> : project.icon_prop !== null
<span className="flex items-center">{getGroupIcon()}</span> ? renderEmoji(project.icon_prop)
<h2 : null);
className={`text-lg font-semibold truncate ${ break;
selectedGroup === "created_by" ? "" : "capitalize" case "labels":
}`} const labelColor = labels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
style={{ icon = (
writingMode: isCollapsed ? "horizontal-tb" : "vertical-rl", <span
}} className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
> style={{ backgroundColor: labelColor }}
{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"
/> />
) : ( );
<Icon iconName="open_in_full" className="text-base font-medium text-custom-text-900" /> break;
)} case "created_by":
</button> const member = members?.find((member) => member.member.id === groupTitle)?.member;
{!disableUserActions && selectedGroup !== "created_by" && ( 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 <button
type="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" 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> </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>
</div> );
); }
}; );

View File

@ -1,9 +1,13 @@
import { useCallback, useState } from "react"; import { useCallback, useState, useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// react-beautiful-dnd // react-beautiful-dnd
import { DropResult } from "react-beautiful-dnd"; import { DropResult } from "react-beautiful-dnd";
// services // services
@ -37,7 +41,6 @@ import {
MODULE_DETAILS, MODULE_DETAILS,
MODULE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS,
PROJECT_ISSUE_LABELS,
STATES_LIST, STATES_LIST,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
@ -46,10 +49,8 @@ type Props = {
disableUserActions?: boolean; disableUserActions?: boolean;
}; };
export const IssuesView: React.FC<Props> = ({ export const IssuesView: React.FC<Props> = observer((props) => {
openIssuesListModal, const { openIssuesListModal, disableUserActions = false } = props;
disableUserActions = false,
}) => {
// create issue modal // create issue modal
const [createIssueModal, setCreateIssueModal] = useState(false); const [createIssueModal, setCreateIssueModal] = useState(false);
const [createViewModal, setCreateViewModal] = useState<any>(null); const [createViewModal, setCreateViewModal] = useState<any>(null);
@ -75,6 +76,9 @@ export const IssuesView: React.FC<Props> = ({
const { user } = useUserAuth(); const { user } = useUserAuth();
const { label: labelStore } = useMobxStore();
const { labels, loadLabels } = labelStore;
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { const {
@ -99,15 +103,12 @@ export const IssuesView: React.FC<Props> = ({
); );
const states = getStatesList(stateGroups); 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()); const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString());
useEffect(() => {
if (workspaceSlug && projectId) loadLabels(workspaceSlug as string, projectId as string);
}, [loadLabels, projectId, workspaceSlug]);
const handleDeleteIssue = useCallback( const handleDeleteIssue = useCallback(
(issue: IIssue) => { (issue: IIssue) => {
setDeleteIssueModal(true); setDeleteIssueModal(true);
@ -564,4 +565,4 @@ export const IssuesView: React.FC<Props> = ({
/> />
</> </>
); );
}; });

View File

@ -1,11 +1,16 @@
import { useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// headless ui // headless ui
import { Disclosure, Transition } from "@headlessui/react"; import { Disclosure, Transition } from "@headlessui/react";
// services // services
import issuesService from "services/issues.service";
import projectService from "services/project.service"; import projectService from "services/project.service";
// hooks // hooks
import useProjects from "hooks/use-projects"; import useProjects from "hooks/use-projects";
@ -20,16 +25,9 @@ import { getPriorityIcon, getStateGroupIcon } from "components/icons";
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
// types // types
import { import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
ICurrentUserResponse,
IIssue,
IIssueLabels,
IIssueViewProps,
IState,
UserAuth,
} from "types";
// fetch-keys // fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys"; import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = { type Props = {
currentState?: IState | null; currentState?: IState | null;
@ -44,18 +42,20 @@ type Props = {
viewProps: IIssueViewProps; viewProps: IIssueViewProps;
}; };
export const SingleList: React.FC<Props> = ({ export const SingleList: React.FC<Props> = observer((props) => {
currentState, const {
groupTitle, currentState,
addIssueToGroup, groupTitle,
handleIssueAction, addIssueToGroup,
openIssuesListModal, handleIssueAction,
removeIssue, openIssuesListModal,
disableUserActions, removeIssue,
user, disableUserActions,
userAuth, user,
viewProps, userAuth,
}) => { viewProps,
} = props;
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@ -65,12 +65,8 @@ export const SingleList: React.FC<Props> = ({
const { groupByProperty: selectedGroup, groupedIssues } = viewProps; const { groupByProperty: selectedGroup, groupedIssues } = viewProps;
const { data: issueLabels } = useSWR<IIssueLabels[]>( const { label: labelStore } = useMobxStore();
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, const { labels, loadLabels } = labelStore;
workspaceSlug && projectId
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
: null
);
const { data: members } = useSWR( const { data: members } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
@ -81,6 +77,10 @@ export const SingleList: React.FC<Props> = ({
const { projects } = useProjects(); const { projects } = useProjects();
useEffect(() => {
if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId.toString());
}, [workspaceSlug, projectId, loadLabels]);
const getGroupTitle = () => { const getGroupTitle = () => {
let title = addSpaceIfCamelCase(groupTitle); let title = addSpaceIfCamelCase(groupTitle);
@ -89,7 +89,7 @@ export const SingleList: React.FC<Props> = ({
title = addSpaceIfCamelCase(currentState?.name ?? ""); title = addSpaceIfCamelCase(currentState?.name ?? "");
break; break;
case "labels": case "labels":
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None"; title = labels?.find((label) => label.id === groupTitle)?.name ?? "None";
break; break;
case "project": case "project":
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None"; title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
@ -128,8 +128,7 @@ export const SingleList: React.FC<Props> = ({
: null); : null);
break; break;
case "labels": case "labels":
const labelColor = const labelColor = labels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
icon = ( icon = (
<span <span
className="h-3 w-3 flex-shrink-0 rounded-full" className="h-3 w-3 flex-shrink-0 rounded-full"
@ -252,4 +251,4 @@ export const SingleList: React.FC<Props> = ({
)} )}
</Disclosure> </Disclosure>
); );
}; });

View File

@ -1,13 +1,12 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// headless ui // headless ui
import { Combobox, Transition } from "@headlessui/react"; import { Combobox, Transition } from "@headlessui/react";
// services
import issuesServices from "services/issues.service";
// ui // ui
import { IssueLabelsList } from "components/ui"; import { IssueLabelsList } from "components/ui";
// icons // icons
@ -20,8 +19,6 @@ import {
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
// types // types
import type { IIssueLabels } from "types"; import type { IIssueLabels } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
type Props = { type Props = {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
@ -30,179 +27,184 @@ type Props = {
projectId: string; projectId: string;
}; };
export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange, projectId }) => { export const IssueLabelSelect: React.FC<Props> = observer(
// states ({ setIsOpen, value, onChange, projectId }) => {
const [query, setQuery] = useState(""); // states
const [query, setQuery] = useState("");
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { data: issueLabels } = useSWR<IIssueLabels[]>( const { label: labelStore } = useMobxStore();
projectId ? PROJECT_ISSUE_LABELS(projectId) : null, const {
workspaceSlug && projectId isLabelsLoading: isLoading,
? () => issuesServices.getIssueLabels(workspaceSlug as string, projectId) labels,
: null loadLabels,
); getLabelChildren,
getFilteredLabels,
} = labelStore;
const filteredOptions = useEffect(() => {
query === "" if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId);
? issueLabels }, [workspaceSlug, projectId, loadLabels]);
: issueLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase()));
return ( const filteredOptions = getFilteredLabels(query);
<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>
<Transition return (
show={open} <Combobox
as={React.Fragment} as="div"
enter="transition ease-out duration-200" value={value}
enterFrom="opacity-0 translate-y-1" onChange={(val) => onChange(val)}
enterTo="opacity-100 translate-y-0" className="relative flex-shrink-0"
leave="transition ease-in duration-150" multiple
leaveFrom="opacity-100 translate-y-0" >
leaveTo="opacity-0 translate-y-1" {({ open }: any) => (
> <>
<Combobox.Options <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">
className={`absolute z-10 mt-1 max-h-52 min-w-[8rem] overflow-auto rounded-md border-none {value && value.length > 0 ? (
bg-custom-background-90 px-2 py-2 text-xs shadow-md focus:outline-none`} <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"> <Combobox.Options
<MagnifyingGlassIcon className="h-3 w-3 text-custom-text-200" /> className={`absolute z-10 mt-1 max-h-52 min-w-[8rem] overflow-auto rounded-md border-none
<Combobox.Input bg-custom-background-90 px-2 py-2 text-xs shadow-md focus:outline-none`}
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 focus:outline-none" >
onChange={(event) => setQuery(event.target.value)} <div className="flex w-full items-center justify-start rounded-sm border-[0.6px] border-custom-border-200 bg-custom-background-90 px-2">
placeholder="Search for label..." <MagnifyingGlassIcon className="h-3 w-3 text-custom-text-200" />
displayValue={(assigned: any) => assigned?.name} <Combobox.Input
/> className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 focus:outline-none"
</div> onChange={(event) => setQuery(event.target.value)}
<div className="py-1.5"> placeholder="Search for label..."
{issueLabels && filteredOptions ? ( displayValue={(assigned: any) => assigned?.name}
filteredOptions.length > 0 ? ( />
filteredOptions.map((label) => { </div>
const children = issueLabels?.filter((l) => l.parent === label.id); <div className="py-1.5">
{!isLoading && filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((label) => {
const children = getLabelChildren(label.id);
if (children.length === 0) { if (children.length === 0) {
if (!label.parent) if (!label.parent)
return ( return (
<Combobox.Option <Combobox.Option
key={label.id} key={label.id}
className={({ active }) => className={({ active }) =>
`${ `${
active ? "bg-custom-background-80" : "" 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` } 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} value={label.id}
> >
{({ selected }) => ( {({ selected }) => (
<div className="flex w-full justify-between gap-2 rounded"> <div className="flex w-full justify-between gap-2 rounded">
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<span <span
className="h-2.5 w-2.5 flex-shrink-0 rounded-full" className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{ style={{
backgroundColor: label.color, backgroundColor: label.color,
}} }}
/> />
<span>{label.name}</span> <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>
</div> </div>
)} <div className="flex items-center justify-center rounded p-1">
</Combobox.Option> <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>
</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
<p className="px-2 text-xs text-custom-text-200">Loading...</p> type="button"
)} className="flex w-full select-none items-center rounded py-2 px-1 hover:bg-custom-background-80"
<button onClick={() => setIsOpen(true)}
type="button" >
className="flex w-full select-none items-center rounded py-2 px-1 hover:bg-custom-background-80" <span className="flex items-center justify-start gap-1 text-custom-text-200">
onClick={() => setIsOpen(true)} <PlusIcon className="h-4 w-4" aria-hidden="true" />
> <span>Create New Label</span>
<span className="flex items-center justify-start gap-1 text-custom-text-200"> </span>
<PlusIcon className="h-4 w-4" aria-hidden="true" /> </button>
<span>Create New Label</span> </div>
</span> </Combobox.Options>
</button> </Transition>
</div> </>
</Combobox.Options> )}
</Transition> </Combobox>
</> );
)} }
</Combobox> );
);
};

View File

@ -2,7 +2,9 @@ import React, { useEffect, useState } from "react";
import { useRouter } from "next/router"; 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 // react-hook-form
import { Controller, UseFormWatch, useForm } from "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"; import { TwitterPicker } from "react-color";
// headless ui // headless ui
import { Listbox, Popover, Transition } from "@headlessui/react"; import { Listbox, Popover, Transition } from "@headlessui/react";
// services
import issuesService from "services/issues.service";
// hooks // hooks
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
// ui // ui
@ -25,9 +25,7 @@ import {
XMarkIcon, XMarkIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
// types // types
import { IIssue, IIssueLabels } from "types"; import { IIssue, LabelForm } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
type Props = { type Props = {
issueDetails: IIssue | undefined; issueDetails: IIssue | undefined;
@ -38,19 +36,13 @@ type Props = {
uneditable: boolean; uneditable: boolean;
}; };
const defaultValues: Partial<IIssueLabels> = { const defaultValues: Partial<LabelForm> = {
name: "", name: "",
color: "#ff0000", color: "#ff0000",
}; };
export const SidebarLabelSelect: React.FC<Props> = ({ export const SidebarLabelSelect: React.FC<Props> = observer((props) => {
issueDetails, const { issueDetails, issueControl, watchIssue, submitChanges, isNotAllowed, uneditable } = props;
issueControl,
watchIssue,
submitChanges,
isNotAllowed,
uneditable,
}) => {
const [createLabelForm, setCreateLabelForm] = useState(false); const [createLabelForm, setCreateLabelForm] = useState(false);
const router = useRouter(); const router = useRouter();
@ -64,33 +56,38 @@ export const SidebarLabelSelect: React.FC<Props> = ({
watch, watch,
control: labelControl, control: labelControl,
setFocus, setFocus,
} = useForm<Partial<IIssueLabels>>({ } = useForm<LabelForm>({
defaultValues, defaultValues,
}); });
const { user } = useUser(); const { user } = useUser();
const { data: issueLabels, mutate: issueLabelMutate } = useSWR<IIssueLabels[]>( const { label: labelStore } = useMobxStore();
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, const {
workspaceSlug && projectId labels,
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string) loadLabels,
: null createLabel,
); getLabelById,
getLabelChildren,
isLabelsLoading: isLoading,
} = labelStore;
const handleNewLabel = async (formData: Partial<IIssueLabels>) => { useEffect(() => {
if (!workspaceSlug || !projectId || isSubmitting) return; if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId.toString());
}, [workspaceSlug, projectId, loadLabels]);
await issuesService const handleNewLabel = async (formData: LabelForm) => {
.createIssueLabel(workspaceSlug as string, projectId as string, formData, user) if (!workspaceSlug || !projectId || isSubmitting || !user) return;
.then((res) => {
await createLabel(workspaceSlug.toString(), projectId.toString(), formData, user).then(
(res: any) => {
reset(defaultValues); reset(defaultValues);
issueLabelMutate((prevData: any) => [...(prevData ?? []), res], false);
submitChanges({ labels_list: [...(issueDetails?.labels ?? []), res.id] }); submitChanges({ labels_list: [...(issueDetails?.labels ?? []), res.id] });
setCreateLabelForm(false); setCreateLabelForm(false);
}); }
);
}; };
useEffect(() => { useEffect(() => {
@ -110,7 +107,7 @@ export const SidebarLabelSelect: React.FC<Props> = ({
<div className="basis-1/2"> <div className="basis-1/2">
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{watchIssue("labels_list")?.map((labelId) => { {watchIssue("labels_list")?.map((labelId) => {
const label = issueLabels?.find((l) => l.id === labelId); const label = getLabelById(labelId);
if (label) if (label)
return ( 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"> <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"> <div className="py-1">
{issueLabels ? ( {!isLoading ? (
issueLabels.length > 0 ? ( labels.length > 0 ? (
issueLabels.map((label: IIssueLabels) => { labels.map((label) => {
const children = issueLabels?.filter( const children = getLabelChildren(label.id);
(l) => l.parent === label.id
);
if (children.length === 0) { if (children.length === 0) {
if (!label.parent) if (!label.parent)
@ -346,4 +341,4 @@ export const SidebarLabelSelect: React.FC<Props> = ({
)} )}
</div> </div>
); );
}; });

View File

@ -1,11 +1,11 @@
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { useRouter } from "next/router"; 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 // component
import { CreateLabelModal } from "components/labels"; import { CreateLabelModal } from "components/labels";
// ui // ui
@ -13,9 +13,7 @@ import { CustomSearchSelect, Tooltip } from "components/ui";
// icons // icons
import { PlusIcon, TagIcon } from "@heroicons/react/24/outline"; import { PlusIcon, TagIcon } from "@heroicons/react/24/outline";
// types // types
import { ICurrentUserResponse, IIssue, IIssueLabels } from "types"; import { ICurrentUserResponse, IIssue } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
@ -28,29 +26,31 @@ type Props = {
isNotAllowed: boolean; isNotAllowed: boolean;
}; };
export const ViewLabelSelect: React.FC<Props> = ({ export const ViewLabelSelect: React.FC<Props> = observer((props) => {
issue, const {
partialUpdateIssue, issue,
position = "left", partialUpdateIssue,
selfPositioned = false, position = "left",
tooltipPosition = "top", selfPositioned = false,
user, tooltipPosition = "top",
isNotAllowed, user,
customButton = false, isNotAllowed,
}) => { customButton = false,
} = props;
const [labelModal, setLabelModal] = useState(false); const [labelModal, setLabelModal] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { data: issueLabels } = useSWR<IIssueLabels[]>( const { label: labelStore } = useMobxStore();
projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null, const { labels, loadLabels, getLabelById } = labelStore;
workspaceSlug && projectId
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
: null
);
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, value: label.id,
query: label.name, query: label.name,
content: ( content: (
@ -74,7 +74,7 @@ export const ViewLabelSelect: React.FC<Props> = ({
issue.labels.length > 0 issue.labels.length > 0
? issue.labels ? issue.labels
.map((labelId) => { .map((labelId) => {
const label = issueLabels?.find((l) => l.id === labelId); const label = getLabelById(labelId);
return label?.name ?? ""; return label?.name ?? "";
}) })
@ -90,7 +90,7 @@ export const ViewLabelSelect: React.FC<Props> = ({
{issue.labels.length > 0 ? ( {issue.labels.length > 0 ? (
<> <>
{issue.labels.slice(0, 4).map((labelId, index) => { {issue.labels.slice(0, 4).map((labelId, index) => {
const label = issueLabels?.find((l) => l.id === labelId); const label = getLabelById(labelId);
return ( return (
<div className={`flex h-4 w-4 rounded-full ${index ? "-ml-3.5" : ""}`}> <div className={`flex h-4 w-4 rounded-full ${index ? "-ml-3.5" : ""}`}>
@ -154,4 +154,4 @@ export const ViewLabelSelect: React.FC<Props> = ({
/> />
</> </>
); );
}; });

View File

@ -2,7 +2,9 @@ import React, { useEffect } from "react";
import { useRouter } from "next/router"; 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 // react-hook-form
import { Controller, useForm } from "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"; import { TwitterPicker } from "react-color";
// headless ui // headless ui
import { Dialog, Popover, Transition } from "@headlessui/react"; import { Dialog, Popover, Transition } from "@headlessui/react";
// services
import issuesService from "services/issues.service";
// ui // ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui"; import { Input, PrimaryButton, SecondaryButton } from "components/ui";
// icons // icons
import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { ChevronDownIcon } from "@heroicons/react/24/outline";
// types // types
import type { ICurrentUserResponse, IIssueLabels, IState } from "types"; import type { ICurrentUserResponse, IIssueLabels, LabelForm } from "types";
// constants // constants
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label"; import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label";
// types // types
@ -31,179 +30,172 @@ type Props = {
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
}; };
const defaultValues: Partial<IState> = { const defaultValues: Partial<LabelForm> = {
name: "", name: "",
color: "rgb(var(--color-text-200))", color: "rgb(var(--color-text-200))",
}; };
export const CreateLabelModal: React.FC<Props> = ({ export const CreateLabelModal: React.FC<Props> = observer(
isOpen, ({ isOpen, projectId, handleClose, user, onSuccess }) => {
projectId, const router = useRouter();
handleClose, const { workspaceSlug } = router.query;
user,
onSuccess,
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { const { label: labelStore } = useMobxStore();
register, const { createLabel } = labelStore;
formState: { errors, isSubmitting },
handleSubmit,
watch,
control,
reset,
setValue,
} = useForm<IIssueLabels>({
defaultValues,
});
useEffect(() => { const {
if (isOpen) setValue("color", getRandomLabelColor()); register,
}, [setValue, isOpen]); formState: { errors, isSubmitting },
handleSubmit,
watch,
control,
reset,
setValue,
} = useForm<IIssueLabels>({
defaultValues,
});
const onClose = () => { useEffect(() => {
handleClose(); if (isOpen) setValue("color", getRandomLabelColor());
reset(defaultValues); }, [setValue, isOpen]);
};
const onSubmit = async (formData: IIssueLabels) => { const onClose = () => {
if (!workspaceSlug) return; handleClose();
reset(defaultValues);
};
await issuesService const onSubmit = async (formData: LabelForm) => {
.createIssueLabel(workspaceSlug as string, projectId as string, formData, user) if (!workspaceSlug || !user) return;
.then((res) => {
mutate<IIssueLabels[]>(
PROJECT_ISSUE_LABELS(projectId),
(prevData) => [res, ...(prevData ?? [])],
false
);
onClose();
if (onSuccess) onSuccess(res);
})
.catch((error) => {
console.log(error);
});
};
return ( await createLabel(workspaceSlug.toString(), projectId as string, formData, user)
<Transition.Root show={isOpen} as={React.Fragment}> .then((response: any) => {
<Dialog as="div" className="relative z-30" onClose={onClose}> onClose();
<Transition.Child if (onSuccess) onSuccess(response);
as={React.Fragment} })
enter="ease-out duration-300" .catch((error) => {
enterFrom="opacity-0" console.log(error);
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"> return (
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0"> <Transition.Root show={isOpen} as={React.Fragment}>
<Transition.Child <Dialog as="div" className="relative z-30" onClose={onClose}>
as={React.Fragment} <Transition.Child
enter="ease-out duration-300" as={React.Fragment}
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" enter="ease-out duration-300"
enterTo="opacity-100 translate-y-0 sm:scale-100" enterFrom="opacity-0"
leave="ease-in duration-200" enterTo="opacity-100"
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveFrom="opacity-100"
> leaveTo="opacity-0"
<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 className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
<div> </Transition.Child>
<Dialog.Title
as="h3" <div className="fixed inset-0 z-10 overflow-y-auto">
className="text-lg font-medium leading-6 text-custom-text-100" <div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
> <Transition.Child
Create Label as={React.Fragment}
</Dialog.Title> enter="ease-out duration-300"
<div className="mt-8 flex items-center gap-2"> enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
<Popover className="relative"> enterTo="opacity-100 translate-y-0 sm:scale-100"
{({ open, close }) => ( leave="ease-in duration-200"
<> leaveFrom="opacity-100 translate-y-0 sm:scale-100"
<Popover.Button leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
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" <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>
{watch("color") && watch("color") !== "" && ( <Dialog.Title
<span as="h3"
className="ml-2 h-5 w-5 rounded" className="text-lg font-medium leading-6 text-custom-text-100"
style={{ >
backgroundColor: watch("color") ?? "black", Create Label
}} </Dialog.Title>
/> <div className="mt-8 flex items-center gap-2">
)} <Popover className="relative">
<ChevronDownIcon {({ open, close }) => (
className={`ml-2 h-5 w-5 group-hover:text-custom-text-200 ${ <>
open ? "text-gray-600" : "text-gray-400" <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" >
/> {watch("color") && watch("color") !== "" && (
</Popover.Button> <span
className="ml-2 h-5 w-5 rounded"
<Transition style={{
as={React.Fragment} backgroundColor: watch("color") ?? "black",
enter="transition ease-out duration-200" }}
enterFrom="opacity-0 translate-y-1" />
enterTo="opacity-100 translate-y-0" )}
leave="transition ease-in duration-150" <ChevronDownIcon
leaveFrom="opacity-100 translate-y-0" className={`ml-2 h-5 w-5 group-hover:text-custom-text-200 ${
leaveTo="opacity-0 translate-y-1" open ? "text-gray-600" : "text-gray-400"
> }`}
<Popover.Panel className="fixed left-5 z-50 mt-3 w-screen max-w-xs transform px-2 sm:px-0"> aria-hidden="true"
<Controller
name="color"
control={control}
render={({ field: { value, onChange } }) => (
<TwitterPicker
color={value}
colors={LABEL_COLOR_OPTIONS}
onChange={(value) => {
onChange(value.hex);
close();
}}
/>
)}
/> />
</Popover.Panel> </Popover.Button>
</Transition>
</> <Transition
)} as={React.Fragment}
</Popover> enter="transition ease-out duration-200"
<div className="flex w-full flex-col gap-0.5 justify-center"> enterFrom="opacity-0 translate-y-1"
<Input enterTo="opacity-100 translate-y-0"
type="text" leave="transition ease-in duration-150"
id="name" leaveFrom="opacity-100 translate-y-0"
name="name" leaveTo="opacity-0 translate-y-1"
placeholder="Label title" >
autoComplete="off" <Popover.Panel className="fixed left-5 z-50 mt-3 w-screen max-w-xs transform px-2 sm:px-0">
error={errors.name} <Controller
register={register} name="color"
width="full" control={control}
validations={{ render={({ field: { value, onChange } }) => (
required: "Label title is required", <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>
</div> <div className="mt-5 flex justify-end gap-2">
<div className="mt-5 flex justify-end gap-2"> <SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton> <PrimaryButton type="submit" loading={isSubmitting}>
<PrimaryButton type="submit" loading={isSubmitting}> {isSubmitting ? "Creating Label..." : "Create Label"}
{isSubmitting ? "Creating Label..." : "Create Label"} </PrimaryButton>
</PrimaryButton> </div>
</div> </form>
</form> </Dialog.Panel>
</Dialog.Panel> </Transition.Child>
</Transition.Child> </div>
</div> </div>
</div> </Dialog>
</Dialog> </Transition.Root>
</Transition.Root> );
); }
}; );

View File

@ -2,7 +2,9 @@ import React, { forwardRef, useEffect } from "react";
import { useRouter } from "next/router"; 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 // react-hook-form
import { Controller, SubmitHandler, useForm } from "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"; import { TwitterPicker } from "react-color";
// headless ui // headless ui
import { Popover, Transition } from "@headlessui/react"; import { Popover, Transition } from "@headlessui/react";
// services
import issuesService from "services/issues.service";
// ui // ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui"; import { Input, PrimaryButton, SecondaryButton } from "components/ui";
// icons // icons
@ -21,7 +21,6 @@ import { ChevronDownIcon } from "@heroicons/react/24/outline";
// types // types
import { IIssueLabels } from "types"; import { IIssueLabels } from "types";
// fetch-keys // fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label"; import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label";
type Props = { type Props = {
@ -37,13 +36,16 @@ const defaultValues: Partial<IIssueLabels> = {
color: "rgb(var(--color-text-200))", color: "rgb(var(--color-text-200))",
}; };
export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>( export const CreateUpdateLabelInline = observer(
function CreateUpdateLabelInline(props, ref) { forwardRef<HTMLDivElement, Props>(function CreateUpdateLabelInline(props, ref) {
const { labelForm, setLabelForm, isUpdating, labelToUpdate, onClose } = props; const { labelForm, setLabelForm, isUpdating, labelToUpdate, onClose } = props;
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { label: labelStore } = useMobxStore();
const { createLabel, updateLabel } = labelStore;
const { user } = useUserAuth(); const { user } = useUserAuth();
const { const {
@ -65,41 +67,27 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
}; };
const handleLabelCreate: SubmitHandler<IIssueLabels> = async (formData) => { const handleLabelCreate: SubmitHandler<IIssueLabels> = async (formData) => {
if (!workspaceSlug || !projectId || isSubmitting) return; if (!workspaceSlug || !projectId || isSubmitting || !user) return;
await issuesService await createLabel(workspaceSlug.toString(), projectId.toString(), formData, user).finally(
.createIssueLabel(workspaceSlug as string, projectId as string, formData, user) () => {
.then((res) => {
mutate<IIssueLabels[]>(
PROJECT_ISSUE_LABELS(projectId as string),
(prevData) => [res, ...(prevData ?? [])],
false
);
handleClose(); handleClose();
}); }
);
}; };
const handleLabelUpdate: SubmitHandler<IIssueLabels> = async (formData) => { const handleLabelUpdate: SubmitHandler<IIssueLabels> = async (formData) => {
if (!workspaceSlug || !projectId || isSubmitting) return; if (!workspaceSlug || !projectId || isSubmitting || !user) return;
await issuesService await updateLabel(
.patchIssueLabel( workspaceSlug.toString(),
workspaceSlug as string, projectId.toString(),
projectId as string, labelToUpdate?.id ?? "",
labelToUpdate?.id ?? "", formData,
formData, user
user ).finally(() => {
) handleClose();
.then(() => { });
reset(defaultValues);
mutate<IIssueLabels[]>(
PROJECT_ISSUE_LABELS(projectId as string),
(prevData) =>
prevData?.map((p) => (p.id === labelToUpdate?.id ? { ...p, ...formData } : p)),
false
);
handleClose();
});
}; };
useEffect(() => { useEffect(() => {
@ -212,5 +200,5 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
)} )}
</div> </div>
); );
} })
); );

View File

@ -2,22 +2,20 @@ import React, { useState } from "react";
import { useRouter } from "next/router"; 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 // headless ui
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// icons // icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// services
import issuesService from "services/issues.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
import { DangerButton, SecondaryButton } from "components/ui"; import { DangerButton, SecondaryButton } from "components/ui";
// types // types
import type { ICurrentUserResponse, IIssueLabels } from "types"; import type { ICurrentUserResponse, IIssueLabels } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -26,12 +24,15 @@ type Props = {
user: ICurrentUserResponse | undefined; 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 [isDeleteLoading, setIsDeleteLoading] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { label: labelStore } = useMobxStore();
const { deleteLabel } = labelStore;
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const handleClose = () => { const handleClose = () => {
@ -40,28 +41,21 @@ export const DeleteLabelModal: React.FC<Props> = ({ isOpen, onClose, data, user
}; };
const handleDeletion = async () => { const handleDeletion = async () => {
if (!workspaceSlug || !projectId || !data) return; if (!workspaceSlug || !projectId || !data || !user) return;
setIsDeleteLoading(true); setIsDeleteLoading(true);
mutate<IIssueLabels[]>( await deleteLabel(workspaceSlug.toString(), projectId.toString(), data.id, user)
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())
.catch(() => { .catch(() => {
setIsDeleteLoading(false);
mutate(PROJECT_ISSUE_LABELS(projectId.toString()));
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: "Label could not be deleted. Please try again.", 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> </Dialog>
</Transition.Root> </Transition.Root>
); );
}; });

View File

@ -2,18 +2,16 @@ import React, { useState } from "react";
import { useRouter } from "next/router"; 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 // headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react"; import { Combobox, Dialog, Transition } from "@headlessui/react";
// icons // icons
import { RectangleStackIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { RectangleStackIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
// services
import issuesService from "services/issues.service";
// types // types
import { ICurrentUserResponse, IIssueLabels } from "types"; import { ICurrentUserResponse, IIssueLabels } from "types";
// constants
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -22,158 +20,141 @@ type Props = {
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
}; };
export const LabelsListModal: React.FC<Props> = ({ isOpen, handleClose, parent, user }) => { export const LabelsListModal: React.FC<Props> = observer(
const [query, setQuery] = useState(""); ({ isOpen, handleClose, parent, user }) => {
const [query, setQuery] = useState("");
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { data: issueLabels, mutate } = useSWR<IIssueLabels[]>( const { label: labelStore } = useMobxStore();
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, const { updateLabel, getLabelChildren, getFilteredLabels } = labelStore;
workspaceSlug && projectId
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
: null
);
const filteredLabels: IIssueLabels[] = const filteredLabels = getFilteredLabels(query);
query === ""
? issueLabels ?? []
: issueLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())) ?? [];
const handleModalClose = () => { const handleModalClose = () => {
handleClose(); handleClose();
setQuery(""); setQuery("");
}; };
const addChildLabel = async (label: IIssueLabels) => { const addChildLabel = async (label: IIssueLabels) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId || !user) return;
mutate( updateLabel(
(prevData: any) => workspaceSlug.toString(),
prevData?.map((l: any) => { projectId.toString(),
if (l.id === label.id) return { ...l, parent: parent?.id ?? "" };
return l;
}),
false
);
await issuesService
.patchIssueLabel(
workspaceSlug as string,
projectId as string,
label.id, label.id,
{ {
parent: parent?.id ?? "", parent: parent?.id ?? "",
}, },
user user
) );
.then(() => mutate()); };
};
return ( return (
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear> <Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
<Dialog as="div" className="relative z-20" onClose={handleModalClose}> <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">
<Transition.Child <Transition.Child
as={React.Fragment} as={React.Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
enterFrom="opacity-0 scale-95" enterFrom="opacity-0"
enterTo="opacity-100 scale-100" enterTo="opacity-100"
leave="ease-in duration-200" leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100"
leaveTo="opacity-0 scale-95" 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"> <div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
<Combobox> </Transition.Child>
<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>
<Combobox.Options static className="max-h-80 scroll-py-2 overflow-y-auto"> <div className="fixed inset-0 z-20 overflow-y-auto p-4 sm:p-6 md:p-20">
{filteredLabels.length > 0 && ( <Transition.Child
<li className="p-2"> as={React.Fragment}
{query === "" && ( enter="ease-out duration-300"
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-custom-text-100"> enterFrom="opacity-0 scale-95"
Labels enterTo="opacity-100 scale-100"
</h2> leave="ease-in duration-200"
)} leaveFrom="opacity-100 scale-100"
<ul className="text-sm text-gray-700"> leaveTo="opacity-0 scale-95"
{filteredLabels.map((label) => { >
const children = issueLabels?.filter((l) => l.parent === label.id); <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>
if ( <div className="relative m-1">
(label.parent === "" || label.parent === null) && // issue does not have any other parent <MagnifyingGlassIcon
label.id !== parent?.id && // issue is not itself className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-custom-text-100 text-opacity-40"
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" aria-hidden="true"
/> />
<p className="mt-4 text-sm text-custom-text-100"> <Combobox.Input
We couldn{"'"}t find any label with that term. Please try again. 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"
</p> placeholder="Search..."
onChange={(e) => setQuery(e.target.value)}
/>
</div> </div>
)}
</Combobox> <Combobox.Options static className="max-h-80 scroll-py-2 overflow-y-auto">
</Dialog.Panel> {filteredLabels.length > 0 && (
</Transition.Child> <li className="p-2">
</div> {query === "" && (
</Dialog> <h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-custom-text-100">
</Transition.Root> 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>
);
}
);

View File

@ -2,12 +2,12 @@ import React from "react";
import { useRouter } from "next/router"; 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 // headless ui
import { Disclosure, Transition } from "@headlessui/react"; import { Disclosure, Transition } from "@headlessui/react";
// services
import issuesService from "services/issues.service";
// ui // ui
import { CustomMenu } from "components/ui"; import { CustomMenu } from "components/ui";
// icons // icons
@ -21,158 +21,141 @@ import {
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
// types // types
import { ICurrentUserResponse, IIssueLabels } from "types"; import { ICurrentUserResponse, IIssueLabels } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
type Props = { type Props = {
label: IIssueLabels; label: IIssueLabels;
labelChildren: IIssueLabels[]; labelChildren: IIssueLabels[];
addLabelToGroup: (parentLabel: IIssueLabels) => void; addLabelToGroup: (parentLabel: IIssueLabels) => void;
editLabel: (label: IIssueLabels) => void; editLabel: (label: IIssueLabels) => void;
handleLabelDelete: () => void; handleLabelDelete: (label: IIssueLabels) => void;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
}; };
export const SingleLabelGroup: React.FC<Props> = ({ export const SingleLabelGroup: React.FC<Props> = observer(
label, ({ label, labelChildren, addLabelToGroup, editLabel, handleLabelDelete, user }) => {
labelChildren, const router = useRouter();
addLabelToGroup, const { workspaceSlug, projectId } = router.query;
editLabel,
handleLabelDelete,
user,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const removeFromGroup = (label: IIssueLabels) => { const { label: labelStore } = useMobxStore();
if (!workspaceSlug || !projectId) return; const { updateLabel } = labelStore;
mutate<IIssueLabels[]>( const removeFromGroup = (label: IIssueLabels) => {
PROJECT_ISSUE_LABELS(projectId as string), if (!workspaceSlug || !projectId || !user) return;
(prevData) =>
prevData?.map((l) => {
if (l.id === label.id) return { ...l, parent: null };
return l; updateLabel(
}), workspaceSlug.toString(),
false projectId.toString(),
);
issuesService
.patchIssueLabel(
workspaceSlug as string,
projectId as string,
label.id, label.id,
{ {
parent: null, parent: null,
}, },
user user
) );
.then(() => { };
mutate(PROJECT_ISSUE_LABELS(projectId as string));
});
};
return ( return (
<Disclosure <Disclosure
as="div" as="div"
className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5 text-custom-text-100" className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5 text-custom-text-100"
defaultOpen defaultOpen
> >
{({ open }) => ( {({ open }) => (
<> <>
<div className="flex cursor-pointer items-center justify-between gap-2"> <div className="flex cursor-pointer items-center justify-between gap-2">
<div className="flex items-center 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>
<span> <span>
<ChevronDownIcon <RectangleGroupIcon className="h-4 w-4" />
className={`h-4 w-4 text-custom-text-100 ${!open ? "rotate-90 transform" : ""}`}
/>
</span> </span>
</Disclosure.Button> <h6>{label.name}</h6>
</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>
))}
</div> </div>
</Disclosure.Panel> <div className="flex items-center gap-2">
</Transition> <CustomMenu ellipsis>
</> <CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
)} <span className="flex items-center justify-start gap-2">
</Disclosure> <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>
);
}
);

View File

@ -4,6 +4,10 @@ import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// react-hook-form // react-hook-form
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
// services // services
@ -20,9 +24,8 @@ import { checkIfArraysHaveSameElements } from "helpers/array.helper";
import { getStatesList } from "helpers/state.helper"; import { getStatesList } from "helpers/state.helper";
// types // types
import { IQuery, IView } from "types"; import { IQuery, IView } from "types";
import issuesService from "services/issues.service";
// fetch-keys // fetch-keys
import { PROJECT_ISSUE_LABELS, STATES_LIST } from "constants/fetch-keys"; import { STATES_LIST } from "constants/fetch-keys";
type Props = { type Props = {
handleFormSubmit: (values: IView) => Promise<void>; handleFormSubmit: (values: IView) => Promise<void>;
@ -37,13 +40,9 @@ const defaultValues: Partial<IView> = {
description: "", description: "",
}; };
export const ViewForm: React.FC<Props> = ({ export const ViewForm: React.FC<Props> = observer((props) => {
handleFormSubmit, const { handleFormSubmit, handleClose, status, data, preLoadedData } = props;
handleClose,
status,
data,
preLoadedData,
}) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -69,16 +68,16 @@ export const ViewForm: React.FC<Props> = ({
); );
const states = getStatesList(stateGroups); const states = getStatesList(stateGroups);
const { data: labels } = useSWR( const { label: labelStore } = useMobxStore();
workspaceSlug && projectId && (filters?.labels ?? []).length > 0 const { labels, loadLabels } = labelStore;
? PROJECT_ISSUE_LABELS(projectId.toString())
: null,
workspaceSlug && projectId && (filters?.labels ?? []).length > 0
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
: null
);
const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString()); 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) => { const handleCreateUpdateView = async (formData: IView) => {
await handleFormSubmit(formData); await handleFormSubmit(formData);
@ -211,4 +210,4 @@ export const ViewForm: React.FC<Props> = ({
</div> </div>
</form> </form>
); );
}; });

View File

@ -1,13 +1,16 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// services // services
import stateService from "services/state.service"; import stateService from "services/state.service";
import projectService from "services/project.service"; import projectService from "services/project.service";
import issuesService from "services/issues.service";
// components // components
import { DueDateFilterModal } from "components/core"; import { DueDateFilterModal } from "components/core";
// ui // ui
@ -20,7 +23,7 @@ import { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types // types
import { IIssueFilterOptions, IQuery } from "types"; import { IIssueFilterOptions, IQuery } from "types";
// fetch-keys // fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys"; import { PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
// constants // constants
import { PRIORITIES } from "constants/project"; import { PRIORITIES } from "constants/project";
import { DUE_DATES } from "constants/due-dates"; import { DUE_DATES } from "constants/due-dates";
@ -32,17 +35,17 @@ type Props = {
height?: "sm" | "md" | "rg" | "lg"; height?: "sm" | "md" | "rg" | "lg";
}; };
export const SelectFilters: React.FC<Props> = ({ export const SelectFilters: React.FC<Props> = observer((props) => {
filters, const { filters, onSelect, direction = "right", height = "md" } = props;
onSelect,
direction = "right",
height = "md",
}) => {
const [isDueDateFilterModalOpen, setIsDueDateFilterModalOpen] = useState(false); const [isDueDateFilterModalOpen, setIsDueDateFilterModalOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { label: labelStore } = useMobxStore();
const { labels, loadLabels } = labelStore;
const { data: states } = useSWR( const { data: states } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId workspaceSlug && projectId
@ -58,12 +61,9 @@ export const SelectFilters: React.FC<Props> = ({
: null : null
); );
const { data: issueLabels } = useSWR( useEffect(() => {
projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null, if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId.toString());
workspaceSlug && projectId }, [workspaceSlug, projectId, loadLabels]);
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId.toString())
: null
);
return ( return (
<> <>
@ -160,9 +160,9 @@ export const SelectFilters: React.FC<Props> = ({
{ {
id: "labels", id: "labels",
label: "Labels", label: "Labels",
value: issueLabels, value: labels,
hasChildren: true, hasChildren: true,
children: issueLabels?.map((label) => ({ children: labels?.map((label) => ({
id: label.id, id: label.id,
label: ( label: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -216,4 +216,4 @@ export const SelectFilters: React.FC<Props> = ({
/> />
</> </>
); );
}; });

View File

@ -4,6 +4,10 @@ import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// react-hook-form // react-hook-form
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
// headless ui // headless ui
@ -16,7 +20,6 @@ import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// services // services
import projectService from "services/project.service"; import projectService from "services/project.service";
import pagesService from "services/pages.service"; import pagesService from "services/pages.service";
import issuesService from "services/issues.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
@ -56,13 +59,12 @@ import { copyTextToClipboard, truncateText } from "helpers/string.helper";
import { orderArrayBy } from "helpers/array.helper"; import { orderArrayBy } from "helpers/array.helper";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
import { IIssueLabels, IPage, IPageBlock, IProjectMember } from "types"; import { IPage, IPageBlock, IProjectMember } from "types";
// fetch-keys // fetch-keys
import { import {
PAGE_BLOCKS_LIST, PAGE_BLOCKS_LIST,
PAGE_DETAILS, PAGE_DETAILS,
PROJECT_DETAILS, PROJECT_DETAILS,
PROJECT_ISSUE_LABELS,
USER_PROJECT_VIEW, USER_PROJECT_VIEW,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
@ -80,6 +82,9 @@ const SinglePage: NextPage = () => {
const { user } = useUser(); const { user } = useUser();
const { label: labelStore } = useMobxStore();
const { labels, loadLabels } = labelStore;
const { handleSubmit, reset, watch, setValue } = useForm<IPage>({ const { handleSubmit, reset, watch, setValue } = useForm<IPage>({
defaultValues: { name: "" }, defaultValues: { name: "" },
}); });
@ -115,13 +120,6 @@ const SinglePage: NextPage = () => {
: null : 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( const { data: memberDetails } = useSWR(
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null, workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
workspaceSlug && projectId workspaceSlug && projectId
@ -129,6 +127,10 @@ const SinglePage: NextPage = () => {
: null : null
); );
useEffect(() => {
if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId.toString());
}, [workspaceSlug, projectId, loadLabels]);
const updatePage = async (formData: IPage) => { const updatePage = async (formData: IPage) => {
if (!workspaceSlug || !projectId || !pageId) return; if (!workspaceSlug || !projectId || !pageId) return;
@ -691,4 +693,4 @@ const SinglePage: NextPage = () => {
); );
}; };
export default SinglePage; export default observer(SinglePage);

View File

@ -1,14 +1,17 @@
import React, { useState, useRef } from "react"; import React, { useState, useRef, useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
// services // services
import projectService from "services/project.service"; import projectService from "services/project.service";
import issuesService from "services/issues.service";
// layouts // layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// components // components
@ -31,7 +34,7 @@ import emptyLabel from "public/empty-state/label.svg";
import { IIssueLabels } from "types"; import { IIssueLabels } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; import { PROJECT_DETAILS } from "constants/fetch-keys";
// helper // helper
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
@ -53,6 +56,9 @@ const LabelsSettings: NextPage = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { label } = useMobxStore();
const { labels, isLabelsLoading: isLoading, getLabelChildren, loadLabels } = label;
const { user } = useUserAuth(); const { user } = useUserAuth();
const scrollToRef = useRef<HTMLDivElement>(null); const scrollToRef = useRef<HTMLDivElement>(null);
@ -64,12 +70,9 @@ const LabelsSettings: NextPage = () => {
: null : null
); );
const { data: issueLabels } = useSWR( useEffect(() => {
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId.toString());
workspaceSlug && projectId }, [loadLabels, projectId, workspaceSlug]);
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
: null
);
const newLabel = () => { const newLabel = () => {
setIsUpdating(false); setIsUpdating(false);
@ -142,10 +145,10 @@ const LabelsSettings: NextPage = () => {
/> />
)} )}
<> <>
{issueLabels ? ( {!isLoading ? (
issueLabels.length > 0 ? ( labels.length > 0 ? (
issueLabels.map((label) => { labels.map((label) => {
const children = issueLabels?.filter((l) => l.parent === label.id); const children = getLabelChildren(label.id);
if (children && children.length === 0) { if (children && children.length === 0) {
if (!label.parent) if (!label.parent)
@ -176,7 +179,7 @@ const LabelsSettings: NextPage = () => {
behavior: "smooth", behavior: "smooth",
}); });
}} }}
handleLabelDelete={() => setSelectDeleteLabel(label)} handleLabelDelete={setSelectDeleteLabel}
user={user} user={user}
/> />
); );
@ -210,4 +213,4 @@ const LabelsSettings: NextPage = () => {
); );
}; };
export default LabelsSettings; export default observer(LabelsSettings);

169
apps/app/store/label.ts Normal file
View 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;

View File

@ -3,6 +3,7 @@ import { enableStaticRendering } from "mobx-react-lite";
// store imports // store imports
import UserStore from "./user"; import UserStore from "./user";
import ThemeStore from "./theme"; import ThemeStore from "./theme";
import LabelStore from "./label";
import ProjectPublishStore, { IProjectPublishStore } from "./project-publish"; import ProjectPublishStore, { IProjectPublishStore } from "./project-publish";
enableStaticRendering(typeof window === "undefined"); enableStaticRendering(typeof window === "undefined");
@ -10,11 +11,13 @@ enableStaticRendering(typeof window === "undefined");
export class RootStore { export class RootStore {
user; user;
theme; theme;
label: LabelStore;
projectPublish: IProjectPublishStore; projectPublish: IProjectPublishStore;
constructor() { constructor() {
this.user = new UserStore(this); this.user = new UserStore(this);
this.theme = new ThemeStore(this); this.theme = new ThemeStore(this);
this.label = new LabelStore(this);
this.projectPublish = new ProjectPublishStore(this); this.projectPublish = new ProjectPublishStore(this);
} }
} }

View File

@ -18,7 +18,7 @@ export * from "./calendar";
export * from "./notifications"; export * from "./notifications";
export * from "./waitlist"; export * from "./waitlist";
export * from "./reaction"; export * from "./reaction";
export * from "./labels";
export type NestedKeyOf<ObjectType extends object> = { export type NestedKeyOf<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object

View File

@ -150,22 +150,6 @@ export type IssuePriorities = {
user: string; 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 { export interface IIssueActivity {
actor: string; actor: string;
actor_detail: IUserLite; actor_detail: IUserLite;

22
apps/app/types/labels.d.ts vendored Normal file
View 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;
}