forked from github/plane
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:
parent
ec62308195
commit
3d7fe40035
@ -227,12 +227,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
||||
</span>
|
||||
) : project.icon_prop ? (
|
||||
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
||||
<span
|
||||
style={{ color: project.icon_prop.color }}
|
||||
className="material-symbols-rounded text-lg"
|
||||
>
|
||||
{project.icon_prop.name}
|
||||
</span>
|
||||
{renderEmoji(project.icon_prop)}
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
) : projectDetails?.icon_prop ? (
|
||||
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
||||
<span
|
||||
style={{ color: projectDetails.icon_prop.color }}
|
||||
className="material-symbols-rounded text-lg"
|
||||
>
|
||||
{projectDetails.icon_prop.name}
|
||||
</span>
|
||||
{renderEmoji(projectDetails.icon_prop)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
|
||||
// icons
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
@ -8,42 +8,31 @@ import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||
// ui
|
||||
import { Avatar } from "components/ui";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// services
|
||||
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";
|
||||
// helpers
|
||||
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 { workspaceSlug, projectId, 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 ?? {});
|
||||
const { viewId } = router.query;
|
||||
|
||||
if (!filters) return <></>;
|
||||
|
||||
@ -166,7 +155,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
) : key === "assignees" ? (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{filters.assignees?.map((memberId: string) => {
|
||||
const member = members?.find((m) => m.member.id === memberId)?.member;
|
||||
const member = members?.find((m) => m.id === memberId);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -207,7 +196,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
) : key === "created_by" ? (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{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 (
|
||||
<div
|
||||
@ -248,7 +237,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
) : key === "labels" ? (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{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;
|
||||
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 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
type: null,
|
||||
state: null,
|
||||
priority: null,
|
||||
assignees: null,
|
||||
labels: null,
|
||||
created_by: null,
|
||||
target_date: null,
|
||||
})
|
||||
}
|
||||
onClick={clearAllFilters}
|
||||
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>
|
||||
|
@ -187,16 +187,19 @@ export const IssuesFilterView: React.FC = () => {
|
||||
?.name ?? "Select"
|
||||
}
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) =>
|
||||
issueView === "kanban" && option.key === null ? null : (
|
||||
{GROUP_BY_OPTIONS.map((option) => {
|
||||
if (issueView === "kanban" && option.key === null) return null;
|
||||
if (option.key === "project") return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
|
@ -1,12 +1,7 @@
|
||||
export * from "./board-view";
|
||||
export * from "./calendar-view";
|
||||
export * from "./filters";
|
||||
export * from "./gantt-chart-view";
|
||||
export * from "./list-view";
|
||||
export * from "./modals";
|
||||
export * from "./spreadsheet-view";
|
||||
export * from "./theme";
|
||||
export * from "./sidebar";
|
||||
export * from "./issues-view";
|
||||
export * from "./image-picker-popover";
|
||||
export * from "./theme";
|
||||
export * from "./views";
|
||||
export * from "./feeds";
|
||||
export * from "./image-picker-popover";
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
203
apps/app/components/core/views/all-views.tsx
Normal file
203
apps/app/components/core/views/all-views.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -1,75 +1,66 @@
|
||||
// hooks
|
||||
import useProjectIssuesView from "hooks/use-issues-view";
|
||||
// components
|
||||
import { SingleBoard } from "components/core/board-view/single-board";
|
||||
import { SingleBoard } from "components/core/views/board-view/single-board";
|
||||
// icons
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types";
|
||||
import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
|
||||
|
||||
type Props = {
|
||||
type: "issue" | "cycle" | "module";
|
||||
states: IState[] | undefined;
|
||||
addIssueToState: (groupTitle: string) => void;
|
||||
makeIssueCopy: (issue: IIssue) => void;
|
||||
handleEditIssue: (issue: IIssue) => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
addIssueToGroup: (groupTitle: string) => void;
|
||||
disableUserActions: boolean;
|
||||
dragDisabled: boolean;
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
handleTrashBox: (isDragging: boolean) => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||
isCompleted?: boolean;
|
||||
states: IState[] | undefined;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
userAuth: UserAuth;
|
||||
viewProps: IIssueViewProps;
|
||||
};
|
||||
|
||||
export const AllBoards: React.FC<Props> = ({
|
||||
type,
|
||||
states,
|
||||
addIssueToState,
|
||||
makeIssueCopy,
|
||||
handleEditIssue,
|
||||
openIssuesListModal,
|
||||
handleDeleteIssue,
|
||||
addIssueToGroup,
|
||||
disableUserActions,
|
||||
dragDisabled,
|
||||
handleIssueAction,
|
||||
handleTrashBox,
|
||||
openIssuesListModal,
|
||||
removeIssue,
|
||||
isCompleted = false,
|
||||
states,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
}) => {
|
||||
const {
|
||||
groupedByIssues,
|
||||
groupByProperty: selectedGroup,
|
||||
showEmptyGroups,
|
||||
} = useProjectIssuesView();
|
||||
const { groupByProperty: selectedGroup, groupedIssues, showEmptyGroups } = viewProps;
|
||||
|
||||
return (
|
||||
<>
|
||||
{groupedByIssues ? (
|
||||
{groupedIssues ? (
|
||||
<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 =
|
||||
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 (
|
||||
<SingleBoard
|
||||
key={index}
|
||||
type={type}
|
||||
addIssueToGroup={() => addIssueToGroup(singleGroup)}
|
||||
currentState={currentState}
|
||||
disableUserActions={disableUserActions}
|
||||
dragDisabled={dragDisabled}
|
||||
groupTitle={singleGroup}
|
||||
handleEditIssue={handleEditIssue}
|
||||
makeIssueCopy={makeIssueCopy}
|
||||
addIssueToState={() => addIssueToState(singleGroup)}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
openIssuesListModal={openIssuesListModal ?? null}
|
||||
handleIssueAction={handleIssueAction}
|
||||
handleTrashBox={handleTrashBox}
|
||||
openIssuesListModal={openIssuesListModal ?? null}
|
||||
removeIssue={removeIssue}
|
||||
isCompleted={isCompleted}
|
||||
user={user}
|
||||
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">
|
||||
<h2 className="text-lg font-semibold">Hidden groups</h2>
|
||||
<div className="space-y-3">
|
||||
{Object.keys(groupedByIssues).map((singleGroup, index) => {
|
||||
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
||||
const currentState =
|
||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||
|
||||
if (groupedByIssues[singleGroup].length === 0)
|
||||
if (groupedIssues[singleGroup].length === 0)
|
||||
return (
|
||||
<div
|
||||
key={index}
|
@ -8,7 +8,7 @@ import useSWR from "swr";
|
||||
import issuesService from "services/issues.service";
|
||||
import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
import useProjects from "hooks/use-projects";
|
||||
// component
|
||||
import { Avatar } from "components/ui";
|
||||
// icons
|
||||
@ -16,47 +16,56 @@ import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicon
|
||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// types
|
||||
import { IIssueLabels, IState } from "types";
|
||||
import { IIssueViewProps, IState } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
currentState?: IState | null;
|
||||
groupTitle: string;
|
||||
addIssueToState: () => void;
|
||||
addIssueToGroup: () => void;
|
||||
isCollapsed: boolean;
|
||||
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isCompleted?: boolean;
|
||||
disableUserActions: boolean;
|
||||
viewProps: IIssueViewProps;
|
||||
};
|
||||
|
||||
export const BoardHeader: React.FC<Props> = ({
|
||||
currentState,
|
||||
groupTitle,
|
||||
addIssueToState,
|
||||
addIssueToGroup,
|
||||
isCollapsed,
|
||||
setIsCollapsed,
|
||||
isCompleted = false,
|
||||
disableUserActions,
|
||||
viewProps,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { groupedByIssues, groupByProperty: selectedGroup } = useIssuesView();
|
||||
const { groupedIssues, groupByProperty: selectedGroup } = viewProps;
|
||||
|
||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
||||
const { data: issueLabels } = useSWR(
|
||||
workspaceSlug && projectId && selectedGroup === "labels"
|
||||
? PROJECT_ISSUE_LABELS(projectId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && selectedGroup === "labels"
|
||||
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: members } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
||||
workspaceSlug && projectId && selectedGroup === "created_by"
|
||||
? PROJECT_MEMBERS(projectId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && selectedGroup === "created_by"
|
||||
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { projects } = useProjects();
|
||||
|
||||
const getGroupTitle = () => {
|
||||
let title = addSpaceIfCamelCase(groupTitle);
|
||||
|
||||
@ -67,6 +76,9 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
case "labels":
|
||||
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
||||
break;
|
||||
case "project":
|
||||
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||
break;
|
||||
case "created_by":
|
||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
||||
title =
|
||||
@ -87,9 +99,22 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
icon =
|
||||
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
|
||||
break;
|
||||
case "state_detail.group":
|
||||
icon = getStateGroupIcon(groupTitle as any, "16", "16");
|
||||
break;
|
||||
case "priority":
|
||||
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||
break;
|
||||
case "project":
|
||||
const project = projects?.find((p) => p.id === groupTitle);
|
||||
icon =
|
||||
project &&
|
||||
(project.emoji !== null
|
||||
? renderEmoji(project.emoji)
|
||||
: project.icon_prop !== null
|
||||
? renderEmoji(project.icon_prop)
|
||||
: null);
|
||||
break;
|
||||
case "labels":
|
||||
const labelColor =
|
||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||
@ -116,7 +141,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
!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
|
||||
className={`flex cursor-pointer items-center gap-x-3 max-w-[316px] ${
|
||||
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
|
||||
@ -126,7 +151,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
<h2
|
||||
className="text-lg font-semibold capitalize truncate"
|
||||
style={{
|
||||
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
|
||||
writingMode: isCollapsed ? "horizontal-tb" : "vertical-rl",
|
||||
}}
|
||||
>
|
||||
{getGroupTitle()}
|
||||
@ -136,7 +161,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
isCollapsed ? "ml-0.5" : ""
|
||||
} 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>
|
||||
</div>
|
||||
</div>
|
||||
@ -155,11 +180,11 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
<ArrowsPointingOutIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
{!isCompleted && selectedGroup !== "created_by" && (
|
||||
{!disableUserActions && selectedGroup !== "created_by" && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
|
||||
onClick={addIssueToState}
|
||||
onClick={addIssueToGroup}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</button>
|
@ -5,9 +5,6 @@ import { useRouter } from "next/router";
|
||||
// react-beautiful-dnd
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
// hooks
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
// components
|
||||
import { BoardHeader, SingleBoardIssue } from "components/core";
|
||||
// ui
|
||||
@ -17,64 +14,63 @@ import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types";
|
||||
import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
|
||||
|
||||
type Props = {
|
||||
type?: "issue" | "cycle" | "module";
|
||||
addIssueToGroup: () => void;
|
||||
currentState?: IState | null;
|
||||
disableUserActions: boolean;
|
||||
dragDisabled: boolean;
|
||||
groupTitle: string;
|
||||
handleEditIssue: (issue: IIssue) => void;
|
||||
makeIssueCopy: (issue: IIssue) => void;
|
||||
addIssueToState: () => void;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
handleTrashBox: (isDragging: boolean) => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||
isCompleted?: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
userAuth: UserAuth;
|
||||
viewProps: IIssueViewProps;
|
||||
};
|
||||
|
||||
export const SingleBoard: React.FC<Props> = ({
|
||||
type,
|
||||
addIssueToGroup,
|
||||
currentState,
|
||||
groupTitle,
|
||||
handleEditIssue,
|
||||
makeIssueCopy,
|
||||
addIssueToState,
|
||||
handleDeleteIssue,
|
||||
openIssuesListModal,
|
||||
disableUserActions,
|
||||
dragDisabled,
|
||||
handleIssueAction,
|
||||
handleTrashBox,
|
||||
openIssuesListModal,
|
||||
removeIssue,
|
||||
isCompleted = false,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
}) => {
|
||||
// collapse/expand
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
|
||||
const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useIssuesView();
|
||||
const { groupedIssues, groupByProperty: selectedGroup, orderBy, properties } = viewProps;
|
||||
|
||||
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
|
||||
const issuesLength = groupedByIssues?.[groupTitle].length;
|
||||
const issuesLength = groupedIssues?.[groupTitle].length;
|
||||
const hasMinimumNumberOfCards = issuesLength ? issuesLength >= 4 : false;
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||
|
||||
return (
|
||||
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
|
||||
<BoardHeader
|
||||
addIssueToState={addIssueToState}
|
||||
addIssueToGroup={addIssueToGroup}
|
||||
currentState={currentState}
|
||||
groupTitle={groupTitle}
|
||||
isCollapsed={isCollapsed}
|
||||
setIsCollapsed={setIsCollapsed}
|
||||
isCompleted={isCompleted}
|
||||
disableUserActions={disableUserActions}
|
||||
viewProps={viewProps}
|
||||
/>
|
||||
{isCollapsed && (
|
||||
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
||||
@ -112,14 +108,12 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
|
||||
} `}
|
||||
>
|
||||
{groupedByIssues?.[groupTitle].map((issue, index) => (
|
||||
{groupedIssues?.[groupTitle].map((issue, index) => (
|
||||
<Draggable
|
||||
key={issue.id}
|
||||
draggableId={issue.id}
|
||||
index={index}
|
||||
isDragDisabled={
|
||||
isNotAllowed || selectedGroup === "created_by" || selectedGroup === "labels"
|
||||
}
|
||||
isDragDisabled={isNotAllowed || dragDisabled}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<SingleBoardIssue
|
||||
@ -128,21 +122,20 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
snapshot={snapshot}
|
||||
type={type}
|
||||
index={index}
|
||||
selectedGroup={selectedGroup}
|
||||
issue={issue}
|
||||
groupTitle={groupTitle}
|
||||
properties={properties}
|
||||
editIssue={() => handleEditIssue(issue)}
|
||||
makeIssueCopy={() => makeIssueCopy(issue)}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
editIssue={() => handleIssueAction(issue, "edit")}
|
||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||
handleTrashBox={handleTrashBox}
|
||||
removeIssue={() => {
|
||||
if (removeIssue && issue.bridge_id)
|
||||
removeIssue(issue.bridge_id, issue.id);
|
||||
}}
|
||||
isCompleted={isCompleted}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
viewProps={viewProps}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
@ -161,18 +154,18 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
<button
|
||||
type="button"
|
||||
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" />
|
||||
Add Issue
|
||||
</button>
|
||||
) : (
|
||||
!isCompleted && (
|
||||
!disableUserActions && (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<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" />
|
||||
Add Issue
|
||||
@ -181,7 +174,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
position="left"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={addIssueToState}>
|
||||
<CustomMenu.MenuItem onClick={addIssueToGroup}>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
@ -15,7 +15,6 @@ import {
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// hooks
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
import useToast from "hooks/use-toast";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
@ -45,14 +44,7 @@ import { LayerDiagonalIcon } from "components/icons";
|
||||
import { handleIssuesMutation } from "constants/issue";
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import {
|
||||
ICurrentUserResponse,
|
||||
IIssue,
|
||||
ISubIssueResponse,
|
||||
Properties,
|
||||
TIssueGroupByOptions,
|
||||
UserAuth,
|
||||
} from "types";
|
||||
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_DETAILS,
|
||||
@ -61,6 +53,7 @@ import {
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
SUB_ISSUES,
|
||||
USER_ISSUES,
|
||||
VIEW_ISSUES,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
@ -69,18 +62,17 @@ type Props = {
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
issue: IIssue;
|
||||
properties: Properties;
|
||||
groupTitle?: string;
|
||||
index: number;
|
||||
selectedGroup: TIssueGroupByOptions;
|
||||
editIssue: () => void;
|
||||
makeIssueCopy: () => void;
|
||||
removeIssue?: (() => void) | null;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
handleTrashBox: (isDragging: boolean) => void;
|
||||
isCompleted?: boolean;
|
||||
disableUserActions: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
userAuth: UserAuth;
|
||||
viewProps: IIssueViewProps;
|
||||
};
|
||||
|
||||
export const SingleBoardIssue: React.FC<Props> = ({
|
||||
@ -88,18 +80,17 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
provided,
|
||||
snapshot,
|
||||
issue,
|
||||
properties,
|
||||
index,
|
||||
selectedGroup,
|
||||
editIssue,
|
||||
makeIssueCopy,
|
||||
removeIssue,
|
||||
groupTitle,
|
||||
handleDeleteIssue,
|
||||
handleTrashBox,
|
||||
isCompleted = false,
|
||||
disableUserActions,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
}) => {
|
||||
// context menu
|
||||
const [contextMenu, setContextMenu] = useState(false);
|
||||
@ -108,7 +99,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
|
||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { orderBy, params } = useIssuesView();
|
||||
const { groupByProperty: selectedGroup, orderBy, params, properties } = viewProps;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
@ -117,7 +108,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
|
||||
const partialUpdateIssue = useCallback(
|
||||
(formData: Partial<IIssue>, issue: IIssue) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
if (!workspaceSlug || !issue) return;
|
||||
|
||||
const fetchKey = cycleId
|
||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
|
||||
@ -125,7 +116,9 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
||||
: viewId
|
||||
? 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) {
|
||||
mutate<ISubIssueResponse>(
|
||||
@ -170,7 +163,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
issuesService
|
||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user)
|
||||
.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user)
|
||||
.then(() => {
|
||||
mutate(fetchKey);
|
||||
|
||||
@ -180,7 +173,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
},
|
||||
[
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
cycleId,
|
||||
moduleId,
|
||||
viewId,
|
||||
@ -189,6 +181,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
selectedGroup,
|
||||
orderBy,
|
||||
params,
|
||||
router,
|
||||
user,
|
||||
]
|
||||
);
|
||||
@ -228,7 +221,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
|
||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||
|
||||
return (
|
||||
<>
|
@ -34,19 +34,17 @@ import {
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
handleEditIssue: (issue: IIssue) => void;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
addIssueToDate: (date: string) => void;
|
||||
isCompleted: boolean;
|
||||
disableUserActions: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
userAuth: UserAuth;
|
||||
};
|
||||
|
||||
export const CalendarView: React.FC<Props> = ({
|
||||
handleEditIssue,
|
||||
handleDeleteIssue,
|
||||
handleIssueAction,
|
||||
addIssueToDate,
|
||||
isCompleted = false,
|
||||
disableUserActions,
|
||||
user,
|
||||
userAuth,
|
||||
}) => {
|
||||
@ -167,7 +165,7 @@ export const CalendarView: React.FC<Props> = ({
|
||||
);
|
||||
}, [currentDate]);
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||
|
||||
return calendarIssues ? (
|
||||
<div className="h-full overflow-y-auto">
|
||||
@ -220,10 +218,10 @@ export const CalendarView: React.FC<Props> = ({
|
||||
>
|
||||
{currentViewDaysData.map((date, index) => (
|
||||
<SingleCalendarDate
|
||||
key={`${date}-${index}`}
|
||||
index={index}
|
||||
date={date}
|
||||
handleEditIssue={handleEditIssue}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
handleIssueAction={handleIssueAction}
|
||||
addIssueToDate={addIssueToDate}
|
||||
isMonthlyView={isMonthlyView}
|
||||
showWeekEnds={showWeekEnds}
|
@ -13,8 +13,7 @@ import { formatDate } from "helpers/calendar.helper";
|
||||
import { ICurrentUserResponse, IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
handleEditIssue: (issue: IIssue) => void;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
index: number;
|
||||
date: {
|
||||
date: string;
|
||||
@ -28,8 +27,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const SingleCalendarDate: React.FC<Props> = ({
|
||||
handleEditIssue,
|
||||
handleDeleteIssue,
|
||||
handleIssueAction,
|
||||
date,
|
||||
index,
|
||||
addIssueToDate,
|
||||
@ -72,8 +70,8 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
issue={issue}
|
||||
handleEditIssue={handleEditIssue}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
7
apps/app/components/core/views/index.ts
Normal file
7
apps/app/components/core/views/index.ts
Normal 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";
|
@ -5,38 +5,26 @@ import { useRouter } from "next/router";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import { DropResult } from "react-beautiful-dnd";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import stateService from "services/state.service";
|
||||
import modulesService from "services/modules.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// contexts
|
||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
import useProjectMembers from "hooks/use-project-members";
|
||||
// components
|
||||
import {
|
||||
AllLists,
|
||||
AllBoards,
|
||||
FilterList,
|
||||
CalendarView,
|
||||
GanttChartView,
|
||||
SpreadsheetView,
|
||||
} from "components/core";
|
||||
import { FiltersList, AllViews } from "components/core";
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import { CreateUpdateViewModal } from "components/views";
|
||||
import { TransferIssues, TransferIssuesModal } from "components/cycles";
|
||||
// ui
|
||||
import { EmptyState, PrimaryButton, Spinner, SecondaryButton } from "components/ui";
|
||||
import { PrimaryButton } 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";
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
@ -49,19 +37,18 @@ import {
|
||||
MODULE_DETAILS,
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
PROJECT_ISSUE_LABELS,
|
||||
STATES_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
type?: "issue" | "cycle" | "module";
|
||||
openIssuesListModal?: () => void;
|
||||
isCompleted?: boolean;
|
||||
disableUserActions?: boolean;
|
||||
};
|
||||
|
||||
export const IssuesView: React.FC<Props> = ({
|
||||
type = "issue",
|
||||
openIssuesListModal,
|
||||
isCompleted = false,
|
||||
disableUserActions = false,
|
||||
}) => {
|
||||
// create issue modal
|
||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
||||
@ -83,14 +70,9 @@ export const IssuesView: React.FC<Props> = ({
|
||||
// trash box
|
||||
const [trashBox, setTrashBox] = useState(false);
|
||||
|
||||
// transfer issue
|
||||
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
|
||||
const { memberRole } = useProjectMyMembership();
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
@ -104,7 +86,9 @@ export const IssuesView: React.FC<Props> = ({
|
||||
isEmpty,
|
||||
setFilters,
|
||||
params,
|
||||
showEmptyGroups,
|
||||
} = useIssuesView();
|
||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
@ -114,6 +98,15 @@ export const IssuesView: React.FC<Props> = ({
|
||||
);
|
||||
const states = getStatesList(stateGroups ?? {});
|
||||
|
||||
const { data: labels } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString());
|
||||
|
||||
const handleDeleteIssue = useCallback(
|
||||
(issue: IIssue) => {
|
||||
setDeleteIssueModal(true);
|
||||
@ -123,7 +116,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
);
|
||||
|
||||
const handleOnDragEnd = useCallback(
|
||||
(result: DropResult) => {
|
||||
async (result: DropResult) => {
|
||||
setTrashBox(false);
|
||||
|
||||
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) => {
|
||||
setCreateIssueModal(true);
|
||||
|
||||
@ -335,6 +328,15 @@ export const IssuesView: React.FC<Props> = ({
|
||||
[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(
|
||||
(bridgeId: string, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
@ -421,13 +423,6 @@ export const IssuesView: React.FC<Props> = ({
|
||||
[workspaceSlug, projectId, moduleId, params, selectedGroup, setToastAlert]
|
||||
);
|
||||
|
||||
const handleTrashBox = useCallback(
|
||||
(isDragging: boolean) => {
|
||||
if (isDragging && !trashBox) setTrashBox(true);
|
||||
},
|
||||
[trashBox, setTrashBox]
|
||||
);
|
||||
|
||||
const nullFilters = Object.keys(filters).filter(
|
||||
(key) => filters[key as keyof IIssueFilterOptions] === null
|
||||
);
|
||||
@ -461,14 +456,27 @@ export const IssuesView: React.FC<Props> = ({
|
||||
data={issueToDelete}
|
||||
user={user}
|
||||
/>
|
||||
<TransferIssuesModal
|
||||
handleClose={() => setTransferIssuesModal(false)}
|
||||
isOpen={transferIssuesModal}
|
||||
/>
|
||||
{areFiltersApplied && (
|
||||
<>
|
||||
<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
|
||||
onClick={() => {
|
||||
if (viewId) {
|
||||
@ -492,140 +500,32 @@ export const IssuesView: React.FC<Props> = ({
|
||||
{<div className="mt-3 border-t border-custom-border-200" />}
|
||||
</>
|
||||
)}
|
||||
|
||||
<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>
|
||||
{groupedByIssues ? (
|
||||
!isEmpty || issueView === "kanban" || issueView === "calendar" ? (
|
||||
<>
|
||||
{isCompleted && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
|
||||
{issueView === "list" ? (
|
||||
<AllLists
|
||||
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>
|
||||
<AllViews
|
||||
addIssueToDate={addIssueToDate}
|
||||
addIssueToGroup={addIssueToGroup}
|
||||
disableUserActions={disableUserActions}
|
||||
dragDisabled={
|
||||
selectedGroup === "created_by" ||
|
||||
selectedGroup === "labels" ||
|
||||
selectedGroup === "state_detail.group"
|
||||
}
|
||||
handleOnDragEnd={handleOnDragEnd}
|
||||
handleIssueAction={handleIssueAction}
|
||||
openIssuesListModal={openIssuesListModal ? openIssuesListModal : null}
|
||||
removeIssue={cycleId ? removeIssueFromCycle : moduleId ? removeIssueFromModule : null}
|
||||
trashBox={trashBox}
|
||||
setTrashBox={setTrashBox}
|
||||
viewProps={{
|
||||
groupByProperty: selectedGroup,
|
||||
groupedIssues: groupedByIssues,
|
||||
isEmpty,
|
||||
issueView,
|
||||
orderBy,
|
||||
params,
|
||||
properties,
|
||||
showEmptyGroups,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
62
apps/app/components/core/views/list-view/all-lists.tsx
Normal file
62
apps/app/components/core/views/list-view/all-lists.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -18,8 +18,6 @@ import {
|
||||
ViewPrioritySelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
// hooks
|
||||
import useIssueView from "hooks/use-issues-view";
|
||||
// ui
|
||||
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
|
||||
// icons
|
||||
@ -37,7 +35,14 @@ import { LayerDiagonalIcon } from "components/icons";
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
import { handleIssuesMutation } from "constants/issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, ISubIssueResponse, Properties, UserAuth } from "types";
|
||||
import {
|
||||
ICurrentUserResponse,
|
||||
IIssue,
|
||||
IIssueViewProps,
|
||||
ISubIssueResponse,
|
||||
Properties,
|
||||
UserAuth,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_DETAILS,
|
||||
@ -46,37 +51,38 @@ import {
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
SUB_ISSUES,
|
||||
USER_ISSUES,
|
||||
VIEW_ISSUES,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
type?: string;
|
||||
issue: IIssue;
|
||||
properties: Properties;
|
||||
groupTitle?: string;
|
||||
editIssue: () => void;
|
||||
index: number;
|
||||
makeIssueCopy: () => void;
|
||||
removeIssue?: (() => void) | null;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
isCompleted?: boolean;
|
||||
disableUserActions: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
userAuth: UserAuth;
|
||||
viewProps: IIssueViewProps;
|
||||
};
|
||||
|
||||
export const SingleListIssue: React.FC<Props> = ({
|
||||
type,
|
||||
issue,
|
||||
properties,
|
||||
editIssue,
|
||||
index,
|
||||
makeIssueCopy,
|
||||
removeIssue,
|
||||
groupTitle,
|
||||
handleDeleteIssue,
|
||||
isCompleted = false,
|
||||
disableUserActions,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
}) => {
|
||||
// context menu
|
||||
const [contextMenu, setContextMenu] = useState(false);
|
||||
@ -88,11 +94,11 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { groupByProperty: selectedGroup, orderBy, params } = useIssueView();
|
||||
const { groupByProperty: selectedGroup, orderBy, params, properties } = viewProps;
|
||||
|
||||
const partialUpdateIssue = useCallback(
|
||||
(formData: Partial<IIssue>, issue: IIssue) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
if (!workspaceSlug || !issue) return;
|
||||
|
||||
const fetchKey = cycleId
|
||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
|
||||
@ -100,7 +106,9 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
||||
: viewId
|
||||
? 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) {
|
||||
mutate<ISubIssueResponse>(
|
||||
@ -145,7 +153,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
issuesService
|
||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user)
|
||||
.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user)
|
||||
.then(() => {
|
||||
mutate(fetchKey);
|
||||
|
||||
@ -155,7 +163,6 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
},
|
||||
[
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
cycleId,
|
||||
moduleId,
|
||||
viewId,
|
||||
@ -164,6 +171,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
selectedGroup,
|
||||
orderBy,
|
||||
params,
|
||||
router,
|
||||
user,
|
||||
]
|
||||
);
|
||||
@ -186,7 +194,8 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
? `/${workspaceSlug}/projects/${projectId}/archived-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 (
|
||||
<>
|
@ -8,7 +8,7 @@ import { Disclosure, Transition } from "@headlessui/react";
|
||||
import issuesService from "services/issues.service";
|
||||
import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
import useProjects from "hooks/use-projects";
|
||||
// components
|
||||
import { SingleListIssue } from "components/core";
|
||||
// ui
|
||||
@ -18,58 +18,52 @@ import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// types
|
||||
import {
|
||||
ICurrentUserResponse,
|
||||
IIssue,
|
||||
IIssueLabels,
|
||||
IIssueViewProps,
|
||||
IState,
|
||||
TIssueGroupByOptions,
|
||||
UserAuth,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
type?: "issue" | "cycle" | "module";
|
||||
currentState?: IState | null;
|
||||
groupTitle: string;
|
||||
groupedByIssues: {
|
||||
[key: string]: IIssue[];
|
||||
};
|
||||
selectedGroup: TIssueGroupByOptions;
|
||||
addIssueToState: () => void;
|
||||
makeIssueCopy: (issue: IIssue) => void;
|
||||
handleEditIssue: (issue: IIssue) => void;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
addIssueToGroup: () => void;
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||
isCompleted?: boolean;
|
||||
disableUserActions: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
userAuth: UserAuth;
|
||||
viewProps: IIssueViewProps;
|
||||
};
|
||||
|
||||
export const SingleList: React.FC<Props> = ({
|
||||
type,
|
||||
currentState,
|
||||
groupTitle,
|
||||
groupedByIssues,
|
||||
selectedGroup,
|
||||
addIssueToState,
|
||||
makeIssueCopy,
|
||||
handleEditIssue,
|
||||
handleDeleteIssue,
|
||||
addIssueToGroup,
|
||||
handleIssueAction,
|
||||
openIssuesListModal,
|
||||
removeIssue,
|
||||
isCompleted = false,
|
||||
disableUserActions,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
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[]>(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
||||
@ -85,6 +79,8 @@ export const SingleList: React.FC<Props> = ({
|
||||
: null
|
||||
);
|
||||
|
||||
const { projects } = useProjects();
|
||||
|
||||
const getGroupTitle = () => {
|
||||
let title = addSpaceIfCamelCase(groupTitle);
|
||||
|
||||
@ -95,6 +91,9 @@ export const SingleList: React.FC<Props> = ({
|
||||
case "labels":
|
||||
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
||||
break;
|
||||
case "project":
|
||||
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||
break;
|
||||
case "created_by":
|
||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
||||
title =
|
||||
@ -115,9 +114,22 @@ export const SingleList: React.FC<Props> = ({
|
||||
icon =
|
||||
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
|
||||
break;
|
||||
case "state_detail.group":
|
||||
icon = getStateGroupIcon(groupTitle as any, "16", "16");
|
||||
break;
|
||||
case "priority":
|
||||
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||
break;
|
||||
case "project":
|
||||
const project = projects?.find((p) => p.id === groupTitle);
|
||||
icon =
|
||||
project &&
|
||||
(project.emoji !== null
|
||||
? renderEmoji(project.emoji)
|
||||
: project.icon_prop !== null
|
||||
? renderEmoji(project.icon_prop)
|
||||
: null);
|
||||
break;
|
||||
case "labels":
|
||||
const labelColor =
|
||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||
@ -138,6 +150,8 @@ export const SingleList: React.FC<Props> = ({
|
||||
return icon;
|
||||
};
|
||||
|
||||
if (!groupedIssues) return null;
|
||||
|
||||
return (
|
||||
<Disclosure as="div" defaultOpen>
|
||||
{({ open }) => (
|
||||
@ -156,7 +170,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
<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">
|
||||
{groupedByIssues[groupTitle as keyof IIssue].length}
|
||||
{groupedIssues[groupTitle as keyof IIssue].length}
|
||||
</span>
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
@ -166,11 +180,11 @@ export const SingleList: React.FC<Props> = ({
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={addIssueToState}
|
||||
onClick={addIssueToGroup}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</button>
|
||||
) : isCompleted ? (
|
||||
) : disableUserActions ? (
|
||||
""
|
||||
) : (
|
||||
<CustomMenu
|
||||
@ -182,7 +196,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
position="right"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={addIssueToState}>Create new</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={addIssueToGroup}>Create new</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||
Add an existing issue
|
||||
@ -201,26 +215,26 @@ export const SingleList: React.FC<Props> = ({
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
{groupedByIssues[groupTitle] ? (
|
||||
groupedByIssues[groupTitle].length > 0 ? (
|
||||
groupedByIssues[groupTitle].map((issue, index) => (
|
||||
{groupedIssues[groupTitle] ? (
|
||||
groupedIssues[groupTitle].length > 0 ? (
|
||||
groupedIssues[groupTitle].map((issue, index) => (
|
||||
<SingleListIssue
|
||||
key={issue.id}
|
||||
type={type}
|
||||
issue={issue}
|
||||
properties={properties}
|
||||
groupTitle={groupTitle}
|
||||
index={index}
|
||||
editIssue={() => handleEditIssue(issue)}
|
||||
makeIssueCopy={() => makeIssueCopy(issue)}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
editIssue={() => handleIssueAction(issue, "edit")}
|
||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||
removeIssue={() => {
|
||||
if (removeIssue !== null && issue.bridge_id)
|
||||
removeIssue(issue.bridge_id, issue.id);
|
||||
}}
|
||||
isCompleted={isCompleted}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
viewProps={viewProps}
|
||||
/>
|
||||
))
|
||||
) : (
|
@ -53,7 +53,7 @@ type Props = {
|
||||
handleEditIssue: (issue: IIssue) => void;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
gridTemplateColumns: string;
|
||||
isCompleted?: boolean;
|
||||
disableUserActions: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
userAuth: UserAuth;
|
||||
nestingLevel: number;
|
||||
@ -68,7 +68,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
handleEditIssue,
|
||||
handleDeleteIssue,
|
||||
gridTemplateColumns,
|
||||
isCompleted = false,
|
||||
disableUserActions,
|
||||
user,
|
||||
userAuth,
|
||||
nestingLevel,
|
||||
@ -190,7 +190,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
)}
|
||||
{!isNotAllowed && !isCompleted && (
|
||||
{!isNotAllowed && !disableUserActions && (
|
||||
<div className="absolute top-0 left-2.5 opacity-0 group-hover:opacity-100">
|
||||
<Popover2
|
||||
isOpen={isOpen}
|
@ -8,32 +8,28 @@ import useSubIssue from "hooks/use-sub-issue";
|
||||
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
||||
|
||||
type Props = {
|
||||
key: string;
|
||||
issue: IIssue;
|
||||
index: number;
|
||||
expandedIssues: string[];
|
||||
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
properties: Properties;
|
||||
handleEditIssue: (issue: IIssue) => void;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
gridTemplateColumns: string;
|
||||
isCompleted?: boolean;
|
||||
disableUserActions: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
userAuth: UserAuth;
|
||||
nestingLevel?: number;
|
||||
};
|
||||
|
||||
export const SpreadsheetIssues: React.FC<Props> = ({
|
||||
key,
|
||||
index,
|
||||
issue,
|
||||
expandedIssues,
|
||||
setExpandedIssues,
|
||||
gridTemplateColumns,
|
||||
properties,
|
||||
handleEditIssue,
|
||||
handleDeleteIssue,
|
||||
isCompleted = false,
|
||||
handleIssueAction,
|
||||
disableUserActions,
|
||||
user,
|
||||
userAuth,
|
||||
nestingLevel = 0,
|
||||
@ -64,9 +60,9 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
||||
handleToggleExpand={handleToggleExpand}
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
properties={properties}
|
||||
handleEditIssue={handleEditIssue}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
isCompleted={isCompleted}
|
||||
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
nestingLevel={nestingLevel}
|
||||
@ -76,7 +72,7 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue, subIndex: number) => (
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetIssues
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
@ -85,9 +81,8 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
||||
setExpandedIssues={setExpandedIssues}
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
properties={properties}
|
||||
handleEditIssue={handleEditIssue}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
isCompleted={isCompleted}
|
||||
handleIssueAction={handleIssueAction}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
nestingLevel={nestingLevel + 1}
|
@ -17,28 +17,26 @@ import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type Props = {
|
||||
type: "issue" | "cycle" | "module";
|
||||
handleEditIssue: (issue: IIssue) => void;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
isCompleted?: boolean;
|
||||
disableUserActions: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
userAuth: UserAuth;
|
||||
};
|
||||
|
||||
export const SpreadsheetView: React.FC<Props> = ({
|
||||
type,
|
||||
handleEditIssue,
|
||||
handleDeleteIssue,
|
||||
handleIssueAction,
|
||||
openIssuesListModal,
|
||||
isCompleted = false,
|
||||
disableUserActions,
|
||||
user,
|
||||
userAuth,
|
||||
}) => {
|
||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||
|
||||
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();
|
||||
|
||||
@ -76,9 +74,8 @@ export const SpreadsheetView: React.FC<Props> = ({
|
||||
setExpandedIssues={setExpandedIssues}
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
properties={properties}
|
||||
handleEditIssue={handleEditIssue}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
isCompleted={isCompleted}
|
||||
handleIssueAction={handleIssueAction}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
@ -99,7 +96,7 @@ export const SpreadsheetView: React.FC<Props> = ({
|
||||
Add Issue
|
||||
</button>
|
||||
) : (
|
||||
!isCompleted && (
|
||||
!disableUserActions && (
|
||||
<CustomMenu
|
||||
className="sticky left-0 z-[1]"
|
||||
customButton={
|
@ -5,6 +5,8 @@ import {
|
||||
StartedStateIcon,
|
||||
UnstartedStateIcon,
|
||||
} from "components/icons";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
export const getStateGroupIcon = (
|
||||
stateGroup: "backlog" | "unstarted" | "started" | "completed" | "cancelled",
|
||||
@ -14,15 +16,45 @@ export const getStateGroupIcon = (
|
||||
) => {
|
||||
switch (stateGroup) {
|
||||
case "backlog":
|
||||
return <BacklogStateIcon width={width} height={height} color={color} />;
|
||||
return (
|
||||
<BacklogStateIcon
|
||||
width={width}
|
||||
height={height}
|
||||
color={color ?? STATE_GROUP_COLORS["backlog"]}
|
||||
/>
|
||||
);
|
||||
case "unstarted":
|
||||
return <UnstartedStateIcon width={width} height={height} color={color} />;
|
||||
return (
|
||||
<UnstartedStateIcon
|
||||
width={width}
|
||||
height={height}
|
||||
color={color ?? STATE_GROUP_COLORS["unstarted"]}
|
||||
/>
|
||||
);
|
||||
case "started":
|
||||
return <StartedStateIcon width={width} height={height} color={color} />;
|
||||
return (
|
||||
<StartedStateIcon
|
||||
width={width}
|
||||
height={height}
|
||||
color={color ?? STATE_GROUP_COLORS["started"]}
|
||||
/>
|
||||
);
|
||||
case "completed":
|
||||
return <CompletedStateIcon width={width} height={height} color={color} />;
|
||||
return (
|
||||
<CompletedStateIcon
|
||||
width={width}
|
||||
height={height}
|
||||
color={color ?? STATE_GROUP_COLORS["completed"]}
|
||||
/>
|
||||
);
|
||||
case "cancelled":
|
||||
return <CancelledStateIcon width={width} height={height} color={color} />;
|
||||
return (
|
||||
<CancelledStateIcon
|
||||
width={width}
|
||||
height={height}
|
||||
color={color ?? STATE_GROUP_COLORS["cancelled"]}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
|
@ -37,37 +37,35 @@ export const FiltersDropdown: React.FC = () => {
|
||||
id: "priority",
|
||||
label: "Priority",
|
||||
value: PRIORITIES,
|
||||
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),
|
||||
})),
|
||||
],
|
||||
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: "inbox_status",
|
||||
label: "Status",
|
||||
value: INBOX_STATUS.map((status) => status.value),
|
||||
children: [
|
||||
...INBOX_STATUS.map((status) => ({
|
||||
id: status.key,
|
||||
label: status.label,
|
||||
value: {
|
||||
key: "inbox_status",
|
||||
value: status.value,
|
||||
},
|
||||
selected: filters?.inbox_status?.includes(status.value),
|
||||
})),
|
||||
],
|
||||
hasChildren: true,
|
||||
children: INBOX_STATUS.map((status) => ({
|
||||
id: status.key,
|
||||
label: status.label,
|
||||
value: {
|
||||
key: "inbox_status",
|
||||
value: status.value,
|
||||
},
|
||||
selected: filters?.inbox_status?.includes(status.value),
|
||||
})),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
@ -63,7 +63,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
|
||||
if (!workspaceSlug || !projectId || !data) return;
|
||||
|
||||
await issueServices
|
||||
.deleteIssue(workspaceSlug as string, projectId as string, data.id, user)
|
||||
.deleteIssue(workspaceSlug as string, data.project, data.id, user)
|
||||
.then(() => {
|
||||
if (issueView === "calendar") {
|
||||
const calendarFetchKey = cycleId
|
||||
@ -72,7 +72,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
|
||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
|
||||
: viewId
|
||||
? VIEW_ISSUES(viewId.toString(), calendarParams)
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), calendarParams);
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, calendarParams);
|
||||
|
||||
mutate<IIssue[]>(
|
||||
calendarFetchKey,
|
||||
@ -86,7 +86,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
|
||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams)
|
||||
: viewId
|
||||
? VIEW_ISSUES(viewId.toString(), spreadsheetParams)
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", spreadsheetParams);
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, spreadsheetParams);
|
||||
if (data.parent) {
|
||||
mutate<ISubIssueResponse>(
|
||||
SUB_ISSUES(data.parent.toString()),
|
||||
|
@ -1,5 +1,6 @@
|
||||
export * from "./attachment";
|
||||
export * from "./comment";
|
||||
export * from "./my-issues";
|
||||
export * from "./sidebar-select";
|
||||
export * from "./view-select";
|
||||
export * from "./activity";
|
||||
@ -9,7 +10,6 @@ export * from "./form";
|
||||
export * from "./gantt-chart";
|
||||
export * from "./main-content";
|
||||
export * from "./modal";
|
||||
export * from "./my-issues-list-item";
|
||||
export * from "./parent-issues-list-modal";
|
||||
export * from "./sidebar";
|
||||
export * from "./sub-issues-list";
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
3
apps/app/components/issues/my-issues/index.ts
Normal file
3
apps/app/components/issues/my-issues/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./my-issues-select-filters";
|
||||
export * from "./my-issues-view-options";
|
||||
export * from "./my-issues-view";
|
@ -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>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
290
apps/app/components/issues/my-issues/my-issues-view-options.tsx
Normal file
290
apps/app/components/issues/my-issues/my-issues-view-options.tsx
Normal 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>
|
||||
);
|
||||
};
|
288
apps/app/components/issues/my-issues/my-issues-view.tsx
Normal file
288
apps/app/components/issues/my-issues/my-issues-view.tsx
Normal 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,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,3 +1,5 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
@ -39,12 +41,14 @@ export const ViewStateSelect: React.FC<Props> = ({
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const [fetchStates, setFetchStates] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && issue ? STATES_LIST(issue.project) : null,
|
||||
workspaceSlug && issue
|
||||
workspaceSlug && issue && fetchStates ? STATES_LIST(issue.project) : null,
|
||||
workspaceSlug && issue && fetchStates
|
||||
? () => stateService.getStates(workspaceSlug as string, issue.project)
|
||||
: 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 = (
|
||||
<Tooltip
|
||||
@ -126,6 +130,7 @@ export const ViewStateSelect: React.FC<Props> = ({
|
||||
{...(customButton ? { customButton: stateLabel } : { label: stateLabel })}
|
||||
position={position}
|
||||
disabled={isNotAllowed}
|
||||
onOpen={() => setFetchStates(true)}
|
||||
noChevron
|
||||
/>
|
||||
);
|
||||
|
@ -244,20 +244,7 @@ export const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen, user })
|
||||
<EmojiIconPicker
|
||||
label={
|
||||
<div className="h-[44px] w-[44px] grid place-items-center rounded-md bg-custom-background-80 outline-none text-lg">
|
||||
{value ? (
|
||||
typeof value === "object" ? (
|
||||
<span
|
||||
style={{ color: value.color }}
|
||||
className="material-symbols-rounded text-lg"
|
||||
>
|
||||
{value.name}
|
||||
</span>
|
||||
) : (
|
||||
renderEmoji(value)
|
||||
)
|
||||
) : (
|
||||
"Icon"
|
||||
)}
|
||||
{value ? renderEmoji(value) : "Icon"}
|
||||
</div>
|
||||
}
|
||||
onChange={onChange}
|
||||
|
@ -176,12 +176,7 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({
|
||||
{renderEmoji(project.emoji)}
|
||||
</span>
|
||||
) : project.icon_prop ? (
|
||||
<span
|
||||
style={{ color: project.icon_prop.color }}
|
||||
className="material-symbols-rounded text-lg"
|
||||
>
|
||||
{project.icon_prop.name}
|
||||
</span>
|
||||
renderEmoji(project.icon_prop)
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-3.5 mb-7 break-words">
|
||||
|
@ -150,12 +150,7 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
||||
</span>
|
||||
) : project.icon_prop ? (
|
||||
<div className="h-7 w-7 grid place-items-center">
|
||||
<span
|
||||
style={{ color: project.icon_prop.color }}
|
||||
className="material-symbols-rounded text-lg"
|
||||
>
|
||||
{project.icon_prop.name}
|
||||
</span>
|
||||
{renderEmoji(project.icon_prop)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||
|
@ -39,6 +39,7 @@ export const CustomSearchSelect = ({
|
||||
noChevron = false,
|
||||
onChange,
|
||||
options,
|
||||
onOpen,
|
||||
optionsClassName = "",
|
||||
position = "left",
|
||||
selfPositioned = false,
|
||||
@ -67,115 +68,119 @@ export const CustomSearchSelect = ({
|
||||
className={`${selfPositioned ? "" : "relative"} flex-shrink-0 text-left ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{({ open }: any) => (
|
||||
<>
|
||||
{customButton ? (
|
||||
<Combobox.Button as="div">{customButton}</Combobox.Button>
|
||||
) : (
|
||||
<Combobox.Button
|
||||
type="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"
|
||||
} ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && (
|
||||
<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`}
|
||||
{({ open }: any) => {
|
||||
if (open && onOpen) onOpen();
|
||||
|
||||
return (
|
||||
<>
|
||||
{customButton ? (
|
||||
<Combobox.Button as="div">{customButton}</Combobox.Button>
|
||||
) : (
|
||||
<Combobox.Button
|
||||
type="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"
|
||||
} ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{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"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && (
|
||||
<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 ? (
|
||||
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
|
||||
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<CheckIcon
|
||||
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
{footerOption}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
{footerOption}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
|
1
apps/app/components/ui/dropdowns/types.d.ts
vendored
1
apps/app/components/ui/dropdowns/types.d.ts
vendored
@ -7,6 +7,7 @@ export type DropdownProps = {
|
||||
label?: string | JSX.Element;
|
||||
maxHeight?: "sm" | "rg" | "md" | "lg";
|
||||
noChevron?: boolean;
|
||||
onOpen?: () => void;
|
||||
optionsClassName?: string;
|
||||
position?: "right" | "left";
|
||||
selfPositioned?: boolean;
|
||||
|
@ -2,6 +2,8 @@ import { Fragment, useState } from "react";
|
||||
|
||||
// headless ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Loader } from "components/ui";
|
||||
// icons
|
||||
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/20/solid";
|
||||
@ -10,9 +12,6 @@ type MultiLevelDropdownProps = {
|
||||
label: string;
|
||||
options: {
|
||||
id: string;
|
||||
label: string;
|
||||
value: any;
|
||||
selected?: boolean;
|
||||
children?: {
|
||||
id: string;
|
||||
label: string | JSX.Element;
|
||||
@ -20,6 +19,11 @@ type MultiLevelDropdownProps = {
|
||||
selected?: boolean;
|
||||
element?: JSX.Element;
|
||||
}[];
|
||||
hasChildren: boolean;
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
selected?: boolean;
|
||||
value: any;
|
||||
}[];
|
||||
onSelect: (value: any) => void;
|
||||
direction?: "left" | "right";
|
||||
@ -69,15 +73,15 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
||||
<Menu.Item
|
||||
as="button"
|
||||
onClick={(e: any) => {
|
||||
if (option.children) {
|
||||
if (option.hasChildren) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (option.onClick) option.onClick();
|
||||
|
||||
if (openChildFor === option.id) setOpenChildFor(null);
|
||||
else setOpenChildFor(option.id);
|
||||
} else {
|
||||
onSelect(option.value);
|
||||
}
|
||||
} else onSelect(option.value);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
@ -90,18 +94,18 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
||||
direction === "right" ? "justify-between" : ""
|
||||
}`}
|
||||
>
|
||||
{direction === "left" && option.children && (
|
||||
{direction === "left" && option.hasChildren && (
|
||||
<ChevronLeftIcon className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
<span>{option.label}</span>
|
||||
{direction === "right" && option.children && (
|
||||
{direction === "right" && option.hasChildren && (
|
||||
<ChevronRightIcon className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Menu.Item>
|
||||
{option.children && option.id === openChildFor && (
|
||||
{option.hasChildren && option.id === openChildFor && (
|
||||
<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 ${
|
||||
direction === "left"
|
||||
@ -119,29 +123,38 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-1 p-1">
|
||||
{option.children.map((child) => {
|
||||
if (child.element) return child.element;
|
||||
else
|
||||
return (
|
||||
<button
|
||||
key={child.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(child.value)}
|
||||
className={`${
|
||||
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
|
||||
className={`h-3.5 w-3.5 opacity-0 ${
|
||||
child.selected ? "opacity-100" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{option.children ? (
|
||||
<div className="space-y-1 p-1">
|
||||
{option.children.map((child) => {
|
||||
if (child.element) return child.element;
|
||||
else
|
||||
return (
|
||||
<button
|
||||
key={child.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(child.value)}
|
||||
className={`${
|
||||
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
|
||||
className={`h-3.5 w-3.5 opacity-0 ${
|
||||
child.selected ? "opacity-100" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
|
@ -1,14 +1,28 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// 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
|
||||
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
||||
// components
|
||||
import { FilterList } from "components/core";
|
||||
// helpers
|
||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// types
|
||||
import { IView } from "types";
|
||||
// components
|
||||
import { SelectFilters } from "components/views";
|
||||
import { IQuery, IView } from "types";
|
||||
import issuesService from "services/issues.service";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, STATES_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
handleFormSubmit: (values: IView) => Promise<void>;
|
||||
@ -30,6 +44,9 @@ export const ViewForm: React.FC<Props> = ({
|
||||
data,
|
||||
preLoadedData,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
@ -42,6 +59,26 @@ export const ViewForm: React.FC<Props> = ({
|
||||
});
|
||||
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) => {
|
||||
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(() => {
|
||||
reset({
|
||||
...defaultValues,
|
||||
@ -106,23 +155,38 @@ export const ViewForm: React.FC<Props> = ({
|
||||
onSelect={(option) => {
|
||||
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", {
|
||||
...filters,
|
||||
[key]: [...((filters?.[key] as any[]) ?? []), option.value],
|
||||
});
|
||||
else {
|
||||
setValue("query", {
|
||||
...filters,
|
||||
[key]: (filters?.[key] as any[])?.filter((item) => item !== option.value),
|
||||
});
|
||||
target_date: valueExists ? null : option.value,
|
||||
} as IQuery);
|
||||
} else {
|
||||
if (!filters?.[key]?.includes(option.value))
|
||||
setValue("query", {
|
||||
...filters,
|
||||
[key]: [...((filters?.[key] as any[]) ?? []), option.value],
|
||||
});
|
||||
else {
|
||||
setValue("query", {
|
||||
...filters,
|
||||
[key]: (filters?.[key] as any[])?.filter((item) => item !== option.value),
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FilterList
|
||||
<FiltersList
|
||||
filters={filters}
|
||||
labels={labels}
|
||||
members={members?.map((m) => m.member)}
|
||||
states={states}
|
||||
clearAllFilters={clearAllFilters}
|
||||
setFilters={(query: any) => {
|
||||
setValue("query", {
|
||||
...filters,
|
||||
|
@ -83,121 +83,116 @@ export const SelectFilters: React.FC<Props> = ({
|
||||
id: "priority",
|
||||
label: "Priority",
|
||||
value: PRIORITIES,
|
||||
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),
|
||||
})),
|
||||
],
|
||||
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",
|
||||
label: "State",
|
||||
value: statesList,
|
||||
children: [
|
||||
...statesList.map((state) => ({
|
||||
id: state.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
{getStateGroupIcon(state.group, "16", "16", state.color)} {state.name}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "state",
|
||||
value: state.id,
|
||||
},
|
||||
selected: filters?.state?.includes(state.id),
|
||||
})),
|
||||
],
|
||||
hasChildren: true,
|
||||
children: statesList.map((state) => ({
|
||||
id: state.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
{getStateGroupIcon(state.group, "16", "16", state.color)} {state.name}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "state",
|
||||
value: state.id,
|
||||
},
|
||||
selected: filters?.state?.includes(state.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "assignees",
|
||||
label: "Assignees",
|
||||
value: members,
|
||||
children: [
|
||||
...(members?.map((member) => ({
|
||||
id: member.member.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "assignees",
|
||||
value: member.member.id,
|
||||
},
|
||||
selected: filters?.assignees?.includes(member.member.id),
|
||||
})) ?? []),
|
||||
],
|
||||
hasChildren: true,
|
||||
children: members?.map((member) => ({
|
||||
id: member.member.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "assignees",
|
||||
value: member.member.id,
|
||||
},
|
||||
selected: filters?.assignees?.includes(member.member.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "created_by",
|
||||
label: "Created by",
|
||||
value: members,
|
||||
children: [
|
||||
...(members?.map((member) => ({
|
||||
id: member.member.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "created_by",
|
||||
value: member.member.id,
|
||||
},
|
||||
selected: filters?.created_by?.includes(member.member.id),
|
||||
})) ?? []),
|
||||
],
|
||||
hasChildren: true,
|
||||
children: members?.map((member) => ({
|
||||
id: member.member.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "created_by",
|
||||
value: member.member.id,
|
||||
},
|
||||
selected: filters?.created_by?.includes(member.member.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "labels",
|
||||
label: "Labels",
|
||||
value: issueLabels,
|
||||
children: [
|
||||
...(issueLabels?.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),
|
||||
})) ?? []),
|
||||
],
|
||||
hasChildren: true,
|
||||
children: issueLabels?.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) => ({
|
||||
...DUE_DATES.map((option) => ({
|
||||
id: option.name,
|
||||
label: option.name,
|
||||
value: {
|
||||
@ -205,7 +200,7 @@ export const SelectFilters: React.FC<Props> = ({
|
||||
value: option.value,
|
||||
},
|
||||
selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], option.value),
|
||||
})) ?? []),
|
||||
})),
|
||||
{
|
||||
id: "custom",
|
||||
label: "Custom",
|
||||
|
@ -37,6 +37,29 @@ const inboxParamsToKey = (params: any) => {
|
||||
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 USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
|
||||
export const USER_WORKSPACES = "USER_WORKSPACES";
|
||||
@ -97,6 +120,8 @@ export const PROJECT_ISSUE_BY_STATE = (projectId: string) =>
|
||||
`PROJECT_ISSUE_BY_STATE_${projectId.toUpperCase()}`;
|
||||
export const PROJECT_ISSUE_LABELS = (projectId: string) =>
|
||||
`PROJECT_ISSUE_LABELS_${projectId.toUpperCase()}`;
|
||||
export const WORKSPACE_LABELS = (workspaceSlug: string) =>
|
||||
`WORKSPACE_LABELS_${workspaceSlug.toUpperCase()}`;
|
||||
export const PROJECT_GITHUB_REPOSITORY = (projectId: string) =>
|
||||
`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 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_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_WORKSPACE_DASHBOARD = (workspaceSlug: string) =>
|
||||
`USER_WORKSPACE_DASHBOARD_${workspaceSlug.toUpperCase()}`;
|
||||
|
@ -2,8 +2,10 @@ export const GROUP_BY_OPTIONS: Array<{
|
||||
name: string;
|
||||
key: TIssueGroupByOptions;
|
||||
}> = [
|
||||
{ name: "State", key: "state" },
|
||||
{ name: "States", key: "state" },
|
||||
{ name: "State Groups", key: "state_detail.group" },
|
||||
{ name: "Priority", key: "priority" },
|
||||
{ name: "Project", key: "project" },
|
||||
{ name: "Labels", key: "labels" },
|
||||
{ name: "Created by", key: "created_by" },
|
||||
{ name: "None", key: null },
|
||||
|
@ -91,8 +91,6 @@ export const initialState: StateType = {
|
||||
assignees: null,
|
||||
labels: null,
|
||||
state: null,
|
||||
issue__assignees__id: null,
|
||||
issue__labels__id: null,
|
||||
created_by: null,
|
||||
target_date: null,
|
||||
},
|
||||
|
@ -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));
|
||||
};
|
38
apps/app/helpers/emoji.helper.tsx
Normal file
38
apps/app/helpers/emoji.helper.tsx
Normal 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));
|
||||
};
|
201
apps/app/hooks/my-issues/use-my-issues-filter.tsx
Normal file
201
apps/app/hooks/my-issues/use-my-issues-filter.tsx
Normal 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;
|
61
apps/app/hooks/my-issues/use-my-issues.tsx
Normal file
61
apps/app/hooks/my-issues/use-my-issues.tsx
Normal 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;
|
@ -41,12 +41,6 @@ const useCalendarIssuesView = () => {
|
||||
priority: filters?.priority ? filters?.priority.join(",") : undefined,
|
||||
type: filters?.type ? filters?.type : 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,
|
||||
target_date: calendarDateRange,
|
||||
};
|
||||
|
@ -57,12 +57,6 @@ const useIssuesView = () => {
|
||||
priority: filters?.priority ? filters?.priority.join(",") : undefined,
|
||||
type: filters?.type ? filters?.type : 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,
|
||||
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
|
||||
sub_issue: showSubIssues,
|
||||
|
@ -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;
|
@ -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;
|
@ -6,11 +6,14 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
// hooks
|
||||
import useUser from "./use-user";
|
||||
|
||||
const useProjectMembers = (workspaceSlug: string, projectId: string) => {
|
||||
const useProjectMembers = (workspaceSlug: string | undefined, projectId: string | undefined) => {
|
||||
const { user } = useUser();
|
||||
// fetching project members
|
||||
const { data: members } = useSWR(PROJECT_MEMBERS(projectId), () =>
|
||||
projectService.projectMembers(workspaceSlug, projectId)
|
||||
const { data: members } = useSWR(
|
||||
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);
|
||||
|
@ -42,12 +42,6 @@ const useSpreadsheetIssuesView = () => {
|
||||
priority: filters?.priority ? filters?.priority.join(",") : undefined,
|
||||
type: filters?.type ? filters?.type : 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,
|
||||
sub_issue: "false",
|
||||
};
|
||||
|
@ -6,12 +6,14 @@ import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
// hooks
|
||||
import useUser from "./use-user";
|
||||
|
||||
const useWorkspaceMembers = (workspaceSlug: string) => {
|
||||
const useWorkspaceMembers = (workspaceSlug: string | undefined, fetchCondition?: boolean) => {
|
||||
fetchCondition = fetchCondition ?? true;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const { data: workspaceMembers, error: workspaceMemberErrors } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug) : null,
|
||||
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug) : null
|
||||
workspaceSlug && fetchCondition ? WORKSPACE_MEMBERS(workspaceSlug) : null,
|
||||
workspaceSlug && fetchCondition ? () => workspaceService.workspaceMembers(workspaceSlug) : null
|
||||
);
|
||||
|
||||
const hasJoined = workspaceMembers?.some((item: any) => item.member.id === (user as any)?.id);
|
||||
|
@ -1,40 +1,67 @@
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// headless ui
|
||||
import { Disclosure, Popover, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { ChevronDownIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||
// images
|
||||
import emptyMyIssues from "public/empty-state/my-issues.svg";
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
// hooks
|
||||
import useIssues from "hooks/use-issues";
|
||||
// ui
|
||||
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";
|
||||
import useProjects from "hooks/use-projects";
|
||||
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
|
||||
// components
|
||||
import { MyIssuesListItem } from "components/issues";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
import { MyIssuesView, MyIssuesViewOptions } from "components/issues";
|
||||
// ui
|
||||
import { PrimaryButton } from "components/ui";
|
||||
import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
import useProjects from "hooks/use-projects";
|
||||
import useUser from "hooks/use-user";
|
||||
|
||||
const MyIssuesPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { myIssues } = useIssues(workspaceSlug as string);
|
||||
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 (
|
||||
<WorkspaceAuthorizationLayout
|
||||
@ -45,59 +72,7 @@ const MyIssuesPage: NextPage = () => {
|
||||
}
|
||||
right={
|
||||
<div className="flex items-center gap-2">
|
||||
{myIssues && myIssues.length > 0 && (
|
||||
<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>
|
||||
)}
|
||||
<MyIssuesViewOptions />
|
||||
<PrimaryButton
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
@ -111,87 +86,26 @@ const MyIssuesPage: NextPage = () => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col space-y-5">
|
||||
{myIssues ? (
|
||||
<>
|
||||
{myIssues.length > 0 ? (
|
||||
<Disclosure as="div" defaultOpen>
|
||||
{({ open }) => (
|
||||
<div>
|
||||
<div className="flex items-center px-4 py-2.5 bg-custom-background-90">
|
||||
<Disclosure.Button>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<h2 className="font-medium leading-5">My Issues</h2>
|
||||
<span className="rounded-full bg-custom-background-80 py-0.5 px-3 text-sm text-custom-text-200">
|
||||
{myIssues.length}
|
||||
</span>
|
||||
</div>
|
||||
</Disclosure.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 className="h-full w-full flex flex-col overflow-hidden">
|
||||
<div className="border-b border-custom-border-300">
|
||||
<div className="flex items-center overflow-x-scroll">
|
||||
{tabsList.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
type="button"
|
||||
onClick={tab.onClick}
|
||||
className={`border-b-2 p-4 text-sm font-medium outline-none whitespace-nowrap ${
|
||||
tab.selected
|
||||
? "border-custom-primary-100 text-custom-primary-100"
|
||||
: "border-transparent"
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<MyIssuesView />
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
);
|
||||
|
@ -12,7 +12,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
||||
// components
|
||||
import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "components/core";
|
||||
import { CycleDetailsSidebar } from "components/cycles";
|
||||
import { CycleDetailsSidebar, TransferIssues, TransferIssuesModal } from "components/cycles";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import cycleServices from "services/cycles.service";
|
||||
@ -36,6 +36,7 @@ const SingleCycle: React.FC = () => {
|
||||
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
||||
const [cycleSidebar, setCycleSidebar] = useState(true);
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||
@ -157,16 +158,22 @@ const SingleCycle: React.FC = () => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<TransferIssuesModal
|
||||
handleClose={() => setTransferIssuesModal(false)}
|
||||
isOpen={transferIssuesModal}
|
||||
/>
|
||||
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
||||
<div
|
||||
className={`h-full flex flex-col ${cycleSidebar ? "mr-[24rem]" : ""} ${
|
||||
analyticsModal ? "mr-[50%]" : ""
|
||||
} duration-300`}
|
||||
>
|
||||
{cycleStatus === "completed" && (
|
||||
<TransferIssues handleClick={() => setTransferIssuesModal(true)} />
|
||||
)}
|
||||
<IssuesView
|
||||
type="cycle"
|
||||
openIssuesListModal={openIssuesListModal}
|
||||
isCompleted={cycleStatus === "completed" ?? false}
|
||||
disableUserActions={cycleStatus === "completed" ?? false}
|
||||
/>
|
||||
</div>
|
||||
<CycleDetailsSidebar
|
||||
|
@ -168,7 +168,7 @@ const SingleModule: React.FC = () => {
|
||||
analyticsModal ? "mr-[50%]" : ""
|
||||
} duration-300`}
|
||||
>
|
||||
<IssuesView type="module" openIssuesListModal={openIssuesListModal} />
|
||||
<IssuesView openIssuesListModal={openIssuesListModal} />
|
||||
</div>
|
||||
|
||||
<ModuleDetailsSidebar
|
||||
|
@ -195,22 +195,7 @@ const GeneralSettings: NextPage = () => {
|
||||
name="emoji_and_icon"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<EmojiIconPicker
|
||||
label={
|
||||
value ? (
|
||||
typeof value === "object" ? (
|
||||
<span
|
||||
style={{ color: value.color }}
|
||||
className="material-symbols-rounded text-lg"
|
||||
>
|
||||
{value.name}
|
||||
</span>
|
||||
) : (
|
||||
renderEmoji(value)
|
||||
)
|
||||
) : (
|
||||
"Icon"
|
||||
)
|
||||
}
|
||||
label={value ? renderEmoji(value) : "Icon"}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
@ -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[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`)
|
||||
.then((response) => response?.data)
|
||||
|
@ -4,6 +4,7 @@ import trackEventServices from "services/track-event.service";
|
||||
|
||||
import type {
|
||||
ICurrentUserResponse,
|
||||
IIssue,
|
||||
IUser,
|
||||
IUserActivityResponse,
|
||||
IUserWorkspaceDashboard,
|
||||
@ -26,8 +27,18 @@ class UserService extends APIService {
|
||||
};
|
||||
}
|
||||
|
||||
async userIssues(workspaceSlug: string): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/my-issues/`)
|
||||
async userIssues(
|
||||
workspaceSlug: string,
|
||||
params: any
|
||||
): Promise<
|
||||
| {
|
||||
[key: string]: IIssue[];
|
||||
}
|
||||
| IIssue[]
|
||||
> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/my-issues/`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
IProductUpdateResponse,
|
||||
ICurrentUserResponse,
|
||||
IWorkspaceBulkInviteFormData,
|
||||
IWorkspaceViewProps,
|
||||
} from "types";
|
||||
|
||||
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)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
|
22
apps/app/types/issues.d.ts
vendored
22
apps/app/types/issues.d.ts
vendored
@ -232,15 +232,20 @@ export interface IIssueFilterOptions {
|
||||
target_date: string[] | null;
|
||||
state: string[] | null;
|
||||
labels: string[] | null;
|
||||
issue__assignees__id: string[] | null;
|
||||
issue__labels__id: string[] | null;
|
||||
priority: string[] | null;
|
||||
created_by: string[] | null;
|
||||
}
|
||||
|
||||
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 =
|
||||
| "-created_at"
|
||||
@ -279,3 +284,14 @@ export interface IIssueAttachment {
|
||||
updated_by: 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;
|
||||
}
|
||||
|
23
apps/app/types/views.d.ts
vendored
23
apps/app/types/views.d.ts
vendored
@ -15,26 +15,11 @@ export interface IView {
|
||||
}
|
||||
|
||||
export interface IQuery {
|
||||
state: string[] | null;
|
||||
parent: string[] | null;
|
||||
priority: string[] | null;
|
||||
labels: string[] | null;
|
||||
assignees: string[] | null;
|
||||
created_by: string[] | null;
|
||||
name: string | null;
|
||||
created_at: [
|
||||
{
|
||||
datetime: string;
|
||||
timeline: "before";
|
||||
},
|
||||
{
|
||||
datetime: string;
|
||||
timeline: "after";
|
||||
}
|
||||
];
|
||||
updated_at: string[] | null;
|
||||
start_date: string[] | null;
|
||||
labels: string[] | null;
|
||||
priority: string[] | null;
|
||||
state: string[] | null;
|
||||
target_date: string[] | null;
|
||||
completed_at: string[] | null;
|
||||
type: string;
|
||||
type: "active" | "backlog" | null;
|
||||
}
|
||||
|
25
apps/app/types/workspace.d.ts
vendored
25
apps/app/types/workspace.d.ts
vendored
@ -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 {
|
||||
readonly id: string;
|
||||
@ -54,6 +62,19 @@ export type Properties = {
|
||||
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 {
|
||||
readonly id: string;
|
||||
user: IUserLite;
|
||||
@ -61,7 +82,7 @@ export interface IWorkspaceMember {
|
||||
member: IUserLite;
|
||||
role: 5 | 10 | 15 | 20;
|
||||
company_role: string | null;
|
||||
view_props: Properties;
|
||||
view_props: IWorkspaceViewProps;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
created_by: string;
|
||||
|
Loading…
Reference in New Issue
Block a user