feat: my issues view layouts and filters, refactor: issue views (#1681)

* refactor: issue views and my issues

* chore: update view dropdown options

* refactor: render emoji function

* refactor: api calss

* fix: build errors

* fix: fetch states only when dropdown is opened

* chore: my issues dnd

* fix: build errors

* refactor: folder structure
This commit is contained in:
Aaryan Khandelwal 2023-07-26 17:51:26 +05:30 committed by GitHub
parent ec62308195
commit 3d7fe40035
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 2249 additions and 1430 deletions

View File

@ -227,12 +227,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
</span> </span>
) : project.icon_prop ? ( ) : project.icon_prop ? (
<div className="h-6 w-6 grid place-items-center flex-shrink-0"> <div className="h-6 w-6 grid place-items-center flex-shrink-0">
<span {renderEmoji(project.icon_prop)}
style={{ color: project.icon_prop.color }}
className="material-symbols-rounded text-lg"
>
{project.icon_prop.name}
</span>
</div> </div>
) : ( ) : (
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white"> <span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
@ -342,12 +337,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
</div> </div>
) : projectDetails?.icon_prop ? ( ) : projectDetails?.icon_prop ? (
<div className="h-6 w-6 grid place-items-center flex-shrink-0"> <div className="h-6 w-6 grid place-items-center flex-shrink-0">
<span {renderEmoji(projectDetails.icon_prop)}
style={{ color: projectDetails.icon_prop.color }}
className="material-symbols-rounded text-lg"
>
{projectDetails.icon_prop.name}
</span>
</div> </div>
) : ( ) : (
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white"> <span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr";
// icons // icons
import { XMarkIcon } from "@heroicons/react/24/outline"; import { XMarkIcon } from "@heroicons/react/24/outline";
@ -8,42 +8,31 @@ import { getPriorityIcon, getStateGroupIcon } from "components/icons";
// ui // ui
import { Avatar } from "components/ui"; import { Avatar } from "components/ui";
// helpers // helpers
import { getStatesList } from "helpers/state.helper";
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// services // helpers
import issuesService from "services/issues.service";
import projectService from "services/project.service";
import stateService from "services/state.service";
// types
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
import { IIssueFilterOptions } from "types";
import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types
import { IIssueFilterOptions, IIssueLabels, IState, IUserLite } from "types";
export const FilterList: React.FC<any> = ({ filters, setFilters }) => { type Props = {
filters: any;
setFilters: any;
clearAllFilters: (...args: any) => void;
labels: IIssueLabels[] | undefined;
members: IUserLite[] | undefined;
states: IState[] | undefined;
};
export const FiltersList: React.FC<Props> = ({
filters,
setFilters,
clearAllFilters,
labels,
members,
states,
}) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query; const { viewId } = router.query;
const { data: members } = useSWR(
projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null
);
const { data: issueLabels } = useSWR(
projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null,
workspaceSlug && projectId
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId.toString())
: null
);
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
const states = getStatesList(stateGroups ?? {});
if (!filters) return <></>; if (!filters) return <></>;
@ -166,7 +155,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
) : key === "assignees" ? ( ) : key === "assignees" ? (
<div className="flex flex-wrap items-center gap-1"> <div className="flex flex-wrap items-center gap-1">
{filters.assignees?.map((memberId: string) => { {filters.assignees?.map((memberId: string) => {
const member = members?.find((m) => m.member.id === memberId)?.member; const member = members?.find((m) => m.id === memberId);
return ( return (
<div <div
@ -207,7 +196,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
) : key === "created_by" ? ( ) : key === "created_by" ? (
<div className="flex flex-wrap items-center gap-1"> <div className="flex flex-wrap items-center gap-1">
{filters.created_by?.map((memberId: string) => { {filters.created_by?.map((memberId: string) => {
const member = members?.find((m) => m.member.id === memberId)?.member; const member = members?.find((m) => m.id === memberId);
return ( return (
<div <div
@ -248,7 +237,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
) : key === "labels" ? ( ) : key === "labels" ? (
<div className="flex flex-wrap items-center gap-1"> <div className="flex flex-wrap items-center gap-1">
{filters.labels?.map((labelId: string) => { {filters.labels?.map((labelId: string) => {
const label = issueLabels?.find((l) => l.id === labelId); const label = labels?.find((l) => l.id === labelId);
if (!label) return null; if (!label) return null;
const color = label.color !== "" ? label.color : "#0f172a"; const color = label.color !== "" ? label.color : "#0f172a";
@ -370,17 +359,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
{Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && ( {Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && (
<button <button
type="button" type="button"
onClick={() => onClick={clearAllFilters}
setFilters({
type: null,
state: null,
priority: null,
assignees: null,
labels: null,
created_by: null,
target_date: null,
})
}
className="flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-80 px-3 py-1.5 text-xs" className="flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-80 px-3 py-1.5 text-xs"
> >
<span>Clear all filters</span> <span>Clear all filters</span>

View File

@ -187,16 +187,19 @@ export const IssuesFilterView: React.FC = () => {
?.name ?? "Select" ?.name ?? "Select"
} }
> >
{GROUP_BY_OPTIONS.map((option) => {GROUP_BY_OPTIONS.map((option) => {
issueView === "kanban" && option.key === null ? null : ( if (issueView === "kanban" && option.key === null) return null;
if (option.key === "project") return null;
return (
<CustomMenu.MenuItem <CustomMenu.MenuItem
key={option.key} key={option.key}
onClick={() => setGroupByProperty(option.key)} onClick={() => setGroupByProperty(option.key)}
> >
{option.name} {option.name}
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
) );
)} })}
</CustomMenu> </CustomMenu>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">

View File

@ -1,12 +1,7 @@
export * from "./board-view";
export * from "./calendar-view";
export * from "./filters"; export * from "./filters";
export * from "./gantt-chart-view";
export * from "./list-view";
export * from "./modals"; export * from "./modals";
export * from "./spreadsheet-view";
export * from "./theme";
export * from "./sidebar"; export * from "./sidebar";
export * from "./issues-view"; export * from "./theme";
export * from "./image-picker-popover"; export * from "./views";
export * from "./feeds"; export * from "./feeds";
export * from "./image-picker-popover";

View File

@ -1,72 +0,0 @@
// hooks
import useIssuesView from "hooks/use-issues-view";
// components
import { SingleList } from "components/core/list-view/single-list";
// types
import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types";
// types
type Props = {
type: "issue" | "cycle" | "module";
states: IState[] | undefined;
addIssueToState: (groupTitle: string) => void;
makeIssueCopy: (issue: IIssue) => void;
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
};
export const AllLists: React.FC<Props> = ({
type,
states,
addIssueToState,
makeIssueCopy,
openIssuesListModal,
handleEditIssue,
handleDeleteIssue,
removeIssue,
isCompleted = false,
user,
userAuth,
}) => {
const { groupedByIssues, groupByProperty: selectedGroup, showEmptyGroups } = useIssuesView();
return (
<>
{groupedByIssues && (
<div className="h-full overflow-y-auto">
{Object.keys(groupedByIssues).map((singleGroup) => {
const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
if (!showEmptyGroups && groupedByIssues[singleGroup].length === 0) return null;
return (
<SingleList
key={singleGroup}
type={type}
groupTitle={singleGroup}
groupedByIssues={groupedByIssues}
selectedGroup={selectedGroup}
currentState={currentState}
addIssueToState={() => addIssueToState(singleGroup)}
makeIssueCopy={makeIssueCopy}
handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
removeIssue={removeIssue}
isCompleted={isCompleted}
user={user}
userAuth={userAuth}
/>
);
})}
</div>
)}
</>
);
};

View File

@ -0,0 +1,203 @@
import React, { useCallback } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// react-beautiful-dnd
import { DragDropContext, DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// services
import stateService from "services/state.service";
// hooks
import useUser from "hooks/use-user";
import { useProjectMyMembership } from "contexts/project-member.context";
// components
import {
AllLists,
AllBoards,
CalendarView,
SpreadsheetView,
GanttChartView,
} from "components/core";
// ui
import { EmptyState, SecondaryButton, Spinner } from "components/ui";
// icons
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
// images
import emptyIssue from "public/empty-state/issue.svg";
import emptyIssueArchive from "public/empty-state/issue-archive.svg";
// helpers
import { getStatesList } from "helpers/state.helper";
// types
import { IIssue, IIssueViewProps } from "types";
// fetch-keys
import { STATES_LIST } from "constants/fetch-keys";
type Props = {
addIssueToDate: (date: string) => void;
addIssueToGroup: (groupTitle: string) => void;
disableUserActions: boolean;
dragDisabled?: boolean;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleOnDragEnd: (result: DropResult) => Promise<void>;
openIssuesListModal: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
trashBox: boolean;
setTrashBox: React.Dispatch<React.SetStateAction<boolean>>;
viewProps: IIssueViewProps;
};
export const AllViews: React.FC<Props> = ({
addIssueToDate,
addIssueToGroup,
disableUserActions,
dragDisabled = false,
handleIssueAction,
handleOnDragEnd,
openIssuesListModal,
removeIssue,
trashBox,
setTrashBox,
viewProps,
}) => {
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { user } = useUser();
const { memberRole } = useProjectMyMembership();
const { groupedIssues, isEmpty, issueView } = viewProps;
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
const states = getStatesList(stateGroups ?? {});
const handleTrashBox = useCallback(
(isDragging: boolean) => {
if (isDragging && !trashBox) setTrashBox(true);
},
[trashBox, setTrashBox]
);
return (
<DragDropContext onDragEnd={handleOnDragEnd}>
<StrictModeDroppable droppableId="trashBox">
{(provided, snapshot) => (
<div
className={`${
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
} fixed top-4 left-1/2 -translate-x-1/2 z-40 w-72 flex items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-custom-background-100 px-3 py-5 text-xs font-medium italic text-red-500 ${
snapshot.isDraggingOver ? "bg-red-500 blur-2xl opacity-70" : ""
} transition duration-300`}
ref={provided.innerRef}
{...provided.droppableProps}
>
<TrashIcon className="h-4 w-4" />
Drop here to delete the issue.
</div>
)}
</StrictModeDroppable>
{groupedIssues ? (
!isEmpty || issueView === "kanban" || issueView === "calendar" ? (
<>
{issueView === "list" ? (
<AllLists
states={states}
addIssueToGroup={addIssueToGroup}
handleIssueAction={handleIssueAction}
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
removeIssue={removeIssue}
disableUserActions={disableUserActions}
user={user}
userAuth={memberRole}
viewProps={viewProps}
/>
) : issueView === "kanban" ? (
<AllBoards
addIssueToGroup={addIssueToGroup}
disableUserActions={disableUserActions}
dragDisabled={dragDisabled}
handleIssueAction={handleIssueAction}
handleTrashBox={handleTrashBox}
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
removeIssue={removeIssue}
states={states}
user={user}
userAuth={memberRole}
viewProps={viewProps}
/>
) : issueView === "calendar" ? (
<CalendarView
handleIssueAction={handleIssueAction}
addIssueToDate={addIssueToDate}
disableUserActions={disableUserActions}
user={user}
userAuth={memberRole}
/>
) : issueView === "spreadsheet" ? (
<SpreadsheetView
handleIssueAction={handleIssueAction}
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
disableUserActions={disableUserActions}
user={user}
userAuth={memberRole}
/>
) : (
issueView === "gantt_chart" && <GanttChartView />
)}
</>
) : router.pathname.includes("archived-issues") ? (
<EmptyState
title="Archived Issues will be shown here"
description="All the issues that have been in the completed or canceled groups for the configured period of time can be viewed here."
image={emptyIssueArchive}
buttonText="Go to Automation Settings"
onClick={() => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`);
}}
/>
) : (
<EmptyState
title={
cycleId
? "Cycle issues will appear here"
: moduleId
? "Module issues will appear here"
: "Project issues will appear here"
}
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
buttonText="New Issue"
buttonIcon={<PlusIcon className="h-4 w-4" />}
secondaryButton={
cycleId || moduleId ? (
<SecondaryButton
className="flex items-center gap-1.5"
onClick={openIssuesListModal ?? (() => {})}
>
<PlusIcon className="h-4 w-4" />
Add an existing issue
</SecondaryButton>
) : null
}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
}}
/>
)
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}
</DragDropContext>
);
};

View File

@ -1,75 +1,66 @@
// hooks
import useProjectIssuesView from "hooks/use-issues-view";
// components // components
import { SingleBoard } from "components/core/board-view/single-board"; import { SingleBoard } from "components/core/views/board-view/single-board";
// icons // icons
import { getStateGroupIcon } from "components/icons"; import { getStateGroupIcon } from "components/icons";
// helpers // helpers
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
// types // types
import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types"; import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
type Props = { type Props = {
type: "issue" | "cycle" | "module"; addIssueToGroup: (groupTitle: string) => void;
states: IState[] | undefined; disableUserActions: boolean;
addIssueToState: (groupTitle: string) => void; dragDisabled: boolean;
makeIssueCopy: (issue: IIssue) => void; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleEditIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
handleTrashBox: (isDragging: boolean) => void; handleTrashBox: (isDragging: boolean) => void;
openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean; states: IState[] | undefined;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
userAuth: UserAuth; userAuth: UserAuth;
viewProps: IIssueViewProps;
}; };
export const AllBoards: React.FC<Props> = ({ export const AllBoards: React.FC<Props> = ({
type, addIssueToGroup,
states, disableUserActions,
addIssueToState, dragDisabled,
makeIssueCopy, handleIssueAction,
handleEditIssue,
openIssuesListModal,
handleDeleteIssue,
handleTrashBox, handleTrashBox,
openIssuesListModal,
removeIssue, removeIssue,
isCompleted = false, states,
user, user,
userAuth, userAuth,
viewProps,
}) => { }) => {
const { const { groupByProperty: selectedGroup, groupedIssues, showEmptyGroups } = viewProps;
groupedByIssues,
groupByProperty: selectedGroup,
showEmptyGroups,
} = useProjectIssuesView();
return ( return (
<> <>
{groupedByIssues ? ( {groupedIssues ? (
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8"> <div className="horizontal-scroll-enable flex h-full gap-x-4 p-8">
{Object.keys(groupedByIssues).map((singleGroup, index) => { {Object.keys(groupedIssues).map((singleGroup, index) => {
const currentState = const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null; selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
if (!showEmptyGroups && groupedByIssues[singleGroup].length === 0) return null; if (!showEmptyGroups && groupedIssues[singleGroup].length === 0) return null;
return ( return (
<SingleBoard <SingleBoard
key={index} key={index}
type={type} addIssueToGroup={() => addIssueToGroup(singleGroup)}
currentState={currentState} currentState={currentState}
disableUserActions={disableUserActions}
dragDisabled={dragDisabled}
groupTitle={singleGroup} groupTitle={singleGroup}
handleEditIssue={handleEditIssue} handleIssueAction={handleIssueAction}
makeIssueCopy={makeIssueCopy}
addIssueToState={() => addIssueToState(singleGroup)}
handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={openIssuesListModal ?? null}
handleTrashBox={handleTrashBox} handleTrashBox={handleTrashBox}
openIssuesListModal={openIssuesListModal ?? null}
removeIssue={removeIssue} removeIssue={removeIssue}
isCompleted={isCompleted}
user={user} user={user}
userAuth={userAuth} userAuth={userAuth}
viewProps={viewProps}
/> />
); );
})} })}
@ -77,11 +68,11 @@ export const AllBoards: React.FC<Props> = ({
<div className="h-full w-96 flex-shrink-0 space-y-2 p-1"> <div className="h-full w-96 flex-shrink-0 space-y-2 p-1">
<h2 className="text-lg font-semibold">Hidden groups</h2> <h2 className="text-lg font-semibold">Hidden groups</h2>
<div className="space-y-3"> <div className="space-y-3">
{Object.keys(groupedByIssues).map((singleGroup, index) => { {Object.keys(groupedIssues).map((singleGroup, index) => {
const currentState = const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null; selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
if (groupedByIssues[singleGroup].length === 0) if (groupedIssues[singleGroup].length === 0)
return ( return (
<div <div
key={index} key={index}

View File

@ -8,7 +8,7 @@ import useSWR from "swr";
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
import projectService from "services/project.service"; import projectService from "services/project.service";
// hooks // hooks
import useIssuesView from "hooks/use-issues-view"; import useProjects from "hooks/use-projects";
// component // component
import { Avatar } from "components/ui"; import { Avatar } from "components/ui";
// icons // icons
@ -16,47 +16,56 @@ import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicon
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";
import { renderEmoji } from "helpers/emoji.helper";
// types // types
import { IIssueLabels, IState } from "types"; import { IIssueViewProps, IState } from "types";
// fetch-keys // fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys"; import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = { type Props = {
currentState?: IState | null; currentState?: IState | null;
groupTitle: string; groupTitle: string;
addIssueToState: () => void; addIssueToGroup: () => void;
isCollapsed: boolean; isCollapsed: boolean;
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>; setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
isCompleted?: boolean; disableUserActions: boolean;
viewProps: IIssueViewProps;
}; };
export const BoardHeader: React.FC<Props> = ({ export const BoardHeader: React.FC<Props> = ({
currentState, currentState,
groupTitle, groupTitle,
addIssueToState, addIssueToGroup,
isCollapsed, isCollapsed,
setIsCollapsed, setIsCollapsed,
isCompleted = false, disableUserActions,
viewProps,
}) => { }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { groupedByIssues, groupByProperty: selectedGroup } = useIssuesView(); const { groupedIssues, groupByProperty: selectedGroup } = viewProps;
const { data: issueLabels } = useSWR<IIssueLabels[]>( const { data: issueLabels } = useSWR(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, workspaceSlug && projectId && selectedGroup === "labels"
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId.toString())
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string) : null,
workspaceSlug && projectId && selectedGroup === "labels"
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
: null : null
); );
const { data: members } = useSWR( const { data: members } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug && projectId && selectedGroup === "created_by"
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId.toString())
? () => projectService.projectMembers(workspaceSlug as string, projectId as string) : null,
workspaceSlug && projectId && selectedGroup === "created_by"
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
: null : null
); );
const { projects } = useProjects();
const getGroupTitle = () => { const getGroupTitle = () => {
let title = addSpaceIfCamelCase(groupTitle); let title = addSpaceIfCamelCase(groupTitle);
@ -67,6 +76,9 @@ export const BoardHeader: React.FC<Props> = ({
case "labels": case "labels":
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None"; title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
break; break;
case "project":
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
break;
case "created_by": case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member; const member = members?.find((member) => member.member.id === groupTitle)?.member;
title = title =
@ -87,9 +99,22 @@ export const BoardHeader: React.FC<Props> = ({
icon = icon =
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color); currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
break; break;
case "state_detail.group":
icon = getStateGroupIcon(groupTitle as any, "16", "16");
break;
case "priority": case "priority":
icon = getPriorityIcon(groupTitle, "text-lg"); icon = getPriorityIcon(groupTitle, "text-lg");
break; 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": case "labels":
const labelColor = const labelColor =
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000"; issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
@ -116,7 +141,7 @@ export const BoardHeader: React.FC<Props> = ({
!isCollapsed ? "flex-col rounded-md bg-custom-background-90" : "" !isCollapsed ? "flex-col rounded-md bg-custom-background-90" : ""
}`} }`}
> >
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}> <div className={`flex items-center ${isCollapsed ? "gap-1" : "flex-col gap-2"}`}>
<div <div
className={`flex cursor-pointer items-center gap-x-3 max-w-[316px] ${ className={`flex cursor-pointer items-center gap-x-3 max-w-[316px] ${
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : "" !isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
@ -126,7 +151,7 @@ export const BoardHeader: React.FC<Props> = ({
<h2 <h2
className="text-lg font-semibold capitalize truncate" className="text-lg font-semibold capitalize truncate"
style={{ style={{
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb", writingMode: isCollapsed ? "horizontal-tb" : "vertical-rl",
}} }}
> >
{getGroupTitle()} {getGroupTitle()}
@ -136,7 +161,7 @@ export const BoardHeader: React.FC<Props> = ({
isCollapsed ? "ml-0.5" : "" isCollapsed ? "ml-0.5" : ""
} min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs`} } min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs`}
> >
{groupedByIssues?.[groupTitle].length ?? 0} {groupedIssues?.[groupTitle].length ?? 0}
</span> </span>
</div> </div>
</div> </div>
@ -155,11 +180,11 @@ export const BoardHeader: React.FC<Props> = ({
<ArrowsPointingOutIcon className="h-4 w-4" /> <ArrowsPointingOutIcon className="h-4 w-4" />
)} )}
</button> </button>
{!isCompleted && selectedGroup !== "created_by" && ( {!disableUserActions && selectedGroup !== "created_by" && (
<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={addIssueToState} onClick={addIssueToGroup}
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
</button> </button>

View File

@ -5,9 +5,6 @@ import { useRouter } from "next/router";
// react-beautiful-dnd // react-beautiful-dnd
import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { Draggable } from "react-beautiful-dnd"; import { Draggable } from "react-beautiful-dnd";
// hooks
import useIssuesView from "hooks/use-issues-view";
import useIssuesProperties from "hooks/use-issue-properties";
// components // components
import { BoardHeader, SingleBoardIssue } from "components/core"; import { BoardHeader, SingleBoardIssue } from "components/core";
// ui // ui
@ -17,64 +14,63 @@ import { PlusIcon } from "@heroicons/react/24/outline";
// helpers // helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types // types
import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types"; import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
type Props = { type Props = {
type?: "issue" | "cycle" | "module"; addIssueToGroup: () => void;
currentState?: IState | null; currentState?: IState | null;
disableUserActions: boolean;
dragDisabled: boolean;
groupTitle: string; groupTitle: string;
handleEditIssue: (issue: IIssue) => void; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
makeIssueCopy: (issue: IIssue) => void;
addIssueToState: () => void;
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
handleTrashBox: (isDragging: boolean) => void; handleTrashBox: (isDragging: boolean) => void;
openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
userAuth: UserAuth; userAuth: UserAuth;
viewProps: IIssueViewProps;
}; };
export const SingleBoard: React.FC<Props> = ({ export const SingleBoard: React.FC<Props> = ({
type, addIssueToGroup,
currentState, currentState,
groupTitle, groupTitle,
handleEditIssue, disableUserActions,
makeIssueCopy, dragDisabled,
addIssueToState, handleIssueAction,
handleDeleteIssue,
openIssuesListModal,
handleTrashBox, handleTrashBox,
openIssuesListModal,
removeIssue, removeIssue,
isCompleted = false,
user, user,
userAuth, userAuth,
viewProps,
}) => { }) => {
// collapse/expand // collapse/expand
const [isCollapsed, setIsCollapsed] = useState(true); const [isCollapsed, setIsCollapsed] = useState(true);
const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useIssuesView(); const { groupedIssues, groupByProperty: selectedGroup, orderBy, properties } = viewProps;
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { cycleId, moduleId } = router.query;
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
// Check if it has at least 4 tickets since it is enough to accommodate the Calendar height // Check if it has at least 4 tickets since it is enough to accommodate the Calendar height
const issuesLength = groupedByIssues?.[groupTitle].length; const issuesLength = groupedIssues?.[groupTitle].length;
const hasMinimumNumberOfCards = issuesLength ? issuesLength >= 4 : false; const hasMinimumNumberOfCards = issuesLength ? issuesLength >= 4 : false;
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted; const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
return ( return (
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}> <div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
<BoardHeader <BoardHeader
addIssueToState={addIssueToState} addIssueToGroup={addIssueToGroup}
currentState={currentState} currentState={currentState}
groupTitle={groupTitle} groupTitle={groupTitle}
isCollapsed={isCollapsed} isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed} setIsCollapsed={setIsCollapsed}
isCompleted={isCompleted} disableUserActions={disableUserActions}
viewProps={viewProps}
/> />
{isCollapsed && ( {isCollapsed && (
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}> <StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
@ -112,14 +108,12 @@ export const SingleBoard: React.FC<Props> = ({
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : "" hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
} `} } `}
> >
{groupedByIssues?.[groupTitle].map((issue, index) => ( {groupedIssues?.[groupTitle].map((issue, index) => (
<Draggable <Draggable
key={issue.id} key={issue.id}
draggableId={issue.id} draggableId={issue.id}
index={index} index={index}
isDragDisabled={ isDragDisabled={isNotAllowed || dragDisabled}
isNotAllowed || selectedGroup === "created_by" || selectedGroup === "labels"
}
> >
{(provided, snapshot) => ( {(provided, snapshot) => (
<SingleBoardIssue <SingleBoardIssue
@ -128,21 +122,20 @@ export const SingleBoard: React.FC<Props> = ({
snapshot={snapshot} snapshot={snapshot}
type={type} type={type}
index={index} index={index}
selectedGroup={selectedGroup}
issue={issue} issue={issue}
groupTitle={groupTitle} groupTitle={groupTitle}
properties={properties} editIssue={() => handleIssueAction(issue, "edit")}
editIssue={() => handleEditIssue(issue)} makeIssueCopy={() => handleIssueAction(issue, "copy")}
makeIssueCopy={() => makeIssueCopy(issue)} handleDeleteIssue={() => handleIssueAction(issue, "delete")}
handleDeleteIssue={handleDeleteIssue}
handleTrashBox={handleTrashBox} handleTrashBox={handleTrashBox}
removeIssue={() => { removeIssue={() => {
if (removeIssue && issue.bridge_id) if (removeIssue && issue.bridge_id)
removeIssue(issue.bridge_id, issue.id); removeIssue(issue.bridge_id, issue.id);
}} }}
isCompleted={isCompleted} disableUserActions={disableUserActions}
user={user} user={user}
userAuth={userAuth} userAuth={userAuth}
viewProps={viewProps}
/> />
)} )}
</Draggable> </Draggable>
@ -161,18 +154,18 @@ export const SingleBoard: React.FC<Props> = ({
<button <button
type="button" type="button"
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1" className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
onClick={addIssueToState} onClick={addIssueToGroup}
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
Add Issue Add Issue
</button> </button>
) : ( ) : (
!isCompleted && ( !disableUserActions && (
<CustomMenu <CustomMenu
customButton={ customButton={
<button <button
type="button" type="button"
className="flex items-center gap-2 font-medium text-custom-primary outline-none" className="flex items-center gap-2 font-medium text-custom-primary outline-none whitespace-nowrap"
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
Add Issue Add Issue
@ -181,7 +174,7 @@ export const SingleBoard: React.FC<Props> = ({
position="left" position="left"
noBorder noBorder
> >
<CustomMenu.MenuItem onClick={addIssueToState}> <CustomMenu.MenuItem onClick={addIssueToGroup}>
Create new Create new
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
{openIssuesListModal && ( {openIssuesListModal && (

View File

@ -15,7 +15,6 @@ import {
// services // services
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
// hooks // hooks
import useIssuesView from "hooks/use-issues-view";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components // components
@ -45,14 +44,7 @@ import { LayerDiagonalIcon } from "components/icons";
import { handleIssuesMutation } from "constants/issue"; import { handleIssuesMutation } from "constants/issue";
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
// types // types
import { import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
ICurrentUserResponse,
IIssue,
ISubIssueResponse,
Properties,
TIssueGroupByOptions,
UserAuth,
} from "types";
// fetch-keys // fetch-keys
import { import {
CYCLE_DETAILS, CYCLE_DETAILS,
@ -61,6 +53,7 @@ import {
MODULE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS,
SUB_ISSUES, SUB_ISSUES,
USER_ISSUES,
VIEW_ISSUES, VIEW_ISSUES,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
@ -69,18 +62,17 @@ type Props = {
provided: DraggableProvided; provided: DraggableProvided;
snapshot: DraggableStateSnapshot; snapshot: DraggableStateSnapshot;
issue: IIssue; issue: IIssue;
properties: Properties;
groupTitle?: string; groupTitle?: string;
index: number; index: number;
selectedGroup: TIssueGroupByOptions;
editIssue: () => void; editIssue: () => void;
makeIssueCopy: () => void; makeIssueCopy: () => void;
removeIssue?: (() => void) | null; removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
handleTrashBox: (isDragging: boolean) => void; handleTrashBox: (isDragging: boolean) => void;
isCompleted?: boolean; disableUserActions: boolean;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
userAuth: UserAuth; userAuth: UserAuth;
viewProps: IIssueViewProps;
}; };
export const SingleBoardIssue: React.FC<Props> = ({ export const SingleBoardIssue: React.FC<Props> = ({
@ -88,18 +80,17 @@ export const SingleBoardIssue: React.FC<Props> = ({
provided, provided,
snapshot, snapshot,
issue, issue,
properties,
index, index,
selectedGroup,
editIssue, editIssue,
makeIssueCopy, makeIssueCopy,
removeIssue, removeIssue,
groupTitle, groupTitle,
handleDeleteIssue, handleDeleteIssue,
handleTrashBox, handleTrashBox,
isCompleted = false, disableUserActions,
user, user,
userAuth, userAuth,
viewProps,
}) => { }) => {
// context menu // context menu
const [contextMenu, setContextMenu] = useState(false); const [contextMenu, setContextMenu] = useState(false);
@ -108,7 +99,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
const actionSectionRef = useRef<HTMLDivElement | null>(null); const actionSectionRef = useRef<HTMLDivElement | null>(null);
const { orderBy, params } = useIssuesView(); const { groupByProperty: selectedGroup, orderBy, params, properties } = viewProps;
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
@ -117,7 +108,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
const partialUpdateIssue = useCallback( const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>, issue: IIssue) => { (formData: Partial<IIssue>, issue: IIssue) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !issue) return;
const fetchKey = cycleId const fetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
@ -125,7 +116,9 @@ export const SingleBoardIssue: React.FC<Props> = ({
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
: viewId : viewId
? VIEW_ISSUES(viewId.toString(), params) ? VIEW_ISSUES(viewId.toString(), params)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); : router.pathname.includes("my-issues")
? USER_ISSUES(workspaceSlug.toString(), params)
: PROJECT_ISSUES_LIST_WITH_PARAMS(issue.project.toString(), params);
if (issue.parent) { if (issue.parent) {
mutate<ISubIssueResponse>( mutate<ISubIssueResponse>(
@ -170,7 +163,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
} }
issuesService issuesService
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user) .patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user)
.then(() => { .then(() => {
mutate(fetchKey); mutate(fetchKey);
@ -180,7 +173,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
}, },
[ [
workspaceSlug, workspaceSlug,
projectId,
cycleId, cycleId,
moduleId, moduleId,
viewId, viewId,
@ -189,6 +181,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
selectedGroup, selectedGroup,
orderBy, orderBy,
params, params,
router,
user, user,
] ]
); );
@ -228,7 +221,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false)); useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted; const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
return ( return (
<> <>

View File

@ -34,19 +34,17 @@ import {
} from "constants/fetch-keys"; } from "constants/fetch-keys";
type Props = { type Props = {
handleEditIssue: (issue: IIssue) => void; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDeleteIssue: (issue: IIssue) => void;
addIssueToDate: (date: string) => void; addIssueToDate: (date: string) => void;
isCompleted: boolean; disableUserActions: boolean;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
userAuth: UserAuth; userAuth: UserAuth;
}; };
export const CalendarView: React.FC<Props> = ({ export const CalendarView: React.FC<Props> = ({
handleEditIssue, handleIssueAction,
handleDeleteIssue,
addIssueToDate, addIssueToDate,
isCompleted = false, disableUserActions,
user, user,
userAuth, userAuth,
}) => { }) => {
@ -167,7 +165,7 @@ export const CalendarView: React.FC<Props> = ({
); );
}, [currentDate]); }, [currentDate]);
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted; const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
return calendarIssues ? ( return calendarIssues ? (
<div className="h-full overflow-y-auto"> <div className="h-full overflow-y-auto">
@ -220,10 +218,10 @@ export const CalendarView: React.FC<Props> = ({
> >
{currentViewDaysData.map((date, index) => ( {currentViewDaysData.map((date, index) => (
<SingleCalendarDate <SingleCalendarDate
key={`${date}-${index}`}
index={index} index={index}
date={date} date={date}
handleEditIssue={handleEditIssue} handleIssueAction={handleIssueAction}
handleDeleteIssue={handleDeleteIssue}
addIssueToDate={addIssueToDate} addIssueToDate={addIssueToDate}
isMonthlyView={isMonthlyView} isMonthlyView={isMonthlyView}
showWeekEnds={showWeekEnds} showWeekEnds={showWeekEnds}

View File

@ -13,8 +13,7 @@ import { formatDate } from "helpers/calendar.helper";
import { ICurrentUserResponse, IIssue } from "types"; import { ICurrentUserResponse, IIssue } from "types";
type Props = { type Props = {
handleEditIssue: (issue: IIssue) => void; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDeleteIssue: (issue: IIssue) => void;
index: number; index: number;
date: { date: {
date: string; date: string;
@ -28,8 +27,7 @@ type Props = {
}; };
export const SingleCalendarDate: React.FC<Props> = ({ export const SingleCalendarDate: React.FC<Props> = ({
handleEditIssue, handleIssueAction,
handleDeleteIssue,
date, date,
index, index,
addIssueToDate, addIssueToDate,
@ -72,8 +70,8 @@ export const SingleCalendarDate: React.FC<Props> = ({
provided={provided} provided={provided}
snapshot={snapshot} snapshot={snapshot}
issue={issue} issue={issue}
handleEditIssue={handleEditIssue} handleEditIssue={() => handleIssueAction(issue, "edit")}
handleDeleteIssue={handleDeleteIssue} handleDeleteIssue={() => handleIssueAction(issue, "delete")}
user={user} user={user}
isNotAllowed={isNotAllowed} isNotAllowed={isNotAllowed}
/> />

View File

@ -0,0 +1,7 @@
export * from "./board-view";
export * from "./calendar-view";
export * from "./gantt-chart-view";
export * from "./list-view";
export * from "./spreadsheet-view";
export * from "./all-views";
export * from "./issues-view";

View File

@ -5,38 +5,26 @@ import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// react-beautiful-dnd // react-beautiful-dnd
import { DragDropContext, DropResult } from "react-beautiful-dnd"; import { DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// services // services
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
import stateService from "services/state.service"; import stateService from "services/state.service";
import modulesService from "services/modules.service"; import modulesService from "services/modules.service";
import trackEventServices from "services/track-event.service"; import trackEventServices from "services/track-event.service";
// contexts
import { useProjectMyMembership } from "contexts/project-member.context";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useIssuesView from "hooks/use-issues-view"; import useIssuesView from "hooks/use-issues-view";
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
import useIssuesProperties from "hooks/use-issue-properties";
import useProjectMembers from "hooks/use-project-members";
// components // components
import { import { FiltersList, AllViews } from "components/core";
AllLists,
AllBoards,
FilterList,
CalendarView,
GanttChartView,
SpreadsheetView,
} from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { CreateUpdateViewModal } from "components/views"; import { CreateUpdateViewModal } from "components/views";
import { TransferIssues, TransferIssuesModal } from "components/cycles";
// ui // ui
import { EmptyState, PrimaryButton, Spinner, SecondaryButton } from "components/ui"; import { PrimaryButton } from "components/ui";
// icons // icons
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
// images
import emptyIssue from "public/empty-state/issue.svg";
import emptyIssueArchive from "public/empty-state/issue-archive.svg";
// helpers // helpers
import { getStatesList } from "helpers/state.helper"; import { getStatesList } from "helpers/state.helper";
import { orderArrayBy } from "helpers/array.helper"; import { orderArrayBy } from "helpers/array.helper";
@ -49,19 +37,18 @@ 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";
type Props = { type Props = {
type?: "issue" | "cycle" | "module";
openIssuesListModal?: () => void; openIssuesListModal?: () => void;
isCompleted?: boolean; disableUserActions?: boolean;
}; };
export const IssuesView: React.FC<Props> = ({ export const IssuesView: React.FC<Props> = ({
type = "issue",
openIssuesListModal, openIssuesListModal,
isCompleted = false, disableUserActions = false,
}) => { }) => {
// create issue modal // create issue modal
const [createIssueModal, setCreateIssueModal] = useState(false); const [createIssueModal, setCreateIssueModal] = useState(false);
@ -83,14 +70,9 @@ export const IssuesView: React.FC<Props> = ({
// trash box // trash box
const [trashBox, setTrashBox] = useState(false); const [trashBox, setTrashBox] = useState(false);
// transfer issue
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const { memberRole } = useProjectMyMembership();
const { user } = useUserAuth(); const { user } = useUserAuth();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -104,7 +86,9 @@ export const IssuesView: React.FC<Props> = ({
isEmpty, isEmpty,
setFilters, setFilters,
params, params,
showEmptyGroups,
} = useIssuesView(); } = useIssuesView();
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const { data: stateGroups } = useSWR( const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
@ -114,6 +98,15 @@ 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 handleDeleteIssue = useCallback( const handleDeleteIssue = useCallback(
(issue: IIssue) => { (issue: IIssue) => {
setDeleteIssueModal(true); setDeleteIssueModal(true);
@ -123,7 +116,7 @@ export const IssuesView: React.FC<Props> = ({
); );
const handleOnDragEnd = useCallback( const handleOnDragEnd = useCallback(
(result: DropResult) => { async (result: DropResult) => {
setTrashBox(false); setTrashBox(false);
if (!result.destination || !workspaceSlug || !projectId || !groupedByIssues) return; if (!result.destination || !workspaceSlug || !projectId || !groupedByIssues) return;
@ -281,7 +274,7 @@ export const IssuesView: React.FC<Props> = ({
] ]
); );
const addIssueToState = useCallback( const addIssueToGroup = useCallback(
(groupTitle: string) => { (groupTitle: string) => {
setCreateIssueModal(true); setCreateIssueModal(true);
@ -335,6 +328,15 @@ export const IssuesView: React.FC<Props> = ({
[setEditIssueModal, setIssueToEdit] [setEditIssueModal, setIssueToEdit]
); );
const handleIssueAction = useCallback(
(issue: IIssue, action: "copy" | "edit" | "delete") => {
if (action === "copy") makeIssueCopy(issue);
else if (action === "edit") handleEditIssue(issue);
else if (action === "delete") handleDeleteIssue(issue);
},
[makeIssueCopy, handleEditIssue, handleDeleteIssue]
);
const removeIssueFromCycle = useCallback( const removeIssueFromCycle = useCallback(
(bridgeId: string, issueId: string) => { (bridgeId: string, issueId: string) => {
if (!workspaceSlug || !projectId || !cycleId) return; if (!workspaceSlug || !projectId || !cycleId) return;
@ -421,13 +423,6 @@ export const IssuesView: React.FC<Props> = ({
[workspaceSlug, projectId, moduleId, params, selectedGroup, setToastAlert] [workspaceSlug, projectId, moduleId, params, selectedGroup, setToastAlert]
); );
const handleTrashBox = useCallback(
(isDragging: boolean) => {
if (isDragging && !trashBox) setTrashBox(true);
},
[trashBox, setTrashBox]
);
const nullFilters = Object.keys(filters).filter( const nullFilters = Object.keys(filters).filter(
(key) => filters[key as keyof IIssueFilterOptions] === null (key) => filters[key as keyof IIssueFilterOptions] === null
); );
@ -461,14 +456,27 @@ export const IssuesView: React.FC<Props> = ({
data={issueToDelete} data={issueToDelete}
user={user} user={user}
/> />
<TransferIssuesModal
handleClose={() => setTransferIssuesModal(false)}
isOpen={transferIssuesModal}
/>
{areFiltersApplied && ( {areFiltersApplied && (
<> <>
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0"> <div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
<FilterList filters={filters} setFilters={setFilters} /> <FiltersList
filters={filters}
setFilters={setFilters}
labels={labels}
members={members?.map((m) => m.member)}
states={states}
clearAllFilters={() =>
setFilters({
assignees: null,
created_by: null,
labels: null,
priority: null,
state: null,
target_date: null,
type: null,
})
}
/>
<PrimaryButton <PrimaryButton
onClick={() => { onClick={() => {
if (viewId) { if (viewId) {
@ -492,140 +500,32 @@ export const IssuesView: React.FC<Props> = ({
{<div className="mt-3 border-t border-custom-border-200" />} {<div className="mt-3 border-t border-custom-border-200" />}
</> </>
)} )}
<AllViews
<DragDropContext onDragEnd={handleOnDragEnd}> addIssueToDate={addIssueToDate}
<StrictModeDroppable droppableId="trashBox"> addIssueToGroup={addIssueToGroup}
{(provided, snapshot) => ( disableUserActions={disableUserActions}
<div dragDisabled={
className={`${ selectedGroup === "created_by" ||
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0" selectedGroup === "labels" ||
} fixed top-4 left-1/2 -translate-x-1/2 z-40 w-72 flex items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-custom-background-100 px-3 py-5 text-xs font-medium italic text-red-500 ${ selectedGroup === "state_detail.group"
snapshot.isDraggingOver ? "bg-red-500 blur-2xl opacity-70" : "" }
} transition duration-300`} handleOnDragEnd={handleOnDragEnd}
ref={provided.innerRef} handleIssueAction={handleIssueAction}
{...provided.droppableProps} openIssuesListModal={openIssuesListModal ? openIssuesListModal : null}
> removeIssue={cycleId ? removeIssueFromCycle : moduleId ? removeIssueFromModule : null}
<TrashIcon className="h-4 w-4" /> trashBox={trashBox}
Drop here to delete the issue. setTrashBox={setTrashBox}
</div> viewProps={{
)} groupByProperty: selectedGroup,
</StrictModeDroppable> groupedIssues: groupedByIssues,
{groupedByIssues ? ( isEmpty,
!isEmpty || issueView === "kanban" || issueView === "calendar" ? ( issueView,
<> orderBy,
{isCompleted && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />} params,
{issueView === "list" ? ( properties,
<AllLists showEmptyGroups,
type={type} }}
states={states} />
addIssueToState={addIssueToState}
makeIssueCopy={makeIssueCopy}
handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
removeIssue={
type === "cycle"
? removeIssueFromCycle
: type === "module"
? removeIssueFromModule
: null
}
isCompleted={isCompleted}
user={user}
userAuth={memberRole}
/>
) : issueView === "kanban" ? (
<AllBoards
type={type}
states={states}
addIssueToState={addIssueToState}
makeIssueCopy={makeIssueCopy}
handleEditIssue={handleEditIssue}
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
handleDeleteIssue={handleDeleteIssue}
handleTrashBox={handleTrashBox}
removeIssue={
type === "cycle"
? removeIssueFromCycle
: type === "module"
? removeIssueFromModule
: null
}
isCompleted={isCompleted}
user={user}
userAuth={memberRole}
/>
) : issueView === "calendar" ? (
<CalendarView
handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
addIssueToDate={addIssueToDate}
isCompleted={isCompleted}
user={user}
userAuth={memberRole}
/>
) : issueView === "spreadsheet" ? (
<SpreadsheetView
type={type}
handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
isCompleted={isCompleted}
user={user}
userAuth={memberRole}
/>
) : (
issueView === "gantt_chart" && <GanttChartView />
)}
</>
) : router.pathname.includes("archived-issues") ? (
<EmptyState
title="Archived Issues will be shown here"
description="All the issues that have been in the completed or canceled groups for the configured period of time can be viewed here."
image={emptyIssueArchive}
buttonText="Go to Automation Settings"
onClick={() => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`);
}}
/>
) : (
<EmptyState
title={
cycleId
? "Cycle issues will appear here"
: moduleId
? "Module issues will appear here"
: "Project issues will appear here"
}
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
buttonText="New Issue"
buttonIcon={<PlusIcon className="h-4 w-4" />}
secondaryButton={
cycleId || moduleId ? (
<SecondaryButton
className="flex items-center gap-1.5"
onClick={openIssuesListModal}
>
<PlusIcon className="h-4 w-4" />
Add an existing issue
</SecondaryButton>
) : null
}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
}}
/>
)
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}
</DragDropContext>
</> </>
); );
}; };

View File

@ -0,0 +1,62 @@
// components
import { SingleList } from "components/core/views/list-view/single-list";
// types
import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
// types
type Props = {
states: IState[] | undefined;
addIssueToGroup: (groupTitle: string) => void;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
disableUserActions: boolean;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
viewProps: IIssueViewProps;
};
export const AllLists: React.FC<Props> = ({
addIssueToGroup,
handleIssueAction,
disableUserActions,
openIssuesListModal,
removeIssue,
states,
user,
userAuth,
viewProps,
}) => {
const { groupByProperty: selectedGroup, groupedIssues, showEmptyGroups } = viewProps;
return (
<>
{groupedIssues && (
<div className="h-full overflow-y-auto">
{Object.keys(groupedIssues).map((singleGroup) => {
const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
if (!showEmptyGroups && groupedIssues[singleGroup].length === 0) return null;
return (
<SingleList
key={singleGroup}
groupTitle={singleGroup}
currentState={currentState}
addIssueToGroup={() => addIssueToGroup(singleGroup)}
handleIssueAction={handleIssueAction}
openIssuesListModal={openIssuesListModal}
removeIssue={removeIssue}
disableUserActions={disableUserActions}
user={user}
userAuth={userAuth}
viewProps={viewProps}
/>
);
})}
</div>
)}
</>
);
};

View File

@ -18,8 +18,6 @@ import {
ViewPrioritySelect, ViewPrioritySelect,
ViewStateSelect, ViewStateSelect,
} from "components/issues"; } from "components/issues";
// hooks
import useIssueView from "hooks/use-issues-view";
// ui // ui
import { Tooltip, CustomMenu, ContextMenu } from "components/ui"; import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
// icons // icons
@ -37,7 +35,14 @@ import { LayerDiagonalIcon } from "components/icons";
import { copyTextToClipboard, truncateText } from "helpers/string.helper"; import { copyTextToClipboard, truncateText } from "helpers/string.helper";
import { handleIssuesMutation } from "constants/issue"; import { handleIssuesMutation } from "constants/issue";
// types // types
import { ICurrentUserResponse, IIssue, ISubIssueResponse, Properties, UserAuth } from "types"; import {
ICurrentUserResponse,
IIssue,
IIssueViewProps,
ISubIssueResponse,
Properties,
UserAuth,
} from "types";
// fetch-keys // fetch-keys
import { import {
CYCLE_DETAILS, CYCLE_DETAILS,
@ -46,37 +51,38 @@ import {
MODULE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS,
SUB_ISSUES, SUB_ISSUES,
USER_ISSUES,
VIEW_ISSUES, VIEW_ISSUES,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
type Props = { type Props = {
type?: string; type?: string;
issue: IIssue; issue: IIssue;
properties: Properties;
groupTitle?: string; groupTitle?: string;
editIssue: () => void; editIssue: () => void;
index: number; index: number;
makeIssueCopy: () => void; makeIssueCopy: () => void;
removeIssue?: (() => void) | null; removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
isCompleted?: boolean; disableUserActions: boolean;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
userAuth: UserAuth; userAuth: UserAuth;
viewProps: IIssueViewProps;
}; };
export const SingleListIssue: React.FC<Props> = ({ export const SingleListIssue: React.FC<Props> = ({
type, type,
issue, issue,
properties,
editIssue, editIssue,
index, index,
makeIssueCopy, makeIssueCopy,
removeIssue, removeIssue,
groupTitle, groupTitle,
handleDeleteIssue, handleDeleteIssue,
isCompleted = false, disableUserActions,
user, user,
userAuth, userAuth,
viewProps,
}) => { }) => {
// context menu // context menu
const [contextMenu, setContextMenu] = useState(false); const [contextMenu, setContextMenu] = useState(false);
@ -88,11 +94,11 @@ export const SingleListIssue: React.FC<Props> = ({
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { groupByProperty: selectedGroup, orderBy, params } = useIssueView(); const { groupByProperty: selectedGroup, orderBy, params, properties } = viewProps;
const partialUpdateIssue = useCallback( const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>, issue: IIssue) => { (formData: Partial<IIssue>, issue: IIssue) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !issue) return;
const fetchKey = cycleId const fetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
@ -100,7 +106,9 @@ export const SingleListIssue: React.FC<Props> = ({
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
: viewId : viewId
? VIEW_ISSUES(viewId.toString(), params) ? VIEW_ISSUES(viewId.toString(), params)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); : router.pathname.includes("my-issues")
? USER_ISSUES(workspaceSlug.toString(), params)
: PROJECT_ISSUES_LIST_WITH_PARAMS(issue.project.toString(), params);
if (issue.parent) { if (issue.parent) {
mutate<ISubIssueResponse>( mutate<ISubIssueResponse>(
@ -145,7 +153,7 @@ export const SingleListIssue: React.FC<Props> = ({
} }
issuesService issuesService
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user) .patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user)
.then(() => { .then(() => {
mutate(fetchKey); mutate(fetchKey);
@ -155,7 +163,6 @@ export const SingleListIssue: React.FC<Props> = ({
}, },
[ [
workspaceSlug, workspaceSlug,
projectId,
cycleId, cycleId,
moduleId, moduleId,
viewId, viewId,
@ -164,6 +171,7 @@ export const SingleListIssue: React.FC<Props> = ({
selectedGroup, selectedGroup,
orderBy, orderBy,
params, params,
router,
user, user,
] ]
); );
@ -186,7 +194,8 @@ export const SingleListIssue: React.FC<Props> = ({
? `/${workspaceSlug}/projects/${projectId}/archived-issues/${issue.id}` ? `/${workspaceSlug}/projects/${projectId}/archived-issues/${issue.id}`
: `/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`; : `/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`;
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted || isArchivedIssues; const isNotAllowed =
userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
return ( return (
<> <>

View File

@ -8,7 +8,7 @@ import { Disclosure, Transition } from "@headlessui/react";
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
import projectService from "services/project.service"; import projectService from "services/project.service";
// hooks // hooks
import useIssuesProperties from "hooks/use-issue-properties"; import useProjects from "hooks/use-projects";
// components // components
import { SingleListIssue } from "components/core"; import { SingleListIssue } from "components/core";
// ui // ui
@ -18,58 +18,52 @@ 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";
import { renderEmoji } from "helpers/emoji.helper";
// types // types
import { import {
ICurrentUserResponse, ICurrentUserResponse,
IIssue, IIssue,
IIssueLabels, IIssueLabels,
IIssueViewProps,
IState, IState,
TIssueGroupByOptions,
UserAuth, UserAuth,
} from "types"; } from "types";
// fetch-keys // fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys"; import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = { type Props = {
type?: "issue" | "cycle" | "module";
currentState?: IState | null; currentState?: IState | null;
groupTitle: string; groupTitle: string;
groupedByIssues: { addIssueToGroup: () => void;
[key: string]: IIssue[]; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
};
selectedGroup: TIssueGroupByOptions;
addIssueToState: () => void;
makeIssueCopy: (issue: IIssue) => void;
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean; disableUserActions: boolean;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
userAuth: UserAuth; userAuth: UserAuth;
viewProps: IIssueViewProps;
}; };
export const SingleList: React.FC<Props> = ({ export const SingleList: React.FC<Props> = ({
type,
currentState, currentState,
groupTitle, groupTitle,
groupedByIssues, addIssueToGroup,
selectedGroup, handleIssueAction,
addIssueToState,
makeIssueCopy,
handleEditIssue,
handleDeleteIssue,
openIssuesListModal, openIssuesListModal,
removeIssue, removeIssue,
isCompleted = false, disableUserActions,
user, user,
userAuth, userAuth,
viewProps,
}) => { }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues"); const isArchivedIssues = router.pathname.includes("archived-issues");
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
const { groupByProperty: selectedGroup, groupedIssues } = viewProps;
const { data: issueLabels } = useSWR<IIssueLabels[]>( const { data: issueLabels } = useSWR<IIssueLabels[]>(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
@ -85,6 +79,8 @@ export const SingleList: React.FC<Props> = ({
: null : null
); );
const { projects } = useProjects();
const getGroupTitle = () => { const getGroupTitle = () => {
let title = addSpaceIfCamelCase(groupTitle); let title = addSpaceIfCamelCase(groupTitle);
@ -95,6 +91,9 @@ export const SingleList: React.FC<Props> = ({
case "labels": case "labels":
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None"; title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
break; break;
case "project":
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
break;
case "created_by": case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member; const member = members?.find((member) => member.member.id === groupTitle)?.member;
title = title =
@ -115,9 +114,22 @@ export const SingleList: React.FC<Props> = ({
icon = icon =
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color); currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
break; break;
case "state_detail.group":
icon = getStateGroupIcon(groupTitle as any, "16", "16");
break;
case "priority": case "priority":
icon = getPriorityIcon(groupTitle, "text-lg"); icon = getPriorityIcon(groupTitle, "text-lg");
break; 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": case "labels":
const labelColor = const labelColor =
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000"; issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
@ -138,6 +150,8 @@ export const SingleList: React.FC<Props> = ({
return icon; return icon;
}; };
if (!groupedIssues) return null;
return ( return (
<Disclosure as="div" defaultOpen> <Disclosure as="div" defaultOpen>
{({ open }) => ( {({ open }) => (
@ -156,7 +170,7 @@ export const SingleList: React.FC<Props> = ({
<h2 className="font-medium leading-5">All Issues</h2> <h2 className="font-medium leading-5">All Issues</h2>
)} )}
<span className="text-custom-text-200 min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs"> <span className="text-custom-text-200 min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs">
{groupedByIssues[groupTitle as keyof IIssue].length} {groupedIssues[groupTitle as keyof IIssue].length}
</span> </span>
</div> </div>
</Disclosure.Button> </Disclosure.Button>
@ -166,11 +180,11 @@ export const SingleList: React.FC<Props> = ({
<button <button
type="button" type="button"
className="p-1 text-custom-text-200 hover:bg-custom-background-80" className="p-1 text-custom-text-200 hover:bg-custom-background-80"
onClick={addIssueToState} onClick={addIssueToGroup}
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
</button> </button>
) : isCompleted ? ( ) : disableUserActions ? (
"" ""
) : ( ) : (
<CustomMenu <CustomMenu
@ -182,7 +196,7 @@ export const SingleList: React.FC<Props> = ({
position="right" position="right"
noBorder noBorder
> >
<CustomMenu.MenuItem onClick={addIssueToState}>Create new</CustomMenu.MenuItem> <CustomMenu.MenuItem onClick={addIssueToGroup}>Create new</CustomMenu.MenuItem>
{openIssuesListModal && ( {openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}> <CustomMenu.MenuItem onClick={openIssuesListModal}>
Add an existing issue Add an existing issue
@ -201,26 +215,26 @@ export const SingleList: React.FC<Props> = ({
leaveTo="transform opacity-0" leaveTo="transform opacity-0"
> >
<Disclosure.Panel> <Disclosure.Panel>
{groupedByIssues[groupTitle] ? ( {groupedIssues[groupTitle] ? (
groupedByIssues[groupTitle].length > 0 ? ( groupedIssues[groupTitle].length > 0 ? (
groupedByIssues[groupTitle].map((issue, index) => ( groupedIssues[groupTitle].map((issue, index) => (
<SingleListIssue <SingleListIssue
key={issue.id} key={issue.id}
type={type} type={type}
issue={issue} issue={issue}
properties={properties}
groupTitle={groupTitle} groupTitle={groupTitle}
index={index} index={index}
editIssue={() => handleEditIssue(issue)} editIssue={() => handleIssueAction(issue, "edit")}
makeIssueCopy={() => makeIssueCopy(issue)} makeIssueCopy={() => handleIssueAction(issue, "copy")}
handleDeleteIssue={handleDeleteIssue} handleDeleteIssue={() => handleIssueAction(issue, "delete")}
removeIssue={() => { removeIssue={() => {
if (removeIssue !== null && issue.bridge_id) if (removeIssue !== null && issue.bridge_id)
removeIssue(issue.bridge_id, issue.id); removeIssue(issue.bridge_id, issue.id);
}} }}
isCompleted={isCompleted} disableUserActions={disableUserActions}
user={user} user={user}
userAuth={userAuth} userAuth={userAuth}
viewProps={viewProps}
/> />
)) ))
) : ( ) : (

View File

@ -53,7 +53,7 @@ type Props = {
handleEditIssue: (issue: IIssue) => void; handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
gridTemplateColumns: string; gridTemplateColumns: string;
isCompleted?: boolean; disableUserActions: boolean;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
userAuth: UserAuth; userAuth: UserAuth;
nestingLevel: number; nestingLevel: number;
@ -68,7 +68,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
handleEditIssue, handleEditIssue,
handleDeleteIssue, handleDeleteIssue,
gridTemplateColumns, gridTemplateColumns,
isCompleted = false, disableUserActions,
user, user,
userAuth, userAuth,
nestingLevel, nestingLevel,
@ -190,7 +190,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
{issue.project_detail?.identifier}-{issue.sequence_id} {issue.project_detail?.identifier}-{issue.sequence_id}
</span> </span>
)} )}
{!isNotAllowed && !isCompleted && ( {!isNotAllowed && !disableUserActions && (
<div className="absolute top-0 left-2.5 opacity-0 group-hover:opacity-100"> <div className="absolute top-0 left-2.5 opacity-0 group-hover:opacity-100">
<Popover2 <Popover2
isOpen={isOpen} isOpen={isOpen}

View File

@ -8,32 +8,28 @@ import useSubIssue from "hooks/use-sub-issue";
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types"; import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
type Props = { type Props = {
key: string;
issue: IIssue; issue: IIssue;
index: number; index: number;
expandedIssues: string[]; expandedIssues: string[];
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>; setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
properties: Properties; properties: Properties;
handleEditIssue: (issue: IIssue) => void; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDeleteIssue: (issue: IIssue) => void;
gridTemplateColumns: string; gridTemplateColumns: string;
isCompleted?: boolean; disableUserActions: boolean;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
userAuth: UserAuth; userAuth: UserAuth;
nestingLevel?: number; nestingLevel?: number;
}; };
export const SpreadsheetIssues: React.FC<Props> = ({ export const SpreadsheetIssues: React.FC<Props> = ({
key,
index, index,
issue, issue,
expandedIssues, expandedIssues,
setExpandedIssues, setExpandedIssues,
gridTemplateColumns, gridTemplateColumns,
properties, properties,
handleEditIssue, handleIssueAction,
handleDeleteIssue, disableUserActions,
isCompleted = false,
user, user,
userAuth, userAuth,
nestingLevel = 0, nestingLevel = 0,
@ -64,9 +60,9 @@ export const SpreadsheetIssues: React.FC<Props> = ({
handleToggleExpand={handleToggleExpand} handleToggleExpand={handleToggleExpand}
gridTemplateColumns={gridTemplateColumns} gridTemplateColumns={gridTemplateColumns}
properties={properties} properties={properties}
handleEditIssue={handleEditIssue} handleEditIssue={() => handleIssueAction(issue, "edit")}
handleDeleteIssue={handleDeleteIssue} handleDeleteIssue={() => handleIssueAction(issue, "delete")}
isCompleted={isCompleted} disableUserActions={disableUserActions}
user={user} user={user}
userAuth={userAuth} userAuth={userAuth}
nestingLevel={nestingLevel} nestingLevel={nestingLevel}
@ -76,7 +72,7 @@ export const SpreadsheetIssues: React.FC<Props> = ({
!isLoading && !isLoading &&
subIssues && subIssues &&
subIssues.length > 0 && subIssues.length > 0 &&
subIssues.map((subIssue: IIssue, subIndex: number) => ( subIssues.map((subIssue: IIssue) => (
<SpreadsheetIssues <SpreadsheetIssues
key={subIssue.id} key={subIssue.id}
issue={subIssue} issue={subIssue}
@ -85,9 +81,8 @@ export const SpreadsheetIssues: React.FC<Props> = ({
setExpandedIssues={setExpandedIssues} setExpandedIssues={setExpandedIssues}
gridTemplateColumns={gridTemplateColumns} gridTemplateColumns={gridTemplateColumns}
properties={properties} properties={properties}
handleEditIssue={handleEditIssue} handleIssueAction={handleIssueAction}
handleDeleteIssue={handleDeleteIssue} disableUserActions={disableUserActions}
isCompleted={isCompleted}
user={user} user={user}
userAuth={userAuth} userAuth={userAuth}
nestingLevel={nestingLevel + 1} nestingLevel={nestingLevel + 1}

View File

@ -17,28 +17,26 @@ import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
import { PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
type Props = { type Props = {
type: "issue" | "cycle" | "module"; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
isCompleted?: boolean; disableUserActions: boolean;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
userAuth: UserAuth; userAuth: UserAuth;
}; };
export const SpreadsheetView: React.FC<Props> = ({ export const SpreadsheetView: React.FC<Props> = ({
type, handleIssueAction,
handleEditIssue,
handleDeleteIssue,
openIssuesListModal, openIssuesListModal,
isCompleted = false, disableUserActions,
user, user,
userAuth, userAuth,
}) => { }) => {
const [expandedIssues, setExpandedIssues] = useState<string[]>([]); const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
const { spreadsheetIssues } = useSpreadsheetIssuesView(); const { spreadsheetIssues } = useSpreadsheetIssuesView();
@ -76,9 +74,8 @@ export const SpreadsheetView: React.FC<Props> = ({
setExpandedIssues={setExpandedIssues} setExpandedIssues={setExpandedIssues}
gridTemplateColumns={gridTemplateColumns} gridTemplateColumns={gridTemplateColumns}
properties={properties} properties={properties}
handleEditIssue={handleEditIssue} handleIssueAction={handleIssueAction}
handleDeleteIssue={handleDeleteIssue} disableUserActions={disableUserActions}
isCompleted={isCompleted}
user={user} user={user}
userAuth={userAuth} userAuth={userAuth}
/> />
@ -99,7 +96,7 @@ export const SpreadsheetView: React.FC<Props> = ({
Add Issue Add Issue
</button> </button>
) : ( ) : (
!isCompleted && ( !disableUserActions && (
<CustomMenu <CustomMenu
className="sticky left-0 z-[1]" className="sticky left-0 z-[1]"
customButton={ customButton={

View File

@ -5,6 +5,8 @@ import {
StartedStateIcon, StartedStateIcon,
UnstartedStateIcon, UnstartedStateIcon,
} from "components/icons"; } from "components/icons";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
export const getStateGroupIcon = ( export const getStateGroupIcon = (
stateGroup: "backlog" | "unstarted" | "started" | "completed" | "cancelled", stateGroup: "backlog" | "unstarted" | "started" | "completed" | "cancelled",
@ -14,15 +16,45 @@ export const getStateGroupIcon = (
) => { ) => {
switch (stateGroup) { switch (stateGroup) {
case "backlog": case "backlog":
return <BacklogStateIcon width={width} height={height} color={color} />; return (
<BacklogStateIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["backlog"]}
/>
);
case "unstarted": case "unstarted":
return <UnstartedStateIcon width={width} height={height} color={color} />; return (
<UnstartedStateIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["unstarted"]}
/>
);
case "started": case "started":
return <StartedStateIcon width={width} height={height} color={color} />; return (
<StartedStateIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["started"]}
/>
);
case "completed": case "completed":
return <CompletedStateIcon width={width} height={height} color={color} />; return (
<CompletedStateIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["completed"]}
/>
);
case "cancelled": case "cancelled":
return <CancelledStateIcon width={width} height={height} color={color} />; return (
<CancelledStateIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["cancelled"]}
/>
);
default: default:
return <></>; return <></>;
} }

View File

@ -37,37 +37,35 @@ export const FiltersDropdown: React.FC = () => {
id: "priority", id: "priority",
label: "Priority", label: "Priority",
value: PRIORITIES, value: PRIORITIES,
children: [ hasChildren: true,
...PRIORITIES.map((priority) => ({ children: PRIORITIES.map((priority) => ({
id: priority === null ? "null" : priority, id: priority === null ? "null" : priority,
label: ( label: (
<div className="flex items-center gap-2 capitalize"> <div className="flex items-center gap-2 capitalize">
{getPriorityIcon(priority)} {priority ?? "None"} {getPriorityIcon(priority)} {priority ?? "None"}
</div> </div>
), ),
value: { value: {
key: "priority", key: "priority",
value: priority === null ? "null" : priority, value: priority === null ? "null" : priority,
}, },
selected: filters?.priority?.includes(priority === null ? "null" : priority), selected: filters?.priority?.includes(priority === null ? "null" : priority),
})), })),
],
}, },
{ {
id: "inbox_status", id: "inbox_status",
label: "Status", label: "Status",
value: INBOX_STATUS.map((status) => status.value), value: INBOX_STATUS.map((status) => status.value),
children: [ hasChildren: true,
...INBOX_STATUS.map((status) => ({ children: INBOX_STATUS.map((status) => ({
id: status.key, id: status.key,
label: status.label, label: status.label,
value: { value: {
key: "inbox_status", key: "inbox_status",
value: status.value, value: status.value,
}, },
selected: filters?.inbox_status?.includes(status.value), selected: filters?.inbox_status?.includes(status.value),
})), })),
],
}, },
]} ]}
/> />

View File

@ -63,7 +63,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
if (!workspaceSlug || !projectId || !data) return; if (!workspaceSlug || !projectId || !data) return;
await issueServices await issueServices
.deleteIssue(workspaceSlug as string, projectId as string, data.id, user) .deleteIssue(workspaceSlug as string, data.project, data.id, user)
.then(() => { .then(() => {
if (issueView === "calendar") { if (issueView === "calendar") {
const calendarFetchKey = cycleId const calendarFetchKey = cycleId
@ -72,7 +72,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams) ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
: viewId : viewId
? VIEW_ISSUES(viewId.toString(), calendarParams) ? VIEW_ISSUES(viewId.toString(), calendarParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), calendarParams); : PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, calendarParams);
mutate<IIssue[]>( mutate<IIssue[]>(
calendarFetchKey, calendarFetchKey,
@ -86,7 +86,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams) ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams)
: viewId : viewId
? VIEW_ISSUES(viewId.toString(), spreadsheetParams) ? VIEW_ISSUES(viewId.toString(), spreadsheetParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", spreadsheetParams); : PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, spreadsheetParams);
if (data.parent) { if (data.parent) {
mutate<ISubIssueResponse>( mutate<ISubIssueResponse>(
SUB_ISSUES(data.parent.toString()), SUB_ISSUES(data.parent.toString()),

View File

@ -1,5 +1,6 @@
export * from "./attachment"; export * from "./attachment";
export * from "./comment"; export * from "./comment";
export * from "./my-issues";
export * from "./sidebar-select"; export * from "./sidebar-select";
export * from "./view-select"; export * from "./view-select";
export * from "./activity"; export * from "./activity";
@ -9,7 +10,6 @@ export * from "./form";
export * from "./gantt-chart"; export * from "./gantt-chart";
export * from "./main-content"; export * from "./main-content";
export * from "./modal"; export * from "./modal";
export * from "./my-issues-list-item";
export * from "./parent-issues-list-modal"; export * from "./parent-issues-list-modal";
export * from "./sidebar"; export * from "./sidebar";
export * from "./sub-issues-list"; export * from "./sub-issues-list";

View File

@ -1,218 +0,0 @@
import React, { useCallback } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { mutate } from "swr";
// hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
// services
import issuesService from "services/issues.service";
// components
import {
ViewDueDateSelect,
ViewPrioritySelect,
ViewStateSelect,
} from "components/issues/view-select";
// icon
import { LinkIcon, PaperClipIcon } from "@heroicons/react/24/outline";
import { LayerDiagonalIcon } from "components/icons";
// ui
import { AssigneesList } from "components/ui/avatar";
import { CustomMenu, Tooltip } from "components/ui";
// types
import { IIssue, Properties } from "types";
// helper
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
// fetch-keys
import { USER_ISSUE } from "constants/fetch-keys";
type Props = {
issue: IIssue;
properties: Properties;
projectId: string;
};
export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { user } = useUserAuth();
const { setToastAlert } = useToast();
const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>, issue: IIssue) => {
if (!workspaceSlug) return;
mutate<IIssue[]>(
USER_ISSUE(workspaceSlug as string),
(prevData) =>
prevData?.map((p) => {
if (p.id === issue.id) return { ...p, ...formData };
return p;
}),
false
);
issuesService
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user)
.then((res) => {
mutate(USER_ISSUE(workspaceSlug as string));
})
.catch((error) => {
console.log(error);
});
},
[workspaceSlug, projectId, user]
);
const handleCopyText = () => {
const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Issue link copied to clipboard.",
});
});
};
const isNotAllowed = false;
return (
<div className="border-b border-custom-border-200 bg-custom-background-100 px-4 py-2.5 last:border-b-0">
<div key={issue.id} className="flex items-center justify-between gap-2">
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<a className="group relative flex items-center gap-2">
{properties?.key && (
<Tooltip
tooltipHeading="Issue ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
>
<span className="flex-shrink-0 text-xs text-custom-text-200">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
</Tooltip>
)}
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<span className="text-[0.825rem] text-custom-text-100">
{truncateText(issue.name, 50)}
</span>
</Tooltip>
</a>
</Link>
<div className="flex flex-wrap items-center gap-2 text-xs">
{properties.priority && (
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.state && (
<ViewStateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && issue.target_date && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={isNotAllowed}
/>
)}
{properties.labels && issue.labels.length > 0 && (
<div className="flex flex-wrap gap-1">
{issue.label_details.map((label) => (
<span
key={label.id}
className="group flex items-center gap-1 rounded-2xl border border-custom-border-200 px-2 py-0.5 text-xs text-custom-text-200"
>
<span
className="h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
</span>
))}
</div>
)}
{properties.assignee && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2 py-1 text-xs shadow-sm">
<Tooltip
position="top-right"
tooltipHeading="Assignees"
tooltipContent={
issue.assignee_details.length > 0
? issue.assignee_details
.map((assignee) =>
assignee?.first_name !== "" ? assignee?.first_name : assignee?.email
)
.join(", ")
: "No Assignee"
}
>
<div className="flex h-4 items-center gap-1">
<AssigneesList userIds={issue.assignees ?? []} />
</div>
</Tooltip>
</div>
)}
{properties.sub_issue_count && issue.sub_issues_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
<LayerDiagonalIcon className="h-3.5 w-3.5" />
{issue.sub_issues_count}
</div>
</Tooltip>
</div>
)}
{properties.link && issue.link_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
<LinkIcon className="h-3.5 w-3.5 text-custom-text-200" />
{issue.link_count}
</div>
</Tooltip>
</div>
)}
{properties.attachment_count && issue.attachment_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-custom-text-200" />
{issue.attachment_count}
</div>
</Tooltip>
</div>
)}
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue link</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,3 @@
export * from "./my-issues-select-filters";
export * from "./my-issues-view-options";
export * from "./my-issues-view";

View File

@ -0,0 +1,168 @@
import { useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import issuesService from "services/issues.service";
// components
import { DueDateFilterModal } from "components/core";
// ui
import { MultiLevelDropdown } from "components/ui";
// icons
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
// helpers
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types
import { IIssueFilterOptions, IQuery } from "types";
// fetch-keys
import { WORKSPACE_LABELS } from "constants/fetch-keys";
// constants
import { GROUP_CHOICES, PRIORITIES } from "constants/project";
import { DUE_DATES } from "constants/due-dates";
type Props = {
filters: Partial<IIssueFilterOptions> | IQuery;
onSelect: (option: any) => void;
direction?: "left" | "right";
height?: "sm" | "md" | "rg" | "lg";
};
export const MyIssuesSelectFilters: React.FC<Props> = ({
filters,
onSelect,
direction = "right",
height = "md",
}) => {
const [isDueDateFilterModalOpen, setIsDueDateFilterModalOpen] = useState(false);
const [fetchLabels, setFetchLabels] = useState(false);
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: labels } = useSWR(
workspaceSlug && fetchLabels ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
workspaceSlug && fetchLabels
? () => issuesService.getWorkspaceLabels(workspaceSlug.toString())
: null
);
return (
<>
{isDueDateFilterModalOpen && (
<DueDateFilterModal
isOpen={isDueDateFilterModalOpen}
handleClose={() => setIsDueDateFilterModalOpen(false)}
/>
)}
<MultiLevelDropdown
label="Filters"
onSelect={onSelect}
direction={direction}
height={height}
options={[
{
id: "priority",
label: "Priority",
value: PRIORITIES,
hasChildren: true,
children: [
...PRIORITIES.map((priority) => ({
id: priority === null ? "null" : priority,
label: (
<div className="flex items-center gap-2 capitalize">
{getPriorityIcon(priority)} {priority ?? "None"}
</div>
),
value: {
key: "priority",
value: priority === null ? "null" : priority,
},
selected: filters?.priority?.includes(priority === null ? "null" : priority),
})),
],
},
{
id: "state_group",
label: "State groups",
value: GROUP_CHOICES,
hasChildren: true,
children: [
...Object.keys(GROUP_CHOICES).map((key) => ({
id: key,
label: (
<div className="flex items-center gap-2">
{getStateGroupIcon(key as any, "16", "16")}{" "}
{GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]}
</div>
),
value: {
key: "state_group",
value: key,
},
selected: filters?.state?.includes(key),
})),
],
},
{
id: "labels",
label: "Labels",
onClick: () => setFetchLabels(true),
value: labels,
hasChildren: true,
children: labels?.map((label) => ({
id: label.id,
label: (
<div className="flex items-center gap-2">
<div
className="h-2 w-2 rounded-full"
style={{
backgroundColor: label.color && label.color !== "" ? label.color : "#000000",
}}
/>
{label.name}
</div>
),
value: {
key: "labels",
value: label.id,
},
selected: filters?.labels?.includes(label.id),
})),
},
{
id: "target_date",
label: "Due date",
value: DUE_DATES,
hasChildren: true,
children: [
...(DUE_DATES?.map((option) => ({
id: option.name,
label: option.name,
value: {
key: "target_date",
value: option.value,
},
selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], option.value),
})) ?? []),
{
id: "custom",
label: "Custom",
value: "custom",
element: (
<button
onClick={() => setIsDueDateFilterModalOpen(true)}
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
>
Custom
</button>
),
},
],
},
]}
/>
</>
);
};

View File

@ -0,0 +1,290 @@
import React from "react";
import { useRouter } from "next/router";
// headless ui
import { Popover, Transition } from "@headlessui/react";
// hooks
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
import useEstimateOption from "hooks/use-estimate-option";
// components
import { MyIssuesSelectFilters } from "components/issues";
// ui
import { CustomMenu, ToggleSwitch, Tooltip } from "components/ui";
// icons
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { FormatListBulletedOutlined, GridViewOutlined } from "@mui/icons-material";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types
import { Properties, TIssueViewOptions } from "types";
// constants
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
{
type: "list",
Icon: FormatListBulletedOutlined,
},
{
type: "kanban",
Icon: GridViewOutlined,
},
];
export const MyIssuesViewOptions: React.FC = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const {
issueView,
setIssueView,
groupBy,
setGroupBy,
orderBy,
setOrderBy,
showEmptyGroups,
setShowEmptyGroups,
properties,
setProperty,
filters,
setFilters,
} = useMyIssuesFilters(workspaceSlug?.toString());
const { isEstimateActive } = useEstimateOption();
return (
<div className="flex items-center gap-2">
<div className="flex items-center gap-x-1">
{issueViewOptions.map((option) => (
<Tooltip
key={option.type}
tooltipContent={
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
}
position="bottom"
>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
issueView === option.type
? "bg-custom-sidebar-background-80"
: "text-custom-sidebar-text-200"
}`}
onClick={() => setIssueView(option.type)}
>
<option.Icon
sx={{
fontSize: 16,
}}
className={option.type === "gantt_chart" ? "rotate-90" : ""}
/>
</button>
</Tooltip>
))}
</div>
<MyIssuesSelectFilters
filters={filters}
onSelect={(option) => {
const key = option.key as keyof typeof filters;
if (key === "target_date") {
const valueExists = checkIfArraysHaveSameElements(
filters?.target_date ?? [],
option.value
);
setFilters({
target_date: valueExists ? null : option.value,
});
} else {
const valueExists = filters[key]?.includes(option.value);
if (valueExists)
setFilters({
[option.key]: ((filters[key] ?? []) as any[])?.filter(
(val) => val !== option.value
),
});
else
setFilters({
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
});
}
}}
direction="left"
height="rg"
/>
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button
className={`group flex items-center gap-2 rounded-md border border-custom-sidebar-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
open
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
: "text-custom-sidebar-text-200"
}`}
>
View
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
<div className="relative divide-y-2 divide-custom-border-200">
<div className="space-y-4 pb-3 text-xs">
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4>
<CustomMenu
label={
groupBy === "project"
? "Project"
: GROUP_BY_OPTIONS.find((option) => option.key === groupBy)?.name ??
"Select"
}
>
{GROUP_BY_OPTIONS.map((option) => {
if (issueView === "kanban" && option.key === null) return null;
if (option.key === "state" || option.key === "created_by")
return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setGroupBy(option.key)}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<CustomMenu
label={
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select"
}
>
{ORDER_BY_OPTIONS.map((option) => {
if (groupBy === "priority" && option.key === "priority") return null;
if (option.key === "sort_order") return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setOrderBy(option.key);
}}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
</>
)}
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Issue type</h4>
<CustomMenu
label={
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters?.type)
?.name ?? "Select"
}
>
{FILTER_ISSUE_OPTIONS.map((option) => (
<CustomMenu.MenuItem
key={option.key}
onClick={() =>
setFilters({
type: option.key,
})
}
>
{option.name}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} />
</div>
{/* <div className="relative flex justify-end gap-x-3">
<button type="button" onClick={() => resetFilterToDefault()}>
Reset to default
</button>
<button
type="button"
className="font-medium text-custom-primary"
onClick={() => setNewFilterDefaultView()}
>
Set as default
</button>
</div> */}
</>
)}
</div>
<div className="space-y-2 py-3">
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
<div className="flex flex-wrap items-center gap-2">
{Object.keys(properties).map((key) => {
if (key === "estimate" && !isEstimateActive) return null;
if (
issueView === "spreadsheet" &&
(key === "attachment_count" ||
key === "link" ||
key === "sub_issue_count")
)
return null;
if (
issueView !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
)
return null;
return (
<button
key={key}
type="button"
className={`rounded border px-2 py-1 text-xs capitalize ${
properties[key as keyof Properties]
? "border-custom-primary bg-custom-primary text-white"
: "border-custom-border-200"
}`}
onClick={() => setProperty(key as keyof Properties)}
>
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
</button>
);
})}
</div>
</div>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
);
};

View File

@ -0,0 +1,288 @@
import { useCallback, useState } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// react-beautiful-dnd
import { DropResult } from "react-beautiful-dnd";
// services
import issuesService from "services/issues.service";
// hooks
import useMyIssues from "hooks/my-issues/use-my-issues";
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
import useUserAuth from "hooks/use-user-auth";
// components
import { AllViews, FiltersList } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { CreateUpdateViewModal } from "components/views";
// types
import { IIssue, IIssueFilterOptions } from "types";
// fetch-keys
import { USER_ISSUES, WORKSPACE_LABELS } from "constants/fetch-keys";
import { orderArrayBy } from "helpers/array.helper";
type Props = {
openIssuesListModal?: () => void;
disableUserActions?: false;
};
export const MyIssuesView: React.FC<Props> = ({
openIssuesListModal,
disableUserActions = false,
}) => {
// create issue modal
const [createIssueModal, setCreateIssueModal] = useState(false);
const [createViewModal, setCreateViewModal] = useState<any>(null);
const [preloadedData, setPreloadedData] = useState<
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
>(undefined);
// update issue modal
const [editIssueModal, setEditIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<
(IIssue & { actionType: "edit" | "delete" }) | undefined
>(undefined);
// delete issue modal
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [issueToDelete, setIssueToDelete] = useState<IIssue | null>(null);
// trash box
const [trashBox, setTrashBox] = useState(false);
const router = useRouter();
const { workspaceSlug } = router.query;
const { user } = useUserAuth();
const { groupedIssues, isEmpty, params } = useMyIssues(workspaceSlug?.toString());
const { filters, setFilters, issueView, groupBy, orderBy, properties, showEmptyGroups } =
useMyIssuesFilters(workspaceSlug?.toString());
const { data: labels } = useSWR(
workspaceSlug && (filters.labels ?? []).length > 0
? WORKSPACE_LABELS(workspaceSlug.toString())
: null,
workspaceSlug && (filters.labels ?? []).length > 0
? () => issuesService.getWorkspaceLabels(workspaceSlug.toString())
: null
);
const handleDeleteIssue = useCallback(
(issue: IIssue) => {
setDeleteIssueModal(true);
setIssueToDelete(issue);
},
[setDeleteIssueModal, setIssueToDelete]
);
const handleOnDragEnd = useCallback(
async (result: DropResult) => {
setTrashBox(false);
console.log(result);
if (!result.destination || !workspaceSlug || !groupedIssues || groupBy !== "priority") return;
const { source, destination } = result;
if (source.droppableId === destination.droppableId) return;
const draggedItem = groupedIssues[source.droppableId][source.index];
if (!draggedItem) return;
if (destination.droppableId === "trashBox") handleDeleteIssue(draggedItem);
else {
const sourceGroup = source.droppableId;
const destinationGroup = destination.droppableId;
draggedItem[groupBy] = destinationGroup;
mutate<{
[key: string]: IIssue[];
}>(
USER_ISSUES(workspaceSlug.toString(), params),
(prevData) => {
if (!prevData) return prevData;
const sourceGroupArray = [...groupedIssues[sourceGroup]];
const destinationGroupArray = [...groupedIssues[destinationGroup]];
sourceGroupArray.splice(source.index, 1);
destinationGroupArray.splice(destination.index, 0, draggedItem);
return {
...prevData,
[sourceGroup]: orderArrayBy(sourceGroupArray, orderBy),
[destinationGroup]: orderArrayBy(destinationGroupArray, orderBy),
};
},
false
);
// patch request
issuesService
.patchIssue(
workspaceSlug as string,
draggedItem.project,
draggedItem.id,
{
priority: draggedItem.priority,
},
user
)
.catch(() => mutate(USER_ISSUES(workspaceSlug.toString(), params)));
}
},
[groupBy, groupedIssues, handleDeleteIssue, orderBy, params, user, workspaceSlug]
);
const addIssueToGroup = useCallback(
(groupTitle: string) => {
setCreateIssueModal(true);
let preloadedValue: string | string[] = groupTitle;
if (groupBy === "labels") {
if (groupTitle === "None") preloadedValue = [];
else preloadedValue = [groupTitle];
}
if (groupBy)
setPreloadedData({
[groupBy]: preloadedValue,
actionType: "createIssue",
});
else setPreloadedData({ actionType: "createIssue" });
},
[setCreateIssueModal, setPreloadedData, groupBy]
);
const addIssueToDate = useCallback(
(date: string) => {
setCreateIssueModal(true);
setPreloadedData({
target_date: date,
actionType: "createIssue",
});
},
[setCreateIssueModal, setPreloadedData]
);
const makeIssueCopy = useCallback(
(issue: IIssue) => {
setCreateIssueModal(true);
setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" });
},
[setCreateIssueModal, setPreloadedData]
);
const handleEditIssue = useCallback(
(issue: IIssue) => {
setEditIssueModal(true);
setIssueToEdit({
...issue,
actionType: "edit",
cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null,
module: issue.issue_module ? issue.issue_module.module : null,
});
},
[setEditIssueModal, setIssueToEdit]
);
const handleIssueAction = useCallback(
(issue: IIssue, action: "copy" | "edit" | "delete") => {
if (action === "copy") makeIssueCopy(issue);
else if (action === "edit") handleEditIssue(issue);
else if (action === "delete") handleDeleteIssue(issue);
},
[makeIssueCopy, handleEditIssue, handleDeleteIssue]
);
const filtersToShow = { ...filters };
delete filtersToShow?.assignees;
delete filtersToShow?.created_by;
const nullFilters = Object.keys(filtersToShow).filter(
(key) => filtersToShow[key as keyof IIssueFilterOptions] === null
);
const areFiltersApplied =
Object.keys(filtersToShow).length > 0 &&
nullFilters.length !== Object.keys(filtersToShow).length;
return (
<>
<CreateUpdateViewModal
isOpen={createViewModal !== null}
handleClose={() => setCreateViewModal(null)}
preLoadedData={createViewModal}
user={user}
/>
<CreateUpdateIssueModal
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
handleClose={() => setCreateIssueModal(false)}
prePopulateData={{
...preloadedData,
}}
/>
<CreateUpdateIssueModal
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
handleClose={() => setEditIssueModal(false)}
data={issueToEdit}
/>
<DeleteIssueModal
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issueToDelete}
user={user}
/>
{areFiltersApplied && (
<>
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
<FiltersList
filters={filtersToShow}
setFilters={setFilters}
labels={labels}
members={undefined}
states={undefined}
clearAllFilters={() =>
setFilters({
labels: null,
priority: null,
state_group: null,
target_date: null,
type: null,
})
}
/>
</div>
{<div className="mt-3 border-t border-custom-border-200" />}
</>
)}
<AllViews
addIssueToDate={addIssueToDate}
addIssueToGroup={addIssueToGroup}
disableUserActions={disableUserActions}
dragDisabled={groupBy !== "priority"}
handleOnDragEnd={handleOnDragEnd}
handleIssueAction={handleIssueAction}
openIssuesListModal={openIssuesListModal ? openIssuesListModal : null}
removeIssue={null}
trashBox={trashBox}
setTrashBox={setTrashBox}
viewProps={{
groupByProperty: groupBy,
groupedIssues,
isEmpty,
issueView,
orderBy,
params,
properties,
showEmptyGroups,
}}
/>
</>
);
};

View File

@ -1,3 +1,5 @@
import { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
@ -39,12 +41,14 @@ export const ViewStateSelect: React.FC<Props> = ({
user, user,
isNotAllowed, isNotAllowed,
}) => { }) => {
const [fetchStates, setFetchStates] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { data: stateGroups } = useSWR( const { data: stateGroups } = useSWR(
workspaceSlug && issue ? STATES_LIST(issue.project) : null, workspaceSlug && issue && fetchStates ? STATES_LIST(issue.project) : null,
workspaceSlug && issue workspaceSlug && issue && fetchStates
? () => stateService.getStates(workspaceSlug as string, issue.project) ? () => stateService.getStates(workspaceSlug as string, issue.project)
: null : null
); );
@ -61,7 +65,7 @@ export const ViewStateSelect: React.FC<Props> = ({
), ),
})); }));
const selectedOption = states?.find((s) => s.id === issue.state); const selectedOption = issue.state_detail;
const stateLabel = ( const stateLabel = (
<Tooltip <Tooltip
@ -126,6 +130,7 @@ export const ViewStateSelect: React.FC<Props> = ({
{...(customButton ? { customButton: stateLabel } : { label: stateLabel })} {...(customButton ? { customButton: stateLabel } : { label: stateLabel })}
position={position} position={position}
disabled={isNotAllowed} disabled={isNotAllowed}
onOpen={() => setFetchStates(true)}
noChevron noChevron
/> />
); );

View File

@ -244,20 +244,7 @@ export const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen, user })
<EmojiIconPicker <EmojiIconPicker
label={ label={
<div className="h-[44px] w-[44px] grid place-items-center rounded-md bg-custom-background-80 outline-none text-lg"> <div className="h-[44px] w-[44px] grid place-items-center rounded-md bg-custom-background-80 outline-none text-lg">
{value ? ( {value ? renderEmoji(value) : "Icon"}
typeof value === "object" ? (
<span
style={{ color: value.color }}
className="material-symbols-rounded text-lg"
>
{value.name}
</span>
) : (
renderEmoji(value)
)
) : (
"Icon"
)}
</div> </div>
} }
onChange={onChange} onChange={onChange}

View File

@ -176,12 +176,7 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({
{renderEmoji(project.emoji)} {renderEmoji(project.emoji)}
</span> </span>
) : project.icon_prop ? ( ) : project.icon_prop ? (
<span renderEmoji(project.icon_prop)
style={{ color: project.icon_prop.color }}
className="material-symbols-rounded text-lg"
>
{project.icon_prop.name}
</span>
) : null} ) : null}
</div> </div>
<p className="mt-3.5 mb-7 break-words"> <p className="mt-3.5 mb-7 break-words">

View File

@ -150,12 +150,7 @@ export const SingleSidebarProject: React.FC<Props> = ({
</span> </span>
) : project.icon_prop ? ( ) : project.icon_prop ? (
<div className="h-7 w-7 grid place-items-center"> <div className="h-7 w-7 grid place-items-center">
<span {renderEmoji(project.icon_prop)}
style={{ color: project.icon_prop.color }}
className="material-symbols-rounded text-lg"
>
{project.icon_prop.name}
</span>
</div> </div>
) : ( ) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white"> <span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">

View File

@ -39,6 +39,7 @@ export const CustomSearchSelect = ({
noChevron = false, noChevron = false,
onChange, onChange,
options, options,
onOpen,
optionsClassName = "", optionsClassName = "",
position = "left", position = "left",
selfPositioned = false, selfPositioned = false,
@ -67,115 +68,119 @@ export const CustomSearchSelect = ({
className={`${selfPositioned ? "" : "relative"} flex-shrink-0 text-left ${className}`} className={`${selfPositioned ? "" : "relative"} flex-shrink-0 text-left ${className}`}
{...props} {...props}
> >
{({ open }: any) => ( {({ open }: any) => {
<> if (open && onOpen) onOpen();
{customButton ? (
<Combobox.Button as="div">{customButton}</Combobox.Button> return (
) : ( <>
<Combobox.Button {customButton ? (
type="button" <Combobox.Button as="div">{customButton}</Combobox.Button>
className={`flex items-center justify-between gap-1 w-full rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none ${ ) : (
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs" <Combobox.Button
} ${ type="button"
disabled className={`flex items-center justify-between gap-1 w-full rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none ${
? "cursor-not-allowed text-custom-text-200" input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
: "cursor-pointer hover:bg-custom-background-80" } ${
} ${buttonClassName}`} disabled
> ? "cursor-not-allowed text-custom-text-200"
{label} : "cursor-pointer hover:bg-custom-background-80"
{!noChevron && !disabled && ( } ${buttonClassName}`}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
)}
</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"
>
<Combobox.Options
className={`absolute z-10 min-w-[10rem] border border-custom-border-300 p-2 rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width
} ${optionsClassName}`}
>
<div className="flex w-full items-center justify-start rounded-sm border-[0.6px] border-custom-border-200 bg-custom-background-90 px-2">
<MagnifyingGlassIcon className="h-3 w-3 text-custom-text-200" />
<Combobox.Input
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Type to search..."
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div
className={`mt-2 space-y-1 ${
maxHeight === "lg"
? "max-h-60"
: maxHeight === "md"
? "max-h-48"
: maxHeight === "rg"
? "max-h-36"
: maxHeight === "sm"
? "max-h-28"
: ""
} overflow-y-scroll`}
> >
{filteredOptions ? ( {label}
filteredOptions.length > 0 ? ( {!noChevron && !disabled && (
filteredOptions.map((option) => ( <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
<Combobox.Option )}
key={option.value} </Combobox.Button>
value={option.value} )}
className={({ active, selected }) => <Transition
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${ show={open}
active || selected ? "bg-custom-background-80" : "" as={React.Fragment}
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` enter="transition ease-out duration-200"
} enterFrom="opacity-0 translate-y-1"
> enterTo="opacity-100 translate-y-0"
{({ active, selected }) => ( leave="transition ease-in duration-150"
<> leaveFrom="opacity-100 translate-y-0"
{option.content} leaveTo="opacity-0 translate-y-1"
{multiple ? ( >
<div <Combobox.Options
className={`flex items-center justify-center rounded border border-custom-border-400 p-0.5 ${ className={`absolute z-10 min-w-[10rem] border border-custom-border-300 p-2 rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
active || selected ? "opacity-100" : "opacity-0" position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
}`} } ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
> width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width
} ${optionsClassName}`}
>
<div className="flex w-full items-center justify-start rounded-sm border-[0.6px] border-custom-border-200 bg-custom-background-90 px-2">
<MagnifyingGlassIcon className="h-3 w-3 text-custom-text-200" />
<Combobox.Input
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Type to search..."
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div
className={`mt-2 space-y-1 ${
maxHeight === "lg"
? "max-h-60"
: maxHeight === "md"
? "max-h-48"
: maxHeight === "rg"
? "max-h-36"
: maxHeight === "sm"
? "max-h-28"
: ""
} overflow-y-scroll`}
>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active || selected ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ active, selected }) => (
<>
{option.content}
{multiple ? (
<div
className={`flex items-center justify-center rounded border border-custom-border-400 p-0.5 ${
active || selected ? "opacity-100" : "opacity-0"
}`}
>
<CheckIcon
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
/>
</div>
) : (
<CheckIcon <CheckIcon
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
/> />
</div> )}
) : ( </>
<CheckIcon )}
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} </Combobox.Option>
/> ))
)} ) : (
</> <span className="flex items-center gap-2 p-1">
)} <p className="text-left text-custom-text-200 ">No matching results</p>
</Combobox.Option> </span>
)) )
) : ( ) : (
<span className="flex items-center gap-2 p-1"> <p className="text-center text-custom-text-200">Loading...</p>
<p className="text-left text-custom-text-200 ">No matching results</p> )}
</span> </div>
) {footerOption}
) : ( </Combobox.Options>
<p className="text-center text-custom-text-200">Loading...</p> </Transition>
)} </>
</div> );
{footerOption} }}
</Combobox.Options>
</Transition>
</>
)}
</Combobox> </Combobox>
); );
}; };

View File

@ -7,6 +7,7 @@ export type DropdownProps = {
label?: string | JSX.Element; label?: string | JSX.Element;
maxHeight?: "sm" | "rg" | "md" | "lg"; maxHeight?: "sm" | "rg" | "md" | "lg";
noChevron?: boolean; noChevron?: boolean;
onOpen?: () => void;
optionsClassName?: string; optionsClassName?: string;
position?: "right" | "left"; position?: "right" | "left";
selfPositioned?: boolean; selfPositioned?: boolean;

View File

@ -2,6 +2,8 @@ import { Fragment, useState } from "react";
// headless ui // headless ui
import { Menu, Transition } from "@headlessui/react"; import { Menu, Transition } from "@headlessui/react";
// ui
import { Loader } from "components/ui";
// icons // icons
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/20/solid"; import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/20/solid";
@ -10,9 +12,6 @@ type MultiLevelDropdownProps = {
label: string; label: string;
options: { options: {
id: string; id: string;
label: string;
value: any;
selected?: boolean;
children?: { children?: {
id: string; id: string;
label: string | JSX.Element; label: string | JSX.Element;
@ -20,6 +19,11 @@ type MultiLevelDropdownProps = {
selected?: boolean; selected?: boolean;
element?: JSX.Element; element?: JSX.Element;
}[]; }[];
hasChildren: boolean;
label: string;
onClick?: () => void;
selected?: boolean;
value: any;
}[]; }[];
onSelect: (value: any) => void; onSelect: (value: any) => void;
direction?: "left" | "right"; direction?: "left" | "right";
@ -69,15 +73,15 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
<Menu.Item <Menu.Item
as="button" as="button"
onClick={(e: any) => { onClick={(e: any) => {
if (option.children) { if (option.hasChildren) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
if (option.onClick) option.onClick();
if (openChildFor === option.id) setOpenChildFor(null); if (openChildFor === option.id) setOpenChildFor(null);
else setOpenChildFor(option.id); else setOpenChildFor(option.id);
} else { } else onSelect(option.value);
onSelect(option.value);
}
}} }}
className="w-full" className="w-full"
> >
@ -90,18 +94,18 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
direction === "right" ? "justify-between" : "" direction === "right" ? "justify-between" : ""
}`} }`}
> >
{direction === "left" && option.children && ( {direction === "left" && option.hasChildren && (
<ChevronLeftIcon className="h-4 w-4" aria-hidden="true" /> <ChevronLeftIcon className="h-4 w-4" aria-hidden="true" />
)} )}
<span>{option.label}</span> <span>{option.label}</span>
{direction === "right" && option.children && ( {direction === "right" && option.hasChildren && (
<ChevronRightIcon className="h-4 w-4" aria-hidden="true" /> <ChevronRightIcon className="h-4 w-4" aria-hidden="true" />
)} )}
</div> </div>
</> </>
)} )}
</Menu.Item> </Menu.Item>
{option.children && option.id === openChildFor && ( {option.hasChildren && option.id === openChildFor && (
<div <div
className={`absolute top-0 w-36 origin-top-right select-none overflow-y-scroll rounded-md bg-custom-background-90 shadow-lg focus:outline-none ${ className={`absolute top-0 w-36 origin-top-right select-none overflow-y-scroll rounded-md bg-custom-background-90 shadow-lg focus:outline-none ${
direction === "left" direction === "left"
@ -119,29 +123,38 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
: "" : ""
}`} }`}
> >
<div className="space-y-1 p-1"> {option.children ? (
{option.children.map((child) => { <div className="space-y-1 p-1">
if (child.element) return child.element; {option.children.map((child) => {
else if (child.element) return child.element;
return ( else
<button return (
key={child.id} <button
type="button" key={child.id}
onClick={() => onSelect(child.value)} type="button"
className={`${ onClick={() => onSelect(child.value)}
child.selected ? "bg-custom-background-80" : "" className={`${
} flex w-full items-center justify-between break-words rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80`} child.selected ? "bg-custom-background-80" : ""
> } flex w-full items-center justify-between break-words rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80`}
{child.label}{" "} >
<CheckIcon {child.label}{" "}
className={`h-3.5 w-3.5 opacity-0 ${ <CheckIcon
child.selected ? "opacity-100" : "" className={`h-3.5 w-3.5 opacity-0 ${
}`} child.selected ? "opacity-100" : ""
/> }`}
</button> />
); </button>
})} );
</div> })}
</div>
) : (
<Loader className="p-1 space-y-2">
<Loader.Item height="20px" />
<Loader.Item height="20px" />
<Loader.Item height="20px" />
<Loader.Item height="20px" />
</Loader>
)}
</div> </div>
)} )}
</div> </div>

View File

@ -1,14 +1,28 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// react-hook-form
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
// services
import stateService from "services/state.service";
// hooks
import useProjectMembers from "hooks/use-project-members";
// components
import { FiltersList } from "components/core";
import { SelectFilters } from "components/views";
// ui // ui
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// components // helpers
import { FilterList } from "components/core"; import { checkIfArraysHaveSameElements } from "helpers/array.helper";
import { getStatesList } from "helpers/state.helper";
// types // types
import { IView } from "types"; import { IQuery, IView } from "types";
// components import issuesService from "services/issues.service";
import { SelectFilters } from "components/views"; // fetch-keys
import { PROJECT_ISSUE_LABELS, STATES_LIST } from "constants/fetch-keys";
type Props = { type Props = {
handleFormSubmit: (values: IView) => Promise<void>; handleFormSubmit: (values: IView) => Promise<void>;
@ -30,6 +44,9 @@ export const ViewForm: React.FC<Props> = ({
data, data,
preLoadedData, preLoadedData,
}) => { }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { const {
register, register,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
@ -42,6 +59,26 @@ export const ViewForm: React.FC<Props> = ({
}); });
const filters = watch("query"); const filters = watch("query");
const { data: stateGroups } = useSWR(
workspaceSlug && projectId && (filters.state ?? []).length > 0
? STATES_LIST(projectId as string)
: null,
workspaceSlug && (filters.state ?? []).length > 0
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
const states = getStatesList(stateGroups ?? {});
const { data: labels } = useSWR(
workspaceSlug && projectId && (filters.labels ?? []).length > 0
? PROJECT_ISSUE_LABELS(projectId.toString())
: null,
workspaceSlug && projectId && (filters.labels ?? []).length > 0
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
: null
);
const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString());
const handleCreateUpdateView = async (formData: IView) => { const handleCreateUpdateView = async (formData: IView) => {
await handleFormSubmit(formData); await handleFormSubmit(formData);
@ -50,6 +87,18 @@ export const ViewForm: React.FC<Props> = ({
}); });
}; };
const clearAllFilters = () => {
setValue("query", {
assignees: null,
created_by: null,
labels: null,
priority: null,
state: null,
target_date: null,
type: null,
});
};
useEffect(() => { useEffect(() => {
reset({ reset({
...defaultValues, ...defaultValues,
@ -106,23 +155,38 @@ export const ViewForm: React.FC<Props> = ({
onSelect={(option) => { onSelect={(option) => {
const key = option.key as keyof typeof filters; const key = option.key as keyof typeof filters;
if (!filters?.[key]?.includes(option.value)) if (key === "target_date") {
const valueExists = checkIfArraysHaveSameElements(
filters?.target_date ?? [],
option.value
);
setValue("query", { setValue("query", {
...filters, target_date: valueExists ? null : option.value,
[key]: [...((filters?.[key] as any[]) ?? []), option.value], } as IQuery);
}); } else {
else { if (!filters?.[key]?.includes(option.value))
setValue("query", { setValue("query", {
...filters, ...filters,
[key]: (filters?.[key] as any[])?.filter((item) => item !== option.value), [key]: [...((filters?.[key] as any[]) ?? []), option.value],
}); });
else {
setValue("query", {
...filters,
[key]: (filters?.[key] as any[])?.filter((item) => item !== option.value),
});
}
} }
}} }}
/> />
</div> </div>
<div> <div>
<FilterList <FiltersList
filters={filters} filters={filters}
labels={labels}
members={members?.map((m) => m.member)}
states={states}
clearAllFilters={clearAllFilters}
setFilters={(query: any) => { setFilters={(query: any) => {
setValue("query", { setValue("query", {
...filters, ...filters,

View File

@ -83,121 +83,116 @@ export const SelectFilters: React.FC<Props> = ({
id: "priority", id: "priority",
label: "Priority", label: "Priority",
value: PRIORITIES, value: PRIORITIES,
children: [ hasChildren: true,
...PRIORITIES.map((priority) => ({ children: PRIORITIES.map((priority) => ({
id: priority === null ? "null" : priority, id: priority === null ? "null" : priority,
label: ( label: (
<div className="flex items-center gap-2 capitalize"> <div className="flex items-center gap-2 capitalize">
{getPriorityIcon(priority)} {priority ?? "None"} {getPriorityIcon(priority)} {priority ?? "None"}
</div> </div>
), ),
value: { value: {
key: "priority", key: "priority",
value: priority === null ? "null" : priority, value: priority === null ? "null" : priority,
}, },
selected: filters?.priority?.includes(priority === null ? "null" : priority), selected: filters?.priority?.includes(priority === null ? "null" : priority),
})), })),
],
}, },
{ {
id: "state", id: "state",
label: "State", label: "State",
value: statesList, value: statesList,
children: [ hasChildren: true,
...statesList.map((state) => ({ children: statesList.map((state) => ({
id: state.id, id: state.id,
label: ( label: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{getStateGroupIcon(state.group, "16", "16", state.color)} {state.name} {getStateGroupIcon(state.group, "16", "16", state.color)} {state.name}
</div> </div>
), ),
value: { value: {
key: "state", key: "state",
value: state.id, value: state.id,
}, },
selected: filters?.state?.includes(state.id), selected: filters?.state?.includes(state.id),
})), })),
],
}, },
{ {
id: "assignees", id: "assignees",
label: "Assignees", label: "Assignees",
value: members, value: members,
children: [ hasChildren: true,
...(members?.map((member) => ({ children: members?.map((member) => ({
id: member.member.id, id: member.member.id,
label: ( label: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Avatar user={member.member} /> <Avatar user={member.member} />
{member.member.first_name && member.member.first_name !== "" {member.member.first_name && member.member.first_name !== ""
? member.member.first_name ? member.member.first_name
: member.member.email} : member.member.email}
</div> </div>
), ),
value: { value: {
key: "assignees", key: "assignees",
value: member.member.id, value: member.member.id,
}, },
selected: filters?.assignees?.includes(member.member.id), selected: filters?.assignees?.includes(member.member.id),
})) ?? []), })),
],
}, },
{ {
id: "created_by", id: "created_by",
label: "Created by", label: "Created by",
value: members, value: members,
children: [ hasChildren: true,
...(members?.map((member) => ({ children: members?.map((member) => ({
id: member.member.id, id: member.member.id,
label: ( label: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Avatar user={member.member} /> <Avatar user={member.member} />
{member.member.first_name && member.member.first_name !== "" {member.member.first_name && member.member.first_name !== ""
? member.member.first_name ? member.member.first_name
: member.member.email} : member.member.email}
</div> </div>
), ),
value: { value: {
key: "created_by", key: "created_by",
value: member.member.id, value: member.member.id,
}, },
selected: filters?.created_by?.includes(member.member.id), selected: filters?.created_by?.includes(member.member.id),
})) ?? []), })),
],
}, },
{ {
id: "labels", id: "labels",
label: "Labels", label: "Labels",
value: issueLabels, value: issueLabels,
children: [ hasChildren: true,
...(issueLabels?.map((label) => ({ children: issueLabels?.map((label) => ({
id: label.id, id: label.id,
label: ( label: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className="h-2 w-2 rounded-full" className="h-2 w-2 rounded-full"
style={{ style={{
backgroundColor: backgroundColor: label.color && label.color !== "" ? label.color : "#000000",
label.color && label.color !== "" ? label.color : "#000000", }}
}} />
/> {label.name}
{label.name} </div>
</div> ),
), value: {
value: { key: "labels",
key: "labels", value: label.id,
value: label.id, },
}, selected: filters?.labels?.includes(label.id),
selected: filters?.labels?.includes(label.id), })),
})) ?? []),
],
}, },
{ {
id: "target_date", id: "target_date",
label: "Due date", label: "Due date",
value: DUE_DATES, value: DUE_DATES,
hasChildren: true,
children: [ children: [
...(DUE_DATES?.map((option) => ({ ...DUE_DATES.map((option) => ({
id: option.name, id: option.name,
label: option.name, label: option.name,
value: { value: {
@ -205,7 +200,7 @@ export const SelectFilters: React.FC<Props> = ({
value: option.value, value: option.value,
}, },
selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], option.value), selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], option.value),
})) ?? []), })),
{ {
id: "custom", id: "custom",
label: "Custom", label: "Custom",

View File

@ -37,6 +37,29 @@ const inboxParamsToKey = (params: any) => {
return `${priorityKey}_${inboxStatusKey}`; return `${priorityKey}_${inboxStatusKey}`;
}; };
const myIssuesParamsToKey = (params: any) => {
const { assignees, created_by, labels, priority, state_group, target_date } = params;
let assigneesKey = assignees ? assignees.split(",") : [];
let createdByKey = created_by ? created_by.split(",") : [];
let stateGroupKey = state_group ? state_group.split(",") : [];
let priorityKey = priority ? priority.split(",") : [];
let labelsKey = labels ? labels.split(",") : [];
const targetDateKey = target_date ?? "";
const type = params.type ? params.type.toUpperCase() : "NULL";
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
const orderBy = params.order_by ? params.order_by.toUpperCase() : "NULL";
// sorting each keys in ascending order
assigneesKey = assigneesKey.sort().join("_");
createdByKey = createdByKey.sort().join("_");
stateGroupKey = stateGroupKey.sort().join("_");
priorityKey = priorityKey.sort().join("_");
labelsKey = labelsKey.sort().join("_");
return `${assigneesKey}_${createdByKey}_${stateGroupKey}_${priorityKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}`;
};
export const CURRENT_USER = "CURRENT_USER"; export const CURRENT_USER = "CURRENT_USER";
export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS"; export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
export const USER_WORKSPACES = "USER_WORKSPACES"; export const USER_WORKSPACES = "USER_WORKSPACES";
@ -97,6 +120,8 @@ export const PROJECT_ISSUE_BY_STATE = (projectId: string) =>
`PROJECT_ISSUE_BY_STATE_${projectId.toUpperCase()}`; `PROJECT_ISSUE_BY_STATE_${projectId.toUpperCase()}`;
export const PROJECT_ISSUE_LABELS = (projectId: string) => export const PROJECT_ISSUE_LABELS = (projectId: string) =>
`PROJECT_ISSUE_LABELS_${projectId.toUpperCase()}`; `PROJECT_ISSUE_LABELS_${projectId.toUpperCase()}`;
export const WORKSPACE_LABELS = (workspaceSlug: string) =>
`WORKSPACE_LABELS_${workspaceSlug.toUpperCase()}`;
export const PROJECT_GITHUB_REPOSITORY = (projectId: string) => export const PROJECT_GITHUB_REPOSITORY = (projectId: string) =>
`PROJECT_GITHUB_REPOSITORY_${projectId.toUpperCase()}`; `PROJECT_GITHUB_REPOSITORY_${projectId.toUpperCase()}`;
@ -123,9 +148,15 @@ export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string, params?: any) => {
export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAILS_${cycleId.toUpperCase()}`; export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAILS_${cycleId.toUpperCase()}`;
export const STATES_LIST = (projectId: string) => `STATES_LIST_${projectId.toUpperCase()}`; export const STATES_LIST = (projectId: string) => `STATES_LIST_${projectId.toUpperCase()}`;
export const STATE_DETAILS = "STATE_DETAILS";
export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug.toUpperCase()}`; export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug.toUpperCase()}`;
export const USER_ISSUES = (workspaceSlug: string, params: any) => {
if (!params) return `USER_ISSUES_${workspaceSlug.toUpperCase()}`;
const paramsKey = myIssuesParamsToKey(params);
return `USER_ISSUES_${paramsKey}`;
};
export const USER_ACTIVITY = "USER_ACTIVITY"; export const USER_ACTIVITY = "USER_ACTIVITY";
export const USER_WORKSPACE_DASHBOARD = (workspaceSlug: string) => export const USER_WORKSPACE_DASHBOARD = (workspaceSlug: string) =>
`USER_WORKSPACE_DASHBOARD_${workspaceSlug.toUpperCase()}`; `USER_WORKSPACE_DASHBOARD_${workspaceSlug.toUpperCase()}`;

View File

@ -2,8 +2,10 @@ export const GROUP_BY_OPTIONS: Array<{
name: string; name: string;
key: TIssueGroupByOptions; key: TIssueGroupByOptions;
}> = [ }> = [
{ name: "State", key: "state" }, { name: "States", key: "state" },
{ name: "State Groups", key: "state_detail.group" },
{ name: "Priority", key: "priority" }, { name: "Priority", key: "priority" },
{ name: "Project", key: "project" },
{ name: "Labels", key: "labels" }, { name: "Labels", key: "labels" },
{ name: "Created by", key: "created_by" }, { name: "Created by", key: "created_by" },
{ name: "None", key: null }, { name: "None", key: null },

View File

@ -91,8 +91,6 @@ export const initialState: StateType = {
assignees: null, assignees: null,
labels: null, labels: null,
state: null, state: null,
issue__assignees__id: null,
issue__labels__id: null,
created_by: null, created_by: null,
target_date: null, target_date: null,
}, },

View File

@ -1,25 +0,0 @@
export const getRandomEmoji = () => {
const emojis = [
"8986",
"9200",
"128204",
"127773",
"127891",
"127947",
"128076",
"128077",
"128187",
"128188",
"128512",
"128522",
"128578",
];
return emojis[Math.floor(Math.random() * emojis.length)];
};
export const renderEmoji = (emoji: string) => {
if (!emoji) return;
return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji));
};

View File

@ -0,0 +1,38 @@
export const getRandomEmoji = () => {
const emojis = [
"8986",
"9200",
"128204",
"127773",
"127891",
"127947",
"128076",
"128077",
"128187",
"128188",
"128512",
"128522",
"128578",
];
return emojis[Math.floor(Math.random() * emojis.length)];
};
export const renderEmoji = (
emoji:
| string
| {
name: string;
color: string;
}
) => {
if (!emoji) return;
if (typeof emoji === "object")
return (
<span style={{ color: emoji.color }} className="material-symbols-rounded text-lg">
{emoji.name}
</span>
);
else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji));
};

View File

@ -0,0 +1,201 @@
import { useEffect, useCallback } from "react";
import useSWR, { mutate } from "swr";
// services
import workspaceService from "services/workspace.service";
// types
import {
IIssueFilterOptions,
IWorkspaceMember,
IWorkspaceViewProps,
Properties,
TIssueGroupByOptions,
TIssueOrderByOptions,
TIssueViewOptions,
} from "types";
// fetch-keys
import { WORKSPACE_MEMBERS_ME } from "constants/fetch-keys";
const initialValues: IWorkspaceViewProps = {
issueView: "list",
filters: {
assignees: null,
created_by: null,
labels: null,
priority: null,
state_group: null,
target_date: null,
type: null,
},
groupByProperty: null,
orderBy: "-created_at",
properties: {
assignee: true,
due_date: true,
key: true,
labels: true,
priority: true,
state: true,
sub_issue_count: true,
attachment_count: true,
link: true,
estimate: true,
created_on: true,
updated_on: true,
},
showEmptyGroups: true,
};
const useMyIssuesFilters = (workspaceSlug: string | undefined) => {
const { data: myWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug as string) : null,
workspaceSlug ? () => workspaceService.workspaceMemberMe(workspaceSlug as string) : null,
{
shouldRetryOnError: false,
}
);
const saveData = useCallback(
(data: Partial<IWorkspaceViewProps>) => {
if (!workspaceSlug || !myWorkspace) return;
const oldData = { ...myWorkspace };
mutate<IWorkspaceMember>(
WORKSPACE_MEMBERS_ME(workspaceSlug.toString()),
(prevData) => {
if (!prevData) return;
return {
...prevData,
view_props: {
...prevData?.view_props,
...data,
},
};
},
false
);
workspaceService.updateWorkspaceView(workspaceSlug, {
view_props: {
...oldData.view_props,
...data,
},
});
},
[myWorkspace, workspaceSlug]
);
const issueView = (myWorkspace?.view_props ?? initialValues).issueView;
const setIssueView = useCallback(
(newView: TIssueViewOptions) => {
console.log("newView", newView);
saveData({
issueView: newView,
});
},
[saveData]
);
const groupBy = (myWorkspace?.view_props ?? initialValues).groupByProperty;
const setGroupBy = useCallback(
(newGroup: TIssueGroupByOptions) => {
saveData({
groupByProperty: newGroup,
});
},
[saveData]
);
const orderBy = (myWorkspace?.view_props ?? initialValues).orderBy;
const setOrderBy = useCallback(
(newOrderBy: TIssueOrderByOptions) => {
saveData({
orderBy: newOrderBy,
});
},
[saveData]
);
const showEmptyGroups = (myWorkspace?.view_props ?? initialValues).showEmptyGroups;
const setShowEmptyGroups = useCallback(() => {
if (!myWorkspace) return;
saveData({
showEmptyGroups: !myWorkspace?.view_props?.showEmptyGroups,
});
}, [myWorkspace, saveData]);
const setProperty = useCallback(
(key: keyof Properties) => {
if (!myWorkspace) return;
saveData({
properties: {
...myWorkspace.view_props?.properties,
[key]: !myWorkspace.view_props?.properties[key],
},
});
},
[myWorkspace, saveData]
);
const filters = (myWorkspace?.view_props ?? initialValues).filters;
const setFilters = useCallback(
(updatedFilter: Partial<IIssueFilterOptions & { state_group: string[] | null }>) => {
if (!myWorkspace) return;
saveData({
filters: {
...myWorkspace.view_props?.filters,
...updatedFilter,
},
});
},
[myWorkspace, saveData]
);
useEffect(() => {
if (!myWorkspace || !workspaceSlug) return;
if (!myWorkspace.view_props) {
workspaceService.updateWorkspaceView(workspaceSlug, {
view_props: { ...initialValues },
});
}
}, [myWorkspace, workspaceSlug]);
const newProperties: Properties = {
assignee: myWorkspace?.view_props.properties.assignee ?? true,
due_date: myWorkspace?.view_props.properties.due_date ?? true,
key: myWorkspace?.view_props.properties.key ?? true,
labels: myWorkspace?.view_props.properties.labels ?? true,
priority: myWorkspace?.view_props.properties.priority ?? true,
state: myWorkspace?.view_props.properties.state ?? true,
sub_issue_count: myWorkspace?.view_props.properties.sub_issue_count ?? true,
attachment_count: myWorkspace?.view_props.properties.attachment_count ?? true,
link: myWorkspace?.view_props.properties.link ?? true,
estimate: myWorkspace?.view_props.properties.estimate ?? true,
created_on: myWorkspace?.view_props.properties.created_on ?? true,
updated_on: myWorkspace?.view_props.properties.updated_on ?? true,
};
return {
issueView,
setIssueView,
groupBy,
setGroupBy,
orderBy,
setOrderBy,
showEmptyGroups,
setShowEmptyGroups,
properties: newProperties,
setProperty,
filters,
setFilters,
};
};
export default useMyIssuesFilters;

View File

@ -0,0 +1,61 @@
import { useMemo } from "react";
import useSWR from "swr";
// services
import userService from "services/user.service";
// hooks
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
// types
import { IIssue } from "types";
// fetch-keys
import { USER_ISSUES } from "constants/fetch-keys";
const useMyIssues = (workspaceSlug: string | undefined) => {
const { filters, groupBy, orderBy } = useMyIssuesFilters(workspaceSlug);
const params: any = {
assignees: filters?.assignees ? filters?.assignees.join(",") : undefined,
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
group_by: groupBy,
labels: filters?.labels ? filters?.labels.join(",") : undefined,
order_by: orderBy,
priority: filters?.priority ? filters?.priority.join(",") : undefined,
state_group: filters?.state_group ? filters?.state_group.join(",") : undefined,
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
type: filters?.type ? filters?.type : undefined,
};
const { data: myIssues, mutate: mutateMyIssues } = useSWR(
workspaceSlug ? USER_ISSUES(workspaceSlug.toString(), params) : null,
workspaceSlug ? () => userService.userIssues(workspaceSlug.toString(), params) : null
);
const groupedIssues:
| {
[key: string]: IIssue[];
}
| undefined = useMemo(() => {
if (!myIssues) return undefined;
if (Array.isArray(myIssues))
return {
allIssues: myIssues,
};
return myIssues;
}, [myIssues]);
const isEmpty =
Object.values(groupedIssues ?? {}).every((group) => group.length === 0) ||
Object.keys(groupedIssues ?? {}).length === 0;
return {
groupedIssues,
isEmpty,
mutateMyIssues,
params,
};
};
export default useMyIssues;

View File

@ -41,12 +41,6 @@ const useCalendarIssuesView = () => {
priority: filters?.priority ? filters?.priority.join(",") : undefined, priority: filters?.priority ? filters?.priority.join(",") : undefined,
type: filters?.type ? filters?.type : undefined, type: filters?.type ? filters?.type : undefined,
labels: filters?.labels ? filters?.labels.join(",") : undefined, labels: filters?.labels ? filters?.labels.join(",") : undefined,
issue__assignees__id: filters?.issue__assignees__id
? filters?.issue__assignees__id.join(",")
: undefined,
issue__labels__id: filters?.issue__labels__id
? filters?.issue__labels__id.join(",")
: undefined,
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
target_date: calendarDateRange, target_date: calendarDateRange,
}; };

View File

@ -57,12 +57,6 @@ const useIssuesView = () => {
priority: filters?.priority ? filters?.priority.join(",") : undefined, priority: filters?.priority ? filters?.priority.join(",") : undefined,
type: filters?.type ? filters?.type : undefined, type: filters?.type ? filters?.type : undefined,
labels: filters?.labels ? filters?.labels.join(",") : undefined, labels: filters?.labels ? filters?.labels.join(",") : undefined,
issue__assignees__id: filters?.issue__assignees__id
? filters?.issue__assignees__id.join(",")
: undefined,
issue__labels__id: filters?.issue__labels__id
? filters?.issue__labels__id.join(",")
: undefined,
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
sub_issue: showSubIssues, sub_issue: showSubIssues,

View File

@ -1,22 +0,0 @@
import useSWR from "swr";
// services
import userService from "services/user.service";
// types
import type { IIssue } from "types";
// fetch-keys
import { USER_ISSUE } from "constants/fetch-keys";
const useIssues = (workspaceSlug: string | undefined) => {
// API Fetching
const { data: myIssues, mutate: mutateMyIssues } = useSWR<IIssue[]>(
workspaceSlug ? USER_ISSUE(workspaceSlug as string) : null,
workspaceSlug ? () => userService.userIssues(workspaceSlug as string) : null
);
return {
myIssues: myIssues,
mutateMyIssues,
};
};
export default useIssues;

View File

@ -1,104 +0,0 @@
import { useState, useEffect, useCallback } from "react";
import useSWR, { mutate } from "swr";
// services
import workspaceService from "services/workspace.service";
// hooks
import useUser from "hooks/use-user";
// types
import { IWorkspaceMember, Properties } from "types";
import { WORKSPACE_MEMBERS_ME } from "constants/fetch-keys";
const initialValues: Properties = {
assignee: true,
due_date: false,
key: true,
labels: false,
priority: false,
state: true,
sub_issue_count: false,
attachment_count: false,
link: false,
estimate: false,
created_on: false,
updated_on: false,
};
const useMyIssuesProperties = (workspaceSlug?: string) => {
const [properties, setProperties] = useState<Properties>(initialValues);
const { user } = useUser();
const { data: myWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug as string) : null,
workspaceSlug ? () => workspaceService.workspaceMemberMe(workspaceSlug as string) : null,
{
shouldRetryOnError: false,
}
);
useEffect(() => {
if (!myWorkspace || !workspaceSlug || !user) return;
setProperties({ ...initialValues, ...myWorkspace.view_props });
if (!myWorkspace.view_props) {
workspaceService.updateWorkspaceView(workspaceSlug, {
view_props: { ...initialValues },
});
}
}, [myWorkspace, workspaceSlug, user]);
const updateIssueProperties = useCallback(
(key: keyof Properties) => {
if (!workspaceSlug || !user) return;
setProperties((prev) => ({ ...prev, [key]: !prev[key] }));
if (myWorkspace) {
mutate<IWorkspaceMember>(
WORKSPACE_MEMBERS_ME(workspaceSlug.toString()),
(prevData) => {
if (!prevData) return;
return {
...prevData,
view_props: { ...prevData?.view_props, [key]: !prevData.view_props?.[key] },
};
},
false
);
if (myWorkspace.view_props) {
workspaceService.updateWorkspaceView(workspaceSlug, {
view_props: {
...myWorkspace.view_props,
[key]: !myWorkspace.view_props[key],
},
});
} else {
workspaceService.updateWorkspaceView(workspaceSlug, {
view_props: { ...initialValues },
});
}
}
},
[workspaceSlug, myWorkspace, user]
);
const newProperties: Properties = {
assignee: properties.assignee,
due_date: properties.due_date,
key: properties.key,
labels: properties.labels,
priority: properties.priority,
state: properties.state,
sub_issue_count: properties.sub_issue_count,
attachment_count: properties.attachment_count,
link: properties.link,
estimate: properties.estimate,
created_on: properties.created_on,
updated_on: properties.updated_on,
};
return [newProperties, updateIssueProperties] as const;
};
export default useMyIssuesProperties;

View File

@ -6,11 +6,14 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys";
// hooks // hooks
import useUser from "./use-user"; import useUser from "./use-user";
const useProjectMembers = (workspaceSlug: string, projectId: string) => { const useProjectMembers = (workspaceSlug: string | undefined, projectId: string | undefined) => {
const { user } = useUser(); const { user } = useUser();
// fetching project members // fetching project members
const { data: members } = useSWR(PROJECT_MEMBERS(projectId), () => const { data: members } = useSWR(
projectService.projectMembers(workspaceSlug, projectId) workspaceSlug && projectId ? PROJECT_MEMBERS(projectId) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug, projectId)
: null
); );
const hasJoined = members?.some((item: any) => item.member.id === (user as any)?.id); const hasJoined = members?.some((item: any) => item.member.id === (user as any)?.id);

View File

@ -42,12 +42,6 @@ const useSpreadsheetIssuesView = () => {
priority: filters?.priority ? filters?.priority.join(",") : undefined, priority: filters?.priority ? filters?.priority.join(",") : undefined,
type: filters?.type ? filters?.type : undefined, type: filters?.type ? filters?.type : undefined,
labels: filters?.labels ? filters?.labels.join(",") : undefined, labels: filters?.labels ? filters?.labels.join(",") : undefined,
issue__assignees__id: filters?.issue__assignees__id
? filters?.issue__assignees__id.join(",")
: undefined,
issue__labels__id: filters?.issue__labels__id
? filters?.issue__labels__id.join(",")
: undefined,
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
sub_issue: "false", sub_issue: "false",
}; };

View File

@ -6,12 +6,14 @@ import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
// hooks // hooks
import useUser from "./use-user"; import useUser from "./use-user";
const useWorkspaceMembers = (workspaceSlug: string) => { const useWorkspaceMembers = (workspaceSlug: string | undefined, fetchCondition?: boolean) => {
fetchCondition = fetchCondition ?? true;
const { user } = useUser(); const { user } = useUser();
const { data: workspaceMembers, error: workspaceMemberErrors } = useSWR( const { data: workspaceMembers, error: workspaceMemberErrors } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug) : null, workspaceSlug && fetchCondition ? WORKSPACE_MEMBERS(workspaceSlug) : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug) : null workspaceSlug && fetchCondition ? () => workspaceService.workspaceMembers(workspaceSlug) : null
); );
const hasJoined = workspaceMembers?.some((item: any) => item.member.id === (user as any)?.id); const hasJoined = workspaceMembers?.some((item: any) => item.member.id === (user as any)?.id);

View File

@ -1,40 +1,67 @@
import React from "react"; import React, { useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// headless ui
import { Disclosure, Popover, Transition } from "@headlessui/react";
// icons // icons
import { ChevronDownIcon, PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
// images
import emptyMyIssues from "public/empty-state/my-issues.svg";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// hooks // hooks
import useIssues from "hooks/use-issues"; import useProjects from "hooks/use-projects";
// ui import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
import { Spinner, PrimaryButton, EmptyState } from "components/ui";
import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs";
// hooks
import useMyIssuesProperties from "hooks/use-my-issues-filter";
// types
import { IIssue, Properties } from "types";
// components // components
import { MyIssuesListItem } from "components/issues"; import { MyIssuesView, MyIssuesViewOptions } from "components/issues";
// helpers // ui
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { PrimaryButton } from "components/ui";
import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
import useProjects from "hooks/use-projects"; import useUser from "hooks/use-user";
const MyIssuesPage: NextPage = () => { const MyIssuesPage: NextPage = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { myIssues } = useIssues(workspaceSlug as string);
const { projects } = useProjects(); const { projects } = useProjects();
const { user } = useUser();
const [properties, setProperties] = useMyIssuesProperties(workspaceSlug as string); const { filters, setFilters } = useMyIssuesFilters(workspaceSlug?.toString());
const tabsList = [
{
key: "assigned",
label: "Assigned to me",
selected: (filters?.assignees ?? []).length > 0,
onClick: () => {
setFilters({
assignees: [user?.id ?? ""],
created_by: null,
});
},
},
{
key: "created",
label: "Created by me",
selected: (filters?.created_by ?? []).length > 0,
onClick: () => {
setFilters({
created_by: [user?.id ?? ""],
assignees: null,
});
},
},
];
useEffect(() => {
if (!filters || !user) return;
if (!filters.assignees && !filters.created_by) {
setFilters({
assignees: [user.id],
});
return;
}
}, [filters, setFilters, user]);
return ( return (
<WorkspaceAuthorizationLayout <WorkspaceAuthorizationLayout
@ -45,59 +72,7 @@ const MyIssuesPage: NextPage = () => {
} }
right={ right={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{myIssues && myIssues.length > 0 && ( <MyIssuesViewOptions />
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-background-90 hover:text-custom-text-100 focus:outline-none ${
open ? "bg-custom-background-90 text-custom-text-100" : "text-custom-text-200"
}`}
>
<span>View</span>
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-1/2 z-10 mr-5 mt-1 w-screen max-w-xs translate-x-1/2 transform overflow-hidden rounded-lg bg-custom-background-100 p-3 shadow-lg">
<div className="space-y-2 py-3">
<h4 className="text-sm text-custom-text-200">Properties</h4>
<div className="flex flex-wrap items-center gap-2">
{Object.keys(properties).map((key) => {
if (key === "estimate" || key === "created_on" || key === "updated_on")
return null;
return (
<button
key={key}
type="button"
className={`rounded border px-2 py-1 text-xs capitalize ${
properties[key as keyof Properties]
? "border-custom-primary bg-custom-primary text-white"
: "border-custom-border-200"
}`}
onClick={() => setProperties(key as keyof Properties)}
>
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
</button>
);
})}
</div>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
)}
<PrimaryButton <PrimaryButton
className="flex items-center gap-2" className="flex items-center gap-2"
onClick={() => { onClick={() => {
@ -111,87 +86,26 @@ const MyIssuesPage: NextPage = () => {
</div> </div>
} }
> >
<div className="flex h-full w-full flex-col space-y-5"> <div className="h-full w-full flex flex-col overflow-hidden">
{myIssues ? ( <div className="border-b border-custom-border-300">
<> <div className="flex items-center overflow-x-scroll">
{myIssues.length > 0 ? ( {tabsList.map((tab) => (
<Disclosure as="div" defaultOpen> <button
{({ open }) => ( key={tab.key}
<div> type="button"
<div className="flex items-center px-4 py-2.5 bg-custom-background-90"> onClick={tab.onClick}
<Disclosure.Button> className={`border-b-2 p-4 text-sm font-medium outline-none whitespace-nowrap ${
<div className="flex items-center gap-x-2"> tab.selected
<h2 className="font-medium leading-5">My Issues</h2> ? "border-custom-primary-100 text-custom-primary-100"
<span className="rounded-full bg-custom-background-80 py-0.5 px-3 text-sm text-custom-text-200"> : "border-transparent"
{myIssues.length} }`}
</span> >
</div> {tab.label}
</Disclosure.Button> </button>
</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>
{myIssues.map((issue: IIssue) => (
<MyIssuesListItem
key={issue.id}
issue={issue}
properties={properties}
projectId={issue.project}
/>
))}
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
) : (
<EmptyState
title={
projects
? projects.length > 0
? "You don't have any issue assigned to you yet"
: "Issues assigned to you will appear here"
: ""
}
description={
projects
? projects.length > 0
? "Keep track of your work in a single place."
: "Let's create your first project and add issues that you want to accomplish."
: ""
}
image={emptyMyIssues}
buttonText={projects ? (projects.length > 0 ? "New Issue" : "New Project") : ""}
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
let e: KeyboardEvent;
if (projects && projects.length > 0)
e = new KeyboardEvent("keydown", {
key: "c",
});
else
e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
}}
/>
)}
</>
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div> </div>
)} </div>
<MyIssuesView />
</div> </div>
</WorkspaceAuthorizationLayout> </WorkspaceAuthorizationLayout>
); );

View File

@ -12,7 +12,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { IssueViewContextProvider } from "contexts/issue-view.context"; import { IssueViewContextProvider } from "contexts/issue-view.context";
// components // components
import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "components/core"; import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "components/core";
import { CycleDetailsSidebar } from "components/cycles"; import { CycleDetailsSidebar, TransferIssues, TransferIssuesModal } from "components/cycles";
// services // services
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
import cycleServices from "services/cycles.service"; import cycleServices from "services/cycles.service";
@ -36,6 +36,7 @@ const SingleCycle: React.FC = () => {
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
const [cycleSidebar, setCycleSidebar] = useState(true); const [cycleSidebar, setCycleSidebar] = useState(true);
const [analyticsModal, setAnalyticsModal] = useState(false); const [analyticsModal, setAnalyticsModal] = useState(false);
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query; const { workspaceSlug, projectId, cycleId } = router.query;
@ -157,16 +158,22 @@ const SingleCycle: React.FC = () => {
</div> </div>
} }
> >
<TransferIssuesModal
handleClose={() => setTransferIssuesModal(false)}
isOpen={transferIssuesModal}
/>
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} /> <AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
<div <div
className={`h-full flex flex-col ${cycleSidebar ? "mr-[24rem]" : ""} ${ className={`h-full flex flex-col ${cycleSidebar ? "mr-[24rem]" : ""} ${
analyticsModal ? "mr-[50%]" : "" analyticsModal ? "mr-[50%]" : ""
} duration-300`} } duration-300`}
> >
{cycleStatus === "completed" && (
<TransferIssues handleClick={() => setTransferIssuesModal(true)} />
)}
<IssuesView <IssuesView
type="cycle"
openIssuesListModal={openIssuesListModal} openIssuesListModal={openIssuesListModal}
isCompleted={cycleStatus === "completed" ?? false} disableUserActions={cycleStatus === "completed" ?? false}
/> />
</div> </div>
<CycleDetailsSidebar <CycleDetailsSidebar

View File

@ -168,7 +168,7 @@ const SingleModule: React.FC = () => {
analyticsModal ? "mr-[50%]" : "" analyticsModal ? "mr-[50%]" : ""
} duration-300`} } duration-300`}
> >
<IssuesView type="module" openIssuesListModal={openIssuesListModal} /> <IssuesView openIssuesListModal={openIssuesListModal} />
</div> </div>
<ModuleDetailsSidebar <ModuleDetailsSidebar

View File

@ -195,22 +195,7 @@ const GeneralSettings: NextPage = () => {
name="emoji_and_icon" name="emoji_and_icon"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<EmojiIconPicker <EmojiIconPicker
label={ label={value ? renderEmoji(value) : "Icon"}
value ? (
typeof value === "object" ? (
<span
style={{ color: value.color }}
className="material-symbols-rounded text-lg"
>
{value.name}
</span>
) : (
renderEmoji(value)
)
) : (
"Icon"
)
}
value={value} value={value}
onChange={onChange} onChange={onChange}
/> />

View File

@ -248,6 +248,14 @@ class ProjectIssuesServices extends APIService {
}); });
} }
async getWorkspaceLabels(workspaceSlug: string): Promise<IIssueLabels[]> {
return this.get(`/api/workspaces/${workspaceSlug}/labels/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async getIssueLabels(workspaceSlug: string, projectId: string): Promise<IIssueLabels[]> { async getIssueLabels(workspaceSlug: string, projectId: string): Promise<IIssueLabels[]> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`) return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`)
.then((response) => response?.data) .then((response) => response?.data)

View File

@ -4,6 +4,7 @@ import trackEventServices from "services/track-event.service";
import type { import type {
ICurrentUserResponse, ICurrentUserResponse,
IIssue,
IUser, IUser,
IUserActivityResponse, IUserActivityResponse,
IUserWorkspaceDashboard, IUserWorkspaceDashboard,
@ -26,8 +27,18 @@ class UserService extends APIService {
}; };
} }
async userIssues(workspaceSlug: string): Promise<any> { async userIssues(
return this.get(`/api/workspaces/${workspaceSlug}/my-issues/`) workspaceSlug: string,
params: any
): Promise<
| {
[key: string]: IIssue[];
}
| IIssue[]
> {
return this.get(`/api/workspaces/${workspaceSlug}/my-issues/`, {
params,
})
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;

View File

@ -14,6 +14,7 @@ import {
IProductUpdateResponse, IProductUpdateResponse,
ICurrentUserResponse, ICurrentUserResponse,
IWorkspaceBulkInviteFormData, IWorkspaceBulkInviteFormData,
IWorkspaceViewProps,
} from "types"; } from "types";
const trackEvent = const trackEvent =
@ -169,7 +170,10 @@ class WorkspaceService extends APIService {
}); });
} }
async updateWorkspaceView(workspaceSlug: string, data: any): Promise<any> { async updateWorkspaceView(
workspaceSlug: string,
data: { view_props: IWorkspaceViewProps }
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/workspace-views/`, data) return this.post(`/api/workspaces/${workspaceSlug}/workspace-views/`, data)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {

View File

@ -232,15 +232,20 @@ export interface IIssueFilterOptions {
target_date: string[] | null; target_date: string[] | null;
state: string[] | null; state: string[] | null;
labels: string[] | null; labels: string[] | null;
issue__assignees__id: string[] | null;
issue__labels__id: string[] | null;
priority: string[] | null; priority: string[] | null;
created_by: string[] | null; created_by: string[] | null;
} }
export type TIssueViewOptions = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart"; export type TIssueViewOptions = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart";
export type TIssueGroupByOptions = "state" | "priority" | "labels" | "created_by" | null; export type TIssueGroupByOptions =
| "state"
| "priority"
| "labels"
| "created_by"
| "state_detail.group"
| "project"
| null;
export type TIssueOrderByOptions = export type TIssueOrderByOptions =
| "-created_at" | "-created_at"
@ -279,3 +284,14 @@ export interface IIssueAttachment {
updated_by: string; updated_by: string;
workspace: string; workspace: string;
} }
export interface IIssueViewProps {
groupedIssues: { [key: string]: IIssue[] } | undefined;
groupByProperty: TIssueGroupByOptions;
isEmpty: boolean;
issueView: TIssueViewOptions;
orderBy: TIssueOrderByOptions;
params: any;
properties: Properties;
showEmptyGroups: boolean;
}

View File

@ -15,26 +15,11 @@ export interface IView {
} }
export interface IQuery { export interface IQuery {
state: string[] | null;
parent: string[] | null;
priority: string[] | null;
labels: string[] | null;
assignees: string[] | null; assignees: string[] | null;
created_by: string[] | null; created_by: string[] | null;
name: string | null; labels: string[] | null;
created_at: [ priority: string[] | null;
{ state: string[] | null;
datetime: string;
timeline: "before";
},
{
datetime: string;
timeline: "after";
}
];
updated_at: string[] | null;
start_date: string[] | null;
target_date: string[] | null; target_date: string[] | null;
completed_at: string[] | null; type: "active" | "backlog" | null;
type: string;
} }

View File

@ -1,4 +1,12 @@
import type { IProjectMember, IUser, IUserLite } from "types"; import type {
IIssueFilterOptions,
IProjectMember,
IUser,
IUserLite,
TIssueGroupByOptions,
TIssueOrderByOptions,
TIssueViewOptions,
} from "types";
export interface IWorkspace { export interface IWorkspace {
readonly id: string; readonly id: string;
@ -54,6 +62,19 @@ export type Properties = {
updated_on: boolean; updated_on: boolean;
}; };
export interface IWorkspaceViewProps {
properties: Properties;
issueView: TIssueViewOptions;
groupByProperty: TIssueGroupByOptions;
orderBy: TIssueOrderByOptions;
filters: Partial<
IIssueFilterOptions & {
state_group: string[] | null;
}
>;
showEmptyGroups: boolean;
}
export interface IWorkspaceMember { export interface IWorkspaceMember {
readonly id: string; readonly id: string;
user: IUserLite; user: IUserLite;
@ -61,7 +82,7 @@ export interface IWorkspaceMember {
member: IUserLite; member: IUserLite;
role: 5 | 10 | 15 | 20; role: 5 | 10 | 15 | 20;
company_role: string | null; company_role: string | null;
view_props: Properties; view_props: IWorkspaceViewProps;
created_at: Date; created_at: Date;
updated_at: Date; updated_at: Date;
created_by: string; created_by: string;