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>
|
</span>
|
||||||
) : project.icon_prop ? (
|
) : project.icon_prop ? (
|
||||||
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
||||||
<span
|
{renderEmoji(project.icon_prop)}
|
||||||
style={{ color: project.icon_prop.color }}
|
|
||||||
className="material-symbols-rounded text-lg"
|
|
||||||
>
|
|
||||||
{project.icon_prop.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||||
@ -342,12 +337,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
) : projectDetails?.icon_prop ? (
|
) : projectDetails?.icon_prop ? (
|
||||||
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
||||||
<span
|
{renderEmoji(projectDetails.icon_prop)}
|
||||||
style={{ color: projectDetails.icon_prop.color }}
|
|
||||||
className="material-symbols-rounded text-lg"
|
|
||||||
>
|
|
||||||
{projectDetails.icon_prop.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
@ -8,42 +8,31 @@ import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
|||||||
// ui
|
// ui
|
||||||
import { Avatar } from "components/ui";
|
import { Avatar } from "components/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { getStatesList } from "helpers/state.helper";
|
|
||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
// services
|
// helpers
|
||||||
import issuesService from "services/issues.service";
|
|
||||||
import projectService from "services/project.service";
|
|
||||||
import stateService from "services/state.service";
|
|
||||||
// types
|
|
||||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
|
||||||
import { IIssueFilterOptions } from "types";
|
|
||||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
|
// types
|
||||||
|
import { IIssueFilterOptions, IIssueLabels, IState, IUserLite } from "types";
|
||||||
|
|
||||||
export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
type Props = {
|
||||||
|
filters: any;
|
||||||
|
setFilters: any;
|
||||||
|
clearAllFilters: (...args: any) => void;
|
||||||
|
labels: IIssueLabels[] | undefined;
|
||||||
|
members: IUserLite[] | undefined;
|
||||||
|
states: IState[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FiltersList: React.FC<Props> = ({
|
||||||
|
filters,
|
||||||
|
setFilters,
|
||||||
|
clearAllFilters,
|
||||||
|
labels,
|
||||||
|
members,
|
||||||
|
states,
|
||||||
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, viewId } = router.query;
|
const { viewId } = router.query;
|
||||||
|
|
||||||
const { data: members } = useSWR(
|
|
||||||
projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: issueLabels } = useSWR(
|
|
||||||
projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId.toString())
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: stateGroups } = useSWR(
|
|
||||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
|
||||||
workspaceSlug
|
|
||||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
const states = getStatesList(stateGroups ?? {});
|
|
||||||
|
|
||||||
if (!filters) return <></>;
|
if (!filters) return <></>;
|
||||||
|
|
||||||
@ -166,7 +155,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
) : key === "assignees" ? (
|
) : key === "assignees" ? (
|
||||||
<div className="flex flex-wrap items-center gap-1">
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
{filters.assignees?.map((memberId: string) => {
|
{filters.assignees?.map((memberId: string) => {
|
||||||
const member = members?.find((m) => m.member.id === memberId)?.member;
|
const member = members?.find((m) => m.id === memberId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -207,7 +196,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
) : key === "created_by" ? (
|
) : key === "created_by" ? (
|
||||||
<div className="flex flex-wrap items-center gap-1">
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
{filters.created_by?.map((memberId: string) => {
|
{filters.created_by?.map((memberId: string) => {
|
||||||
const member = members?.find((m) => m.member.id === memberId)?.member;
|
const member = members?.find((m) => m.id === memberId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -248,7 +237,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
) : key === "labels" ? (
|
) : key === "labels" ? (
|
||||||
<div className="flex flex-wrap items-center gap-1">
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
{filters.labels?.map((labelId: string) => {
|
{filters.labels?.map((labelId: string) => {
|
||||||
const label = issueLabels?.find((l) => l.id === labelId);
|
const label = labels?.find((l) => l.id === labelId);
|
||||||
|
|
||||||
if (!label) return null;
|
if (!label) return null;
|
||||||
const color = label.color !== "" ? label.color : "#0f172a";
|
const color = label.color !== "" ? label.color : "#0f172a";
|
||||||
@ -370,17 +359,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
|||||||
{Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && (
|
{Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={clearAllFilters}
|
||||||
setFilters({
|
|
||||||
type: null,
|
|
||||||
state: null,
|
|
||||||
priority: null,
|
|
||||||
assignees: null,
|
|
||||||
labels: null,
|
|
||||||
created_by: null,
|
|
||||||
target_date: null,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-80 px-3 py-1.5 text-xs"
|
className="flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-80 px-3 py-1.5 text-xs"
|
||||||
>
|
>
|
||||||
<span>Clear all filters</span>
|
<span>Clear all filters</span>
|
||||||
|
@ -187,16 +187,19 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
?.name ?? "Select"
|
?.name ?? "Select"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{GROUP_BY_OPTIONS.map((option) =>
|
{GROUP_BY_OPTIONS.map((option) => {
|
||||||
issueView === "kanban" && option.key === null ? null : (
|
if (issueView === "kanban" && option.key === null) return null;
|
||||||
|
if (option.key === "project") return null;
|
||||||
|
|
||||||
|
return (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
key={option.key}
|
key={option.key}
|
||||||
onClick={() => setGroupByProperty(option.key)}
|
onClick={() => setGroupByProperty(option.key)}
|
||||||
>
|
>
|
||||||
{option.name}
|
{option.name}
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)
|
);
|
||||||
)}
|
})}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
export * from "./board-view";
|
|
||||||
export * from "./calendar-view";
|
|
||||||
export * from "./filters";
|
export * from "./filters";
|
||||||
export * from "./gantt-chart-view";
|
|
||||||
export * from "./list-view";
|
|
||||||
export * from "./modals";
|
export * from "./modals";
|
||||||
export * from "./spreadsheet-view";
|
|
||||||
export * from "./theme";
|
|
||||||
export * from "./sidebar";
|
export * from "./sidebar";
|
||||||
export * from "./issues-view";
|
export * from "./theme";
|
||||||
export * from "./image-picker-popover";
|
export * from "./views";
|
||||||
export * from "./feeds";
|
export * from "./feeds";
|
||||||
|
export * from "./image-picker-popover";
|
||||||
|
@ -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
|
// components
|
||||||
import { SingleBoard } from "components/core/board-view/single-board";
|
import { SingleBoard } from "components/core/views/board-view/single-board";
|
||||||
// icons
|
// icons
|
||||||
import { getStateGroupIcon } from "components/icons";
|
import { getStateGroupIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types";
|
import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type: "issue" | "cycle" | "module";
|
addIssueToGroup: (groupTitle: string) => void;
|
||||||
states: IState[] | undefined;
|
disableUserActions: boolean;
|
||||||
addIssueToState: (groupTitle: string) => void;
|
dragDisabled: boolean;
|
||||||
makeIssueCopy: (issue: IIssue) => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
handleEditIssue: (issue: IIssue) => void;
|
|
||||||
openIssuesListModal?: (() => void) | null;
|
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
|
openIssuesListModal?: (() => void) | null;
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
isCompleted?: boolean;
|
states: IState[] | undefined;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
|
viewProps: IIssueViewProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AllBoards: React.FC<Props> = ({
|
export const AllBoards: React.FC<Props> = ({
|
||||||
type,
|
addIssueToGroup,
|
||||||
states,
|
disableUserActions,
|
||||||
addIssueToState,
|
dragDisabled,
|
||||||
makeIssueCopy,
|
handleIssueAction,
|
||||||
handleEditIssue,
|
|
||||||
openIssuesListModal,
|
|
||||||
handleDeleteIssue,
|
|
||||||
handleTrashBox,
|
handleTrashBox,
|
||||||
|
openIssuesListModal,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
isCompleted = false,
|
states,
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
|
viewProps,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const { groupByProperty: selectedGroup, groupedIssues, showEmptyGroups } = viewProps;
|
||||||
groupedByIssues,
|
|
||||||
groupByProperty: selectedGroup,
|
|
||||||
showEmptyGroups,
|
|
||||||
} = useProjectIssuesView();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{groupedByIssues ? (
|
{groupedIssues ? (
|
||||||
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8">
|
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8">
|
||||||
{Object.keys(groupedByIssues).map((singleGroup, index) => {
|
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
||||||
const currentState =
|
const currentState =
|
||||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||||
|
|
||||||
if (!showEmptyGroups && groupedByIssues[singleGroup].length === 0) return null;
|
if (!showEmptyGroups && groupedIssues[singleGroup].length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SingleBoard
|
<SingleBoard
|
||||||
key={index}
|
key={index}
|
||||||
type={type}
|
addIssueToGroup={() => addIssueToGroup(singleGroup)}
|
||||||
currentState={currentState}
|
currentState={currentState}
|
||||||
|
disableUserActions={disableUserActions}
|
||||||
|
dragDisabled={dragDisabled}
|
||||||
groupTitle={singleGroup}
|
groupTitle={singleGroup}
|
||||||
handleEditIssue={handleEditIssue}
|
handleIssueAction={handleIssueAction}
|
||||||
makeIssueCopy={makeIssueCopy}
|
|
||||||
addIssueToState={() => addIssueToState(singleGroup)}
|
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
openIssuesListModal={openIssuesListModal ?? null}
|
|
||||||
handleTrashBox={handleTrashBox}
|
handleTrashBox={handleTrashBox}
|
||||||
|
openIssuesListModal={openIssuesListModal ?? null}
|
||||||
removeIssue={removeIssue}
|
removeIssue={removeIssue}
|
||||||
isCompleted={isCompleted}
|
|
||||||
user={user}
|
user={user}
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
|
viewProps={viewProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -77,11 +68,11 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
<div className="h-full w-96 flex-shrink-0 space-y-2 p-1">
|
<div className="h-full w-96 flex-shrink-0 space-y-2 p-1">
|
||||||
<h2 className="text-lg font-semibold">Hidden groups</h2>
|
<h2 className="text-lg font-semibold">Hidden groups</h2>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{Object.keys(groupedByIssues).map((singleGroup, index) => {
|
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
||||||
const currentState =
|
const currentState =
|
||||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||||
|
|
||||||
if (groupedByIssues[singleGroup].length === 0)
|
if (groupedIssues[singleGroup].length === 0)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
@ -8,7 +8,7 @@ import useSWR from "swr";
|
|||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useIssuesView from "hooks/use-issues-view";
|
import useProjects from "hooks/use-projects";
|
||||||
// component
|
// component
|
||||||
import { Avatar } from "components/ui";
|
import { Avatar } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -16,47 +16,56 @@ import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicon
|
|||||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssueLabels, IState } from "types";
|
import { IIssueViewProps, IState } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
currentState?: IState | null;
|
currentState?: IState | null;
|
||||||
groupTitle: string;
|
groupTitle: string;
|
||||||
addIssueToState: () => void;
|
addIssueToGroup: () => void;
|
||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
isCompleted?: boolean;
|
disableUserActions: boolean;
|
||||||
|
viewProps: IIssueViewProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BoardHeader: React.FC<Props> = ({
|
export const BoardHeader: React.FC<Props> = ({
|
||||||
currentState,
|
currentState,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
addIssueToState,
|
addIssueToGroup,
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
setIsCollapsed,
|
setIsCollapsed,
|
||||||
isCompleted = false,
|
disableUserActions,
|
||||||
|
viewProps,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const { groupedByIssues, groupByProperty: selectedGroup } = useIssuesView();
|
const { groupedIssues, groupByProperty: selectedGroup } = viewProps;
|
||||||
|
|
||||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
const { data: issueLabels } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
workspaceSlug && projectId && selectedGroup === "labels"
|
||||||
workspaceSlug && projectId
|
? PROJECT_ISSUE_LABELS(projectId.toString())
|
||||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
: null,
|
||||||
|
workspaceSlug && projectId && selectedGroup === "labels"
|
||||||
|
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: members } = useSWR(
|
const { data: members } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
workspaceSlug && projectId && selectedGroup === "created_by"
|
||||||
workspaceSlug && projectId
|
? PROJECT_MEMBERS(projectId.toString())
|
||||||
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
: null,
|
||||||
|
workspaceSlug && projectId && selectedGroup === "created_by"
|
||||||
|
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { projects } = useProjects();
|
||||||
|
|
||||||
const getGroupTitle = () => {
|
const getGroupTitle = () => {
|
||||||
let title = addSpaceIfCamelCase(groupTitle);
|
let title = addSpaceIfCamelCase(groupTitle);
|
||||||
|
|
||||||
@ -67,6 +76,9 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
case "labels":
|
case "labels":
|
||||||
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
||||||
break;
|
break;
|
||||||
|
case "project":
|
||||||
|
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||||
|
break;
|
||||||
case "created_by":
|
case "created_by":
|
||||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
||||||
title =
|
title =
|
||||||
@ -87,9 +99,22 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
icon =
|
icon =
|
||||||
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
|
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
|
||||||
break;
|
break;
|
||||||
|
case "state_detail.group":
|
||||||
|
icon = getStateGroupIcon(groupTitle as any, "16", "16");
|
||||||
|
break;
|
||||||
case "priority":
|
case "priority":
|
||||||
icon = getPriorityIcon(groupTitle, "text-lg");
|
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||||
break;
|
break;
|
||||||
|
case "project":
|
||||||
|
const project = projects?.find((p) => p.id === groupTitle);
|
||||||
|
icon =
|
||||||
|
project &&
|
||||||
|
(project.emoji !== null
|
||||||
|
? renderEmoji(project.emoji)
|
||||||
|
: project.icon_prop !== null
|
||||||
|
? renderEmoji(project.icon_prop)
|
||||||
|
: null);
|
||||||
|
break;
|
||||||
case "labels":
|
case "labels":
|
||||||
const labelColor =
|
const labelColor =
|
||||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||||
@ -116,7 +141,7 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
!isCollapsed ? "flex-col rounded-md bg-custom-background-90" : ""
|
!isCollapsed ? "flex-col rounded-md bg-custom-background-90" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
|
<div className={`flex items-center ${isCollapsed ? "gap-1" : "flex-col gap-2"}`}>
|
||||||
<div
|
<div
|
||||||
className={`flex cursor-pointer items-center gap-x-3 max-w-[316px] ${
|
className={`flex cursor-pointer items-center gap-x-3 max-w-[316px] ${
|
||||||
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
|
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
|
||||||
@ -126,7 +151,7 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
<h2
|
<h2
|
||||||
className="text-lg font-semibold capitalize truncate"
|
className="text-lg font-semibold capitalize truncate"
|
||||||
style={{
|
style={{
|
||||||
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
|
writingMode: isCollapsed ? "horizontal-tb" : "vertical-rl",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{getGroupTitle()}
|
{getGroupTitle()}
|
||||||
@ -136,7 +161,7 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
isCollapsed ? "ml-0.5" : ""
|
isCollapsed ? "ml-0.5" : ""
|
||||||
} min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs`}
|
} min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs`}
|
||||||
>
|
>
|
||||||
{groupedByIssues?.[groupTitle].length ?? 0}
|
{groupedIssues?.[groupTitle].length ?? 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -155,11 +180,11 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
<ArrowsPointingOutIcon className="h-4 w-4" />
|
<ArrowsPointingOutIcon className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{!isCompleted && selectedGroup !== "created_by" && (
|
{!disableUserActions && selectedGroup !== "created_by" && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
|
className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
|
||||||
onClick={addIssueToState}
|
onClick={addIssueToGroup}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
@ -5,9 +5,6 @@ import { useRouter } from "next/router";
|
|||||||
// react-beautiful-dnd
|
// react-beautiful-dnd
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
import { Draggable } from "react-beautiful-dnd";
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
// hooks
|
|
||||||
import useIssuesView from "hooks/use-issues-view";
|
|
||||||
import useIssuesProperties from "hooks/use-issue-properties";
|
|
||||||
// components
|
// components
|
||||||
import { BoardHeader, SingleBoardIssue } from "components/core";
|
import { BoardHeader, SingleBoardIssue } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
@ -17,64 +14,63 @@ import { PlusIcon } from "@heroicons/react/24/outline";
|
|||||||
// helpers
|
// helpers
|
||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types";
|
import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type?: "issue" | "cycle" | "module";
|
addIssueToGroup: () => void;
|
||||||
currentState?: IState | null;
|
currentState?: IState | null;
|
||||||
|
disableUserActions: boolean;
|
||||||
|
dragDisabled: boolean;
|
||||||
groupTitle: string;
|
groupTitle: string;
|
||||||
handleEditIssue: (issue: IIssue) => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
makeIssueCopy: (issue: IIssue) => void;
|
|
||||||
addIssueToState: () => void;
|
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
|
||||||
openIssuesListModal?: (() => void) | null;
|
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
|
openIssuesListModal?: (() => void) | null;
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
isCompleted?: boolean;
|
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
|
viewProps: IIssueViewProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleBoard: React.FC<Props> = ({
|
export const SingleBoard: React.FC<Props> = ({
|
||||||
type,
|
addIssueToGroup,
|
||||||
currentState,
|
currentState,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
handleEditIssue,
|
disableUserActions,
|
||||||
makeIssueCopy,
|
dragDisabled,
|
||||||
addIssueToState,
|
handleIssueAction,
|
||||||
handleDeleteIssue,
|
|
||||||
openIssuesListModal,
|
|
||||||
handleTrashBox,
|
handleTrashBox,
|
||||||
|
openIssuesListModal,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
isCompleted = false,
|
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
|
viewProps,
|
||||||
}) => {
|
}) => {
|
||||||
// collapse/expand
|
// collapse/expand
|
||||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||||
|
|
||||||
const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useIssuesView();
|
const { groupedIssues, groupByProperty: selectedGroup, orderBy, properties } = viewProps;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||||
|
|
||||||
// Check if it has at least 4 tickets since it is enough to accommodate the Calendar height
|
// Check if it has at least 4 tickets since it is enough to accommodate the Calendar height
|
||||||
const issuesLength = groupedByIssues?.[groupTitle].length;
|
const issuesLength = groupedIssues?.[groupTitle].length;
|
||||||
const hasMinimumNumberOfCards = issuesLength ? issuesLength >= 4 : false;
|
const hasMinimumNumberOfCards = issuesLength ? issuesLength >= 4 : false;
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
|
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
|
||||||
<BoardHeader
|
<BoardHeader
|
||||||
addIssueToState={addIssueToState}
|
addIssueToGroup={addIssueToGroup}
|
||||||
currentState={currentState}
|
currentState={currentState}
|
||||||
groupTitle={groupTitle}
|
groupTitle={groupTitle}
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
setIsCollapsed={setIsCollapsed}
|
setIsCollapsed={setIsCollapsed}
|
||||||
isCompleted={isCompleted}
|
disableUserActions={disableUserActions}
|
||||||
|
viewProps={viewProps}
|
||||||
/>
|
/>
|
||||||
{isCollapsed && (
|
{isCollapsed && (
|
||||||
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
||||||
@ -112,14 +108,12 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
|
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
|
||||||
} `}
|
} `}
|
||||||
>
|
>
|
||||||
{groupedByIssues?.[groupTitle].map((issue, index) => (
|
{groupedIssues?.[groupTitle].map((issue, index) => (
|
||||||
<Draggable
|
<Draggable
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
draggableId={issue.id}
|
draggableId={issue.id}
|
||||||
index={index}
|
index={index}
|
||||||
isDragDisabled={
|
isDragDisabled={isNotAllowed || dragDisabled}
|
||||||
isNotAllowed || selectedGroup === "created_by" || selectedGroup === "labels"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<SingleBoardIssue
|
<SingleBoardIssue
|
||||||
@ -128,21 +122,20 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
snapshot={snapshot}
|
snapshot={snapshot}
|
||||||
type={type}
|
type={type}
|
||||||
index={index}
|
index={index}
|
||||||
selectedGroup={selectedGroup}
|
|
||||||
issue={issue}
|
issue={issue}
|
||||||
groupTitle={groupTitle}
|
groupTitle={groupTitle}
|
||||||
properties={properties}
|
editIssue={() => handleIssueAction(issue, "edit")}
|
||||||
editIssue={() => handleEditIssue(issue)}
|
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||||
makeIssueCopy={() => makeIssueCopy(issue)}
|
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
handleTrashBox={handleTrashBox}
|
handleTrashBox={handleTrashBox}
|
||||||
removeIssue={() => {
|
removeIssue={() => {
|
||||||
if (removeIssue && issue.bridge_id)
|
if (removeIssue && issue.bridge_id)
|
||||||
removeIssue(issue.bridge_id, issue.id);
|
removeIssue(issue.bridge_id, issue.id);
|
||||||
}}
|
}}
|
||||||
isCompleted={isCompleted}
|
disableUserActions={disableUserActions}
|
||||||
user={user}
|
user={user}
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
|
viewProps={viewProps}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
@ -161,18 +154,18 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
|
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
|
||||||
onClick={addIssueToState}
|
onClick={addIssueToGroup}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
Add Issue
|
Add Issue
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
!isCompleted && (
|
!disableUserActions && (
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
customButton={
|
customButton={
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-2 font-medium text-custom-primary outline-none"
|
className="flex items-center gap-2 font-medium text-custom-primary outline-none whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
Add Issue
|
Add Issue
|
||||||
@ -181,7 +174,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
position="left"
|
position="left"
|
||||||
noBorder
|
noBorder
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem onClick={addIssueToState}>
|
<CustomMenu.MenuItem onClick={addIssueToGroup}>
|
||||||
Create new
|
Create new
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
{openIssuesListModal && (
|
{openIssuesListModal && (
|
@ -15,7 +15,6 @@ import {
|
|||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useIssuesView from "hooks/use-issues-view";
|
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// components
|
// components
|
||||||
@ -45,14 +44,7 @@ import { LayerDiagonalIcon } from "components/icons";
|
|||||||
import { handleIssuesMutation } from "constants/issue";
|
import { handleIssuesMutation } from "constants/issue";
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import {
|
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
|
||||||
ICurrentUserResponse,
|
|
||||||
IIssue,
|
|
||||||
ISubIssueResponse,
|
|
||||||
Properties,
|
|
||||||
TIssueGroupByOptions,
|
|
||||||
UserAuth,
|
|
||||||
} from "types";
|
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import {
|
import {
|
||||||
CYCLE_DETAILS,
|
CYCLE_DETAILS,
|
||||||
@ -61,6 +53,7 @@ import {
|
|||||||
MODULE_ISSUES_WITH_PARAMS,
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
SUB_ISSUES,
|
SUB_ISSUES,
|
||||||
|
USER_ISSUES,
|
||||||
VIEW_ISSUES,
|
VIEW_ISSUES,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
@ -69,18 +62,17 @@ type Props = {
|
|||||||
provided: DraggableProvided;
|
provided: DraggableProvided;
|
||||||
snapshot: DraggableStateSnapshot;
|
snapshot: DraggableStateSnapshot;
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
properties: Properties;
|
|
||||||
groupTitle?: string;
|
groupTitle?: string;
|
||||||
index: number;
|
index: number;
|
||||||
selectedGroup: TIssueGroupByOptions;
|
|
||||||
editIssue: () => void;
|
editIssue: () => void;
|
||||||
makeIssueCopy: () => void;
|
makeIssueCopy: () => void;
|
||||||
removeIssue?: (() => void) | null;
|
removeIssue?: (() => void) | null;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
isCompleted?: boolean;
|
disableUserActions: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
|
viewProps: IIssueViewProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleBoardIssue: React.FC<Props> = ({
|
export const SingleBoardIssue: React.FC<Props> = ({
|
||||||
@ -88,18 +80,17 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
provided,
|
provided,
|
||||||
snapshot,
|
snapshot,
|
||||||
issue,
|
issue,
|
||||||
properties,
|
|
||||||
index,
|
index,
|
||||||
selectedGroup,
|
|
||||||
editIssue,
|
editIssue,
|
||||||
makeIssueCopy,
|
makeIssueCopy,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
handleTrashBox,
|
handleTrashBox,
|
||||||
isCompleted = false,
|
disableUserActions,
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
|
viewProps,
|
||||||
}) => {
|
}) => {
|
||||||
// context menu
|
// context menu
|
||||||
const [contextMenu, setContextMenu] = useState(false);
|
const [contextMenu, setContextMenu] = useState(false);
|
||||||
@ -108,7 +99,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
|
|
||||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const { orderBy, params } = useIssuesView();
|
const { groupByProperty: selectedGroup, orderBy, params, properties } = viewProps;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
@ -117,7 +108,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
|
|
||||||
const partialUpdateIssue = useCallback(
|
const partialUpdateIssue = useCallback(
|
||||||
(formData: Partial<IIssue>, issue: IIssue) => {
|
(formData: Partial<IIssue>, issue: IIssue) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !issue) return;
|
||||||
|
|
||||||
const fetchKey = cycleId
|
const fetchKey = cycleId
|
||||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
|
||||||
@ -125,7 +116,9 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
||||||
: viewId
|
: viewId
|
||||||
? VIEW_ISSUES(viewId.toString(), params)
|
? VIEW_ISSUES(viewId.toString(), params)
|
||||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
|
: router.pathname.includes("my-issues")
|
||||||
|
? USER_ISSUES(workspaceSlug.toString(), params)
|
||||||
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(issue.project.toString(), params);
|
||||||
|
|
||||||
if (issue.parent) {
|
if (issue.parent) {
|
||||||
mutate<ISubIssueResponse>(
|
mutate<ISubIssueResponse>(
|
||||||
@ -170,7 +163,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
issuesService
|
issuesService
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user)
|
.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutate(fetchKey);
|
mutate(fetchKey);
|
||||||
|
|
||||||
@ -180,7 +173,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
projectId,
|
|
||||||
cycleId,
|
cycleId,
|
||||||
moduleId,
|
moduleId,
|
||||||
viewId,
|
viewId,
|
||||||
@ -189,6 +181,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
selectedGroup,
|
selectedGroup,
|
||||||
orderBy,
|
orderBy,
|
||||||
params,
|
params,
|
||||||
|
router,
|
||||||
user,
|
user,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@ -228,7 +221,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
|
|
||||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
@ -34,19 +34,17 @@ import {
|
|||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleEditIssue: (issue: IIssue) => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
|
||||||
addIssueToDate: (date: string) => void;
|
addIssueToDate: (date: string) => void;
|
||||||
isCompleted: boolean;
|
disableUserActions: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CalendarView: React.FC<Props> = ({
|
export const CalendarView: React.FC<Props> = ({
|
||||||
handleEditIssue,
|
handleIssueAction,
|
||||||
handleDeleteIssue,
|
|
||||||
addIssueToDate,
|
addIssueToDate,
|
||||||
isCompleted = false,
|
disableUserActions,
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
}) => {
|
}) => {
|
||||||
@ -167,7 +165,7 @@ export const CalendarView: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
}, [currentDate]);
|
}, [currentDate]);
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||||
|
|
||||||
return calendarIssues ? (
|
return calendarIssues ? (
|
||||||
<div className="h-full overflow-y-auto">
|
<div className="h-full overflow-y-auto">
|
||||||
@ -220,10 +218,10 @@ export const CalendarView: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{currentViewDaysData.map((date, index) => (
|
{currentViewDaysData.map((date, index) => (
|
||||||
<SingleCalendarDate
|
<SingleCalendarDate
|
||||||
|
key={`${date}-${index}`}
|
||||||
index={index}
|
index={index}
|
||||||
date={date}
|
date={date}
|
||||||
handleEditIssue={handleEditIssue}
|
handleIssueAction={handleIssueAction}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
addIssueToDate={addIssueToDate}
|
addIssueToDate={addIssueToDate}
|
||||||
isMonthlyView={isMonthlyView}
|
isMonthlyView={isMonthlyView}
|
||||||
showWeekEnds={showWeekEnds}
|
showWeekEnds={showWeekEnds}
|
@ -13,8 +13,7 @@ import { formatDate } from "helpers/calendar.helper";
|
|||||||
import { ICurrentUserResponse, IIssue } from "types";
|
import { ICurrentUserResponse, IIssue } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleEditIssue: (issue: IIssue) => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
|
||||||
index: number;
|
index: number;
|
||||||
date: {
|
date: {
|
||||||
date: string;
|
date: string;
|
||||||
@ -28,8 +27,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SingleCalendarDate: React.FC<Props> = ({
|
export const SingleCalendarDate: React.FC<Props> = ({
|
||||||
handleEditIssue,
|
handleIssueAction,
|
||||||
handleDeleteIssue,
|
|
||||||
date,
|
date,
|
||||||
index,
|
index,
|
||||||
addIssueToDate,
|
addIssueToDate,
|
||||||
@ -72,8 +70,8 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
|||||||
provided={provided}
|
provided={provided}
|
||||||
snapshot={snapshot}
|
snapshot={snapshot}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
handleEditIssue={handleEditIssue}
|
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||||
user={user}
|
user={user}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
/>
|
/>
|
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";
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
// react-beautiful-dnd
|
// react-beautiful-dnd
|
||||||
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
import { DropResult } from "react-beautiful-dnd";
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
import stateService from "services/state.service";
|
import stateService from "services/state.service";
|
||||||
import modulesService from "services/modules.service";
|
import modulesService from "services/modules.service";
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
// contexts
|
|
||||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useIssuesView from "hooks/use-issues-view";
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
|
import useIssuesProperties from "hooks/use-issue-properties";
|
||||||
|
import useProjectMembers from "hooks/use-project-members";
|
||||||
// components
|
// components
|
||||||
import {
|
import { FiltersList, AllViews } from "components/core";
|
||||||
AllLists,
|
|
||||||
AllBoards,
|
|
||||||
FilterList,
|
|
||||||
CalendarView,
|
|
||||||
GanttChartView,
|
|
||||||
SpreadsheetView,
|
|
||||||
} from "components/core";
|
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { CreateUpdateViewModal } from "components/views";
|
||||||
import { TransferIssues, TransferIssuesModal } from "components/cycles";
|
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState, PrimaryButton, Spinner, SecondaryButton } from "components/ui";
|
import { PrimaryButton } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
// images
|
|
||||||
import emptyIssue from "public/empty-state/issue.svg";
|
|
||||||
import emptyIssueArchive from "public/empty-state/issue-archive.svg";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { getStatesList } from "helpers/state.helper";
|
import { getStatesList } from "helpers/state.helper";
|
||||||
import { orderArrayBy } from "helpers/array.helper";
|
import { orderArrayBy } from "helpers/array.helper";
|
||||||
@ -49,19 +37,18 @@ import {
|
|||||||
MODULE_DETAILS,
|
MODULE_DETAILS,
|
||||||
MODULE_ISSUES_WITH_PARAMS,
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
PROJECT_ISSUE_LABELS,
|
||||||
STATES_LIST,
|
STATES_LIST,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type?: "issue" | "cycle" | "module";
|
|
||||||
openIssuesListModal?: () => void;
|
openIssuesListModal?: () => void;
|
||||||
isCompleted?: boolean;
|
disableUserActions?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssuesView: React.FC<Props> = ({
|
export const IssuesView: React.FC<Props> = ({
|
||||||
type = "issue",
|
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
isCompleted = false,
|
disableUserActions = false,
|
||||||
}) => {
|
}) => {
|
||||||
// create issue modal
|
// create issue modal
|
||||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
const [createIssueModal, setCreateIssueModal] = useState(false);
|
||||||
@ -83,14 +70,9 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
// trash box
|
// trash box
|
||||||
const [trashBox, setTrashBox] = useState(false);
|
const [trashBox, setTrashBox] = useState(false);
|
||||||
|
|
||||||
// transfer issue
|
|
||||||
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
|
||||||
const { memberRole } = useProjectMyMembership();
|
|
||||||
|
|
||||||
const { user } = useUserAuth();
|
const { user } = useUserAuth();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -104,7 +86,9 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
isEmpty,
|
isEmpty,
|
||||||
setFilters,
|
setFilters,
|
||||||
params,
|
params,
|
||||||
|
showEmptyGroups,
|
||||||
} = useIssuesView();
|
} = useIssuesView();
|
||||||
|
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||||
|
|
||||||
const { data: stateGroups } = useSWR(
|
const { data: stateGroups } = useSWR(
|
||||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||||
@ -114,6 +98,15 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
const states = getStatesList(stateGroups ?? {});
|
const states = getStatesList(stateGroups ?? {});
|
||||||
|
|
||||||
|
const { data: labels } = useSWR(
|
||||||
|
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString());
|
||||||
|
|
||||||
const handleDeleteIssue = useCallback(
|
const handleDeleteIssue = useCallback(
|
||||||
(issue: IIssue) => {
|
(issue: IIssue) => {
|
||||||
setDeleteIssueModal(true);
|
setDeleteIssueModal(true);
|
||||||
@ -123,7 +116,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleOnDragEnd = useCallback(
|
const handleOnDragEnd = useCallback(
|
||||||
(result: DropResult) => {
|
async (result: DropResult) => {
|
||||||
setTrashBox(false);
|
setTrashBox(false);
|
||||||
|
|
||||||
if (!result.destination || !workspaceSlug || !projectId || !groupedByIssues) return;
|
if (!result.destination || !workspaceSlug || !projectId || !groupedByIssues) return;
|
||||||
@ -281,7 +274,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const addIssueToState = useCallback(
|
const addIssueToGroup = useCallback(
|
||||||
(groupTitle: string) => {
|
(groupTitle: string) => {
|
||||||
setCreateIssueModal(true);
|
setCreateIssueModal(true);
|
||||||
|
|
||||||
@ -335,6 +328,15 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
[setEditIssueModal, setIssueToEdit]
|
[setEditIssueModal, setIssueToEdit]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleIssueAction = useCallback(
|
||||||
|
(issue: IIssue, action: "copy" | "edit" | "delete") => {
|
||||||
|
if (action === "copy") makeIssueCopy(issue);
|
||||||
|
else if (action === "edit") handleEditIssue(issue);
|
||||||
|
else if (action === "delete") handleDeleteIssue(issue);
|
||||||
|
},
|
||||||
|
[makeIssueCopy, handleEditIssue, handleDeleteIssue]
|
||||||
|
);
|
||||||
|
|
||||||
const removeIssueFromCycle = useCallback(
|
const removeIssueFromCycle = useCallback(
|
||||||
(bridgeId: string, issueId: string) => {
|
(bridgeId: string, issueId: string) => {
|
||||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||||
@ -421,13 +423,6 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
[workspaceSlug, projectId, moduleId, params, selectedGroup, setToastAlert]
|
[workspaceSlug, projectId, moduleId, params, selectedGroup, setToastAlert]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTrashBox = useCallback(
|
|
||||||
(isDragging: boolean) => {
|
|
||||||
if (isDragging && !trashBox) setTrashBox(true);
|
|
||||||
},
|
|
||||||
[trashBox, setTrashBox]
|
|
||||||
);
|
|
||||||
|
|
||||||
const nullFilters = Object.keys(filters).filter(
|
const nullFilters = Object.keys(filters).filter(
|
||||||
(key) => filters[key as keyof IIssueFilterOptions] === null
|
(key) => filters[key as keyof IIssueFilterOptions] === null
|
||||||
);
|
);
|
||||||
@ -461,14 +456,27 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
data={issueToDelete}
|
data={issueToDelete}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
<TransferIssuesModal
|
|
||||||
handleClose={() => setTransferIssuesModal(false)}
|
|
||||||
isOpen={transferIssuesModal}
|
|
||||||
/>
|
|
||||||
{areFiltersApplied && (
|
{areFiltersApplied && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
|
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
|
||||||
<FilterList filters={filters} setFilters={setFilters} />
|
<FiltersList
|
||||||
|
filters={filters}
|
||||||
|
setFilters={setFilters}
|
||||||
|
labels={labels}
|
||||||
|
members={members?.map((m) => m.member)}
|
||||||
|
states={states}
|
||||||
|
clearAllFilters={() =>
|
||||||
|
setFilters({
|
||||||
|
assignees: null,
|
||||||
|
created_by: null,
|
||||||
|
labels: null,
|
||||||
|
priority: null,
|
||||||
|
state: null,
|
||||||
|
target_date: null,
|
||||||
|
type: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (viewId) {
|
if (viewId) {
|
||||||
@ -492,140 +500,32 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
{<div className="mt-3 border-t border-custom-border-200" />}
|
{<div className="mt-3 border-t border-custom-border-200" />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<AllViews
|
||||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
addIssueToDate={addIssueToDate}
|
||||||
<StrictModeDroppable droppableId="trashBox">
|
addIssueToGroup={addIssueToGroup}
|
||||||
{(provided, snapshot) => (
|
disableUserActions={disableUserActions}
|
||||||
<div
|
dragDisabled={
|
||||||
className={`${
|
selectedGroup === "created_by" ||
|
||||||
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
selectedGroup === "labels" ||
|
||||||
} fixed top-4 left-1/2 -translate-x-1/2 z-40 w-72 flex items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-custom-background-100 px-3 py-5 text-xs font-medium italic text-red-500 ${
|
selectedGroup === "state_detail.group"
|
||||||
snapshot.isDraggingOver ? "bg-red-500 blur-2xl opacity-70" : ""
|
}
|
||||||
} transition duration-300`}
|
handleOnDragEnd={handleOnDragEnd}
|
||||||
ref={provided.innerRef}
|
handleIssueAction={handleIssueAction}
|
||||||
{...provided.droppableProps}
|
openIssuesListModal={openIssuesListModal ? openIssuesListModal : null}
|
||||||
>
|
removeIssue={cycleId ? removeIssueFromCycle : moduleId ? removeIssueFromModule : null}
|
||||||
<TrashIcon className="h-4 w-4" />
|
trashBox={trashBox}
|
||||||
Drop here to delete the issue.
|
setTrashBox={setTrashBox}
|
||||||
</div>
|
viewProps={{
|
||||||
)}
|
groupByProperty: selectedGroup,
|
||||||
</StrictModeDroppable>
|
groupedIssues: groupedByIssues,
|
||||||
{groupedByIssues ? (
|
isEmpty,
|
||||||
!isEmpty || issueView === "kanban" || issueView === "calendar" ? (
|
issueView,
|
||||||
<>
|
orderBy,
|
||||||
{isCompleted && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
|
params,
|
||||||
{issueView === "list" ? (
|
properties,
|
||||||
<AllLists
|
showEmptyGroups,
|
||||||
type={type}
|
}}
|
||||||
states={states}
|
/>
|
||||||
addIssueToState={addIssueToState}
|
|
||||||
makeIssueCopy={makeIssueCopy}
|
|
||||||
handleEditIssue={handleEditIssue}
|
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
|
||||||
removeIssue={
|
|
||||||
type === "cycle"
|
|
||||||
? removeIssueFromCycle
|
|
||||||
: type === "module"
|
|
||||||
? removeIssueFromModule
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
isCompleted={isCompleted}
|
|
||||||
user={user}
|
|
||||||
userAuth={memberRole}
|
|
||||||
/>
|
|
||||||
) : issueView === "kanban" ? (
|
|
||||||
<AllBoards
|
|
||||||
type={type}
|
|
||||||
states={states}
|
|
||||||
addIssueToState={addIssueToState}
|
|
||||||
makeIssueCopy={makeIssueCopy}
|
|
||||||
handleEditIssue={handleEditIssue}
|
|
||||||
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
handleTrashBox={handleTrashBox}
|
|
||||||
removeIssue={
|
|
||||||
type === "cycle"
|
|
||||||
? removeIssueFromCycle
|
|
||||||
: type === "module"
|
|
||||||
? removeIssueFromModule
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
isCompleted={isCompleted}
|
|
||||||
user={user}
|
|
||||||
userAuth={memberRole}
|
|
||||||
/>
|
|
||||||
) : issueView === "calendar" ? (
|
|
||||||
<CalendarView
|
|
||||||
handleEditIssue={handleEditIssue}
|
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
addIssueToDate={addIssueToDate}
|
|
||||||
isCompleted={isCompleted}
|
|
||||||
user={user}
|
|
||||||
userAuth={memberRole}
|
|
||||||
/>
|
|
||||||
) : issueView === "spreadsheet" ? (
|
|
||||||
<SpreadsheetView
|
|
||||||
type={type}
|
|
||||||
handleEditIssue={handleEditIssue}
|
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
|
||||||
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
|
|
||||||
isCompleted={isCompleted}
|
|
||||||
user={user}
|
|
||||||
userAuth={memberRole}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
issueView === "gantt_chart" && <GanttChartView />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : router.pathname.includes("archived-issues") ? (
|
|
||||||
<EmptyState
|
|
||||||
title="Archived Issues will be shown here"
|
|
||||||
description="All the issues that have been in the completed or canceled groups for the configured period of time can be viewed here."
|
|
||||||
image={emptyIssueArchive}
|
|
||||||
buttonText="Go to Automation Settings"
|
|
||||||
onClick={() => {
|
|
||||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<EmptyState
|
|
||||||
title={
|
|
||||||
cycleId
|
|
||||||
? "Cycle issues will appear here"
|
|
||||||
: moduleId
|
|
||||||
? "Module issues will appear here"
|
|
||||||
: "Project issues will appear here"
|
|
||||||
}
|
|
||||||
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
|
|
||||||
image={emptyIssue}
|
|
||||||
buttonText="New Issue"
|
|
||||||
buttonIcon={<PlusIcon className="h-4 w-4" />}
|
|
||||||
secondaryButton={
|
|
||||||
cycleId || moduleId ? (
|
|
||||||
<SecondaryButton
|
|
||||||
className="flex items-center gap-1.5"
|
|
||||||
onClick={openIssuesListModal}
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
Add an existing issue
|
|
||||||
</SecondaryButton>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", {
|
|
||||||
key: "c",
|
|
||||||
});
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DragDropContext>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
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,
|
ViewPrioritySelect,
|
||||||
ViewStateSelect,
|
ViewStateSelect,
|
||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
// hooks
|
|
||||||
import useIssueView from "hooks/use-issues-view";
|
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
|
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -37,7 +35,14 @@ import { LayerDiagonalIcon } from "components/icons";
|
|||||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||||
import { handleIssuesMutation } from "constants/issue";
|
import { handleIssuesMutation } from "constants/issue";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, ISubIssueResponse, Properties, UserAuth } from "types";
|
import {
|
||||||
|
ICurrentUserResponse,
|
||||||
|
IIssue,
|
||||||
|
IIssueViewProps,
|
||||||
|
ISubIssueResponse,
|
||||||
|
Properties,
|
||||||
|
UserAuth,
|
||||||
|
} from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import {
|
import {
|
||||||
CYCLE_DETAILS,
|
CYCLE_DETAILS,
|
||||||
@ -46,37 +51,38 @@ import {
|
|||||||
MODULE_ISSUES_WITH_PARAMS,
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
SUB_ISSUES,
|
SUB_ISSUES,
|
||||||
|
USER_ISSUES,
|
||||||
VIEW_ISSUES,
|
VIEW_ISSUES,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type?: string;
|
type?: string;
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
properties: Properties;
|
|
||||||
groupTitle?: string;
|
groupTitle?: string;
|
||||||
editIssue: () => void;
|
editIssue: () => void;
|
||||||
index: number;
|
index: number;
|
||||||
makeIssueCopy: () => void;
|
makeIssueCopy: () => void;
|
||||||
removeIssue?: (() => void) | null;
|
removeIssue?: (() => void) | null;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
isCompleted?: boolean;
|
disableUserActions: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
|
viewProps: IIssueViewProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleListIssue: React.FC<Props> = ({
|
export const SingleListIssue: React.FC<Props> = ({
|
||||||
type,
|
type,
|
||||||
issue,
|
issue,
|
||||||
properties,
|
|
||||||
editIssue,
|
editIssue,
|
||||||
index,
|
index,
|
||||||
makeIssueCopy,
|
makeIssueCopy,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
isCompleted = false,
|
disableUserActions,
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
|
viewProps,
|
||||||
}) => {
|
}) => {
|
||||||
// context menu
|
// context menu
|
||||||
const [contextMenu, setContextMenu] = useState(false);
|
const [contextMenu, setContextMenu] = useState(false);
|
||||||
@ -88,11 +94,11 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { groupByProperty: selectedGroup, orderBy, params } = useIssueView();
|
const { groupByProperty: selectedGroup, orderBy, params, properties } = viewProps;
|
||||||
|
|
||||||
const partialUpdateIssue = useCallback(
|
const partialUpdateIssue = useCallback(
|
||||||
(formData: Partial<IIssue>, issue: IIssue) => {
|
(formData: Partial<IIssue>, issue: IIssue) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !issue) return;
|
||||||
|
|
||||||
const fetchKey = cycleId
|
const fetchKey = cycleId
|
||||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
|
||||||
@ -100,7 +106,9 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
||||||
: viewId
|
: viewId
|
||||||
? VIEW_ISSUES(viewId.toString(), params)
|
? VIEW_ISSUES(viewId.toString(), params)
|
||||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
|
: router.pathname.includes("my-issues")
|
||||||
|
? USER_ISSUES(workspaceSlug.toString(), params)
|
||||||
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(issue.project.toString(), params);
|
||||||
|
|
||||||
if (issue.parent) {
|
if (issue.parent) {
|
||||||
mutate<ISubIssueResponse>(
|
mutate<ISubIssueResponse>(
|
||||||
@ -145,7 +153,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
issuesService
|
issuesService
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user)
|
.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutate(fetchKey);
|
mutate(fetchKey);
|
||||||
|
|
||||||
@ -155,7 +163,6 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
projectId,
|
|
||||||
cycleId,
|
cycleId,
|
||||||
moduleId,
|
moduleId,
|
||||||
viewId,
|
viewId,
|
||||||
@ -164,6 +171,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
selectedGroup,
|
selectedGroup,
|
||||||
orderBy,
|
orderBy,
|
||||||
params,
|
params,
|
||||||
|
router,
|
||||||
user,
|
user,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@ -186,7 +194,8 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
? `/${workspaceSlug}/projects/${projectId}/archived-issues/${issue.id}`
|
? `/${workspaceSlug}/projects/${projectId}/archived-issues/${issue.id}`
|
||||||
: `/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`;
|
: `/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`;
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted || isArchivedIssues;
|
const isNotAllowed =
|
||||||
|
userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
@ -8,7 +8,7 @@ import { Disclosure, Transition } from "@headlessui/react";
|
|||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useIssuesProperties from "hooks/use-issue-properties";
|
import useProjects from "hooks/use-projects";
|
||||||
// components
|
// components
|
||||||
import { SingleListIssue } from "components/core";
|
import { SingleListIssue } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
@ -18,58 +18,52 @@ import { PlusIcon } from "@heroicons/react/24/outline";
|
|||||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
// types
|
// types
|
||||||
import {
|
import {
|
||||||
ICurrentUserResponse,
|
ICurrentUserResponse,
|
||||||
IIssue,
|
IIssue,
|
||||||
IIssueLabels,
|
IIssueLabels,
|
||||||
|
IIssueViewProps,
|
||||||
IState,
|
IState,
|
||||||
TIssueGroupByOptions,
|
|
||||||
UserAuth,
|
UserAuth,
|
||||||
} from "types";
|
} from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type?: "issue" | "cycle" | "module";
|
|
||||||
currentState?: IState | null;
|
currentState?: IState | null;
|
||||||
groupTitle: string;
|
groupTitle: string;
|
||||||
groupedByIssues: {
|
addIssueToGroup: () => void;
|
||||||
[key: string]: IIssue[];
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
};
|
|
||||||
selectedGroup: TIssueGroupByOptions;
|
|
||||||
addIssueToState: () => void;
|
|
||||||
makeIssueCopy: (issue: IIssue) => void;
|
|
||||||
handleEditIssue: (issue: IIssue) => void;
|
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
isCompleted?: boolean;
|
disableUserActions: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
|
viewProps: IIssueViewProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleList: React.FC<Props> = ({
|
export const SingleList: React.FC<Props> = ({
|
||||||
type,
|
|
||||||
currentState,
|
currentState,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
groupedByIssues,
|
addIssueToGroup,
|
||||||
selectedGroup,
|
handleIssueAction,
|
||||||
addIssueToState,
|
|
||||||
makeIssueCopy,
|
|
||||||
handleEditIssue,
|
|
||||||
handleDeleteIssue,
|
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
isCompleted = false,
|
disableUserActions,
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
|
viewProps,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
|
||||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||||
|
|
||||||
|
const { groupByProperty: selectedGroup, groupedIssues } = viewProps;
|
||||||
|
|
||||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
||||||
@ -85,6 +79,8 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { projects } = useProjects();
|
||||||
|
|
||||||
const getGroupTitle = () => {
|
const getGroupTitle = () => {
|
||||||
let title = addSpaceIfCamelCase(groupTitle);
|
let title = addSpaceIfCamelCase(groupTitle);
|
||||||
|
|
||||||
@ -95,6 +91,9 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
case "labels":
|
case "labels":
|
||||||
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
||||||
break;
|
break;
|
||||||
|
case "project":
|
||||||
|
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||||
|
break;
|
||||||
case "created_by":
|
case "created_by":
|
||||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
||||||
title =
|
title =
|
||||||
@ -115,9 +114,22 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
icon =
|
icon =
|
||||||
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
|
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
|
||||||
break;
|
break;
|
||||||
|
case "state_detail.group":
|
||||||
|
icon = getStateGroupIcon(groupTitle as any, "16", "16");
|
||||||
|
break;
|
||||||
case "priority":
|
case "priority":
|
||||||
icon = getPriorityIcon(groupTitle, "text-lg");
|
icon = getPriorityIcon(groupTitle, "text-lg");
|
||||||
break;
|
break;
|
||||||
|
case "project":
|
||||||
|
const project = projects?.find((p) => p.id === groupTitle);
|
||||||
|
icon =
|
||||||
|
project &&
|
||||||
|
(project.emoji !== null
|
||||||
|
? renderEmoji(project.emoji)
|
||||||
|
: project.icon_prop !== null
|
||||||
|
? renderEmoji(project.icon_prop)
|
||||||
|
: null);
|
||||||
|
break;
|
||||||
case "labels":
|
case "labels":
|
||||||
const labelColor =
|
const labelColor =
|
||||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||||
@ -138,6 +150,8 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
return icon;
|
return icon;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!groupedIssues) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure as="div" defaultOpen>
|
<Disclosure as="div" defaultOpen>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
@ -156,7 +170,7 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
<h2 className="font-medium leading-5">All Issues</h2>
|
<h2 className="font-medium leading-5">All Issues</h2>
|
||||||
)}
|
)}
|
||||||
<span className="text-custom-text-200 min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs">
|
<span className="text-custom-text-200 min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs">
|
||||||
{groupedByIssues[groupTitle as keyof IIssue].length}
|
{groupedIssues[groupTitle as keyof IIssue].length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
@ -166,11 +180,11 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
||||||
onClick={addIssueToState}
|
onClick={addIssueToGroup}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
) : isCompleted ? (
|
) : disableUserActions ? (
|
||||||
""
|
""
|
||||||
) : (
|
) : (
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
@ -182,7 +196,7 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
position="right"
|
position="right"
|
||||||
noBorder
|
noBorder
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem onClick={addIssueToState}>Create new</CustomMenu.MenuItem>
|
<CustomMenu.MenuItem onClick={addIssueToGroup}>Create new</CustomMenu.MenuItem>
|
||||||
{openIssuesListModal && (
|
{openIssuesListModal && (
|
||||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||||
Add an existing issue
|
Add an existing issue
|
||||||
@ -201,26 +215,26 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
leaveTo="transform opacity-0"
|
leaveTo="transform opacity-0"
|
||||||
>
|
>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
{groupedByIssues[groupTitle] ? (
|
{groupedIssues[groupTitle] ? (
|
||||||
groupedByIssues[groupTitle].length > 0 ? (
|
groupedIssues[groupTitle].length > 0 ? (
|
||||||
groupedByIssues[groupTitle].map((issue, index) => (
|
groupedIssues[groupTitle].map((issue, index) => (
|
||||||
<SingleListIssue
|
<SingleListIssue
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
type={type}
|
type={type}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
properties={properties}
|
|
||||||
groupTitle={groupTitle}
|
groupTitle={groupTitle}
|
||||||
index={index}
|
index={index}
|
||||||
editIssue={() => handleEditIssue(issue)}
|
editIssue={() => handleIssueAction(issue, "edit")}
|
||||||
makeIssueCopy={() => makeIssueCopy(issue)}
|
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||||
removeIssue={() => {
|
removeIssue={() => {
|
||||||
if (removeIssue !== null && issue.bridge_id)
|
if (removeIssue !== null && issue.bridge_id)
|
||||||
removeIssue(issue.bridge_id, issue.id);
|
removeIssue(issue.bridge_id, issue.id);
|
||||||
}}
|
}}
|
||||||
isCompleted={isCompleted}
|
disableUserActions={disableUserActions}
|
||||||
user={user}
|
user={user}
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
|
viewProps={viewProps}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
@ -53,7 +53,7 @@ type Props = {
|
|||||||
handleEditIssue: (issue: IIssue) => void;
|
handleEditIssue: (issue: IIssue) => void;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
gridTemplateColumns: string;
|
gridTemplateColumns: string;
|
||||||
isCompleted?: boolean;
|
disableUserActions: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
nestingLevel: number;
|
nestingLevel: number;
|
||||||
@ -68,7 +68,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
handleEditIssue,
|
handleEditIssue,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
gridTemplateColumns,
|
gridTemplateColumns,
|
||||||
isCompleted = false,
|
disableUserActions,
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
nestingLevel,
|
nestingLevel,
|
||||||
@ -190,7 +190,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!isNotAllowed && !isCompleted && (
|
{!isNotAllowed && !disableUserActions && (
|
||||||
<div className="absolute top-0 left-2.5 opacity-0 group-hover:opacity-100">
|
<div className="absolute top-0 left-2.5 opacity-0 group-hover:opacity-100">
|
||||||
<Popover2
|
<Popover2
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
@ -8,32 +8,28 @@ import useSubIssue from "hooks/use-sub-issue";
|
|||||||
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
key: string;
|
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
index: number;
|
index: number;
|
||||||
expandedIssues: string[];
|
expandedIssues: string[];
|
||||||
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
properties: Properties;
|
properties: Properties;
|
||||||
handleEditIssue: (issue: IIssue) => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
|
||||||
gridTemplateColumns: string;
|
gridTemplateColumns: string;
|
||||||
isCompleted?: boolean;
|
disableUserActions: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
nestingLevel?: number;
|
nestingLevel?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetIssues: React.FC<Props> = ({
|
export const SpreadsheetIssues: React.FC<Props> = ({
|
||||||
key,
|
|
||||||
index,
|
index,
|
||||||
issue,
|
issue,
|
||||||
expandedIssues,
|
expandedIssues,
|
||||||
setExpandedIssues,
|
setExpandedIssues,
|
||||||
gridTemplateColumns,
|
gridTemplateColumns,
|
||||||
properties,
|
properties,
|
||||||
handleEditIssue,
|
handleIssueAction,
|
||||||
handleDeleteIssue,
|
disableUserActions,
|
||||||
isCompleted = false,
|
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
nestingLevel = 0,
|
nestingLevel = 0,
|
||||||
@ -64,9 +60,9 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
|||||||
handleToggleExpand={handleToggleExpand}
|
handleToggleExpand={handleToggleExpand}
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
gridTemplateColumns={gridTemplateColumns}
|
||||||
properties={properties}
|
properties={properties}
|
||||||
handleEditIssue={handleEditIssue}
|
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||||
isCompleted={isCompleted}
|
disableUserActions={disableUserActions}
|
||||||
user={user}
|
user={user}
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
nestingLevel={nestingLevel}
|
nestingLevel={nestingLevel}
|
||||||
@ -76,7 +72,7 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
|||||||
!isLoading &&
|
!isLoading &&
|
||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue: IIssue, subIndex: number) => (
|
subIssues.map((subIssue: IIssue) => (
|
||||||
<SpreadsheetIssues
|
<SpreadsheetIssues
|
||||||
key={subIssue.id}
|
key={subIssue.id}
|
||||||
issue={subIssue}
|
issue={subIssue}
|
||||||
@ -85,9 +81,8 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
|||||||
setExpandedIssues={setExpandedIssues}
|
setExpandedIssues={setExpandedIssues}
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
gridTemplateColumns={gridTemplateColumns}
|
||||||
properties={properties}
|
properties={properties}
|
||||||
handleEditIssue={handleEditIssue}
|
handleIssueAction={handleIssueAction}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
disableUserActions={disableUserActions}
|
||||||
isCompleted={isCompleted}
|
|
||||||
user={user}
|
user={user}
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
nestingLevel={nestingLevel + 1}
|
nestingLevel={nestingLevel + 1}
|
@ -17,28 +17,26 @@ import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
|
|||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type: "issue" | "cycle" | "module";
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
handleEditIssue: (issue: IIssue) => void;
|
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
isCompleted?: boolean;
|
disableUserActions: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetView: React.FC<Props> = ({
|
export const SpreadsheetView: React.FC<Props> = ({
|
||||||
type,
|
handleIssueAction,
|
||||||
handleEditIssue,
|
|
||||||
handleDeleteIssue,
|
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
isCompleted = false,
|
disableUserActions,
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
}) => {
|
}) => {
|
||||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
|
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||||
|
|
||||||
const { spreadsheetIssues } = useSpreadsheetIssuesView();
|
const { spreadsheetIssues } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
@ -76,9 +74,8 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
setExpandedIssues={setExpandedIssues}
|
setExpandedIssues={setExpandedIssues}
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
gridTemplateColumns={gridTemplateColumns}
|
||||||
properties={properties}
|
properties={properties}
|
||||||
handleEditIssue={handleEditIssue}
|
handleIssueAction={handleIssueAction}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
disableUserActions={disableUserActions}
|
||||||
isCompleted={isCompleted}
|
|
||||||
user={user}
|
user={user}
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
/>
|
/>
|
||||||
@ -99,7 +96,7 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
Add Issue
|
Add Issue
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
!isCompleted && (
|
!disableUserActions && (
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
className="sticky left-0 z-[1]"
|
className="sticky left-0 z-[1]"
|
||||||
customButton={
|
customButton={
|
@ -5,6 +5,8 @@ import {
|
|||||||
StartedStateIcon,
|
StartedStateIcon,
|
||||||
UnstartedStateIcon,
|
UnstartedStateIcon,
|
||||||
} from "components/icons";
|
} from "components/icons";
|
||||||
|
// constants
|
||||||
|
import { STATE_GROUP_COLORS } from "constants/state";
|
||||||
|
|
||||||
export const getStateGroupIcon = (
|
export const getStateGroupIcon = (
|
||||||
stateGroup: "backlog" | "unstarted" | "started" | "completed" | "cancelled",
|
stateGroup: "backlog" | "unstarted" | "started" | "completed" | "cancelled",
|
||||||
@ -14,15 +16,45 @@ export const getStateGroupIcon = (
|
|||||||
) => {
|
) => {
|
||||||
switch (stateGroup) {
|
switch (stateGroup) {
|
||||||
case "backlog":
|
case "backlog":
|
||||||
return <BacklogStateIcon width={width} height={height} color={color} />;
|
return (
|
||||||
|
<BacklogStateIcon
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
color={color ?? STATE_GROUP_COLORS["backlog"]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case "unstarted":
|
case "unstarted":
|
||||||
return <UnstartedStateIcon width={width} height={height} color={color} />;
|
return (
|
||||||
|
<UnstartedStateIcon
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
color={color ?? STATE_GROUP_COLORS["unstarted"]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case "started":
|
case "started":
|
||||||
return <StartedStateIcon width={width} height={height} color={color} />;
|
return (
|
||||||
|
<StartedStateIcon
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
color={color ?? STATE_GROUP_COLORS["started"]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case "completed":
|
case "completed":
|
||||||
return <CompletedStateIcon width={width} height={height} color={color} />;
|
return (
|
||||||
|
<CompletedStateIcon
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
color={color ?? STATE_GROUP_COLORS["completed"]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case "cancelled":
|
case "cancelled":
|
||||||
return <CancelledStateIcon width={width} height={height} color={color} />;
|
return (
|
||||||
|
<CancelledStateIcon
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
color={color ?? STATE_GROUP_COLORS["cancelled"]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
@ -37,37 +37,35 @@ export const FiltersDropdown: React.FC = () => {
|
|||||||
id: "priority",
|
id: "priority",
|
||||||
label: "Priority",
|
label: "Priority",
|
||||||
value: PRIORITIES,
|
value: PRIORITIES,
|
||||||
children: [
|
hasChildren: true,
|
||||||
...PRIORITIES.map((priority) => ({
|
children: PRIORITIES.map((priority) => ({
|
||||||
id: priority === null ? "null" : priority,
|
id: priority === null ? "null" : priority,
|
||||||
label: (
|
label: (
|
||||||
<div className="flex items-center gap-2 capitalize">
|
<div className="flex items-center gap-2 capitalize">
|
||||||
{getPriorityIcon(priority)} {priority ?? "None"}
|
{getPriorityIcon(priority)} {priority ?? "None"}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
value: {
|
value: {
|
||||||
key: "priority",
|
key: "priority",
|
||||||
value: priority === null ? "null" : priority,
|
value: priority === null ? "null" : priority,
|
||||||
},
|
},
|
||||||
selected: filters?.priority?.includes(priority === null ? "null" : priority),
|
selected: filters?.priority?.includes(priority === null ? "null" : priority),
|
||||||
})),
|
})),
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "inbox_status",
|
id: "inbox_status",
|
||||||
label: "Status",
|
label: "Status",
|
||||||
value: INBOX_STATUS.map((status) => status.value),
|
value: INBOX_STATUS.map((status) => status.value),
|
||||||
children: [
|
hasChildren: true,
|
||||||
...INBOX_STATUS.map((status) => ({
|
children: INBOX_STATUS.map((status) => ({
|
||||||
id: status.key,
|
id: status.key,
|
||||||
label: status.label,
|
label: status.label,
|
||||||
value: {
|
value: {
|
||||||
key: "inbox_status",
|
key: "inbox_status",
|
||||||
value: status.value,
|
value: status.value,
|
||||||
},
|
},
|
||||||
selected: filters?.inbox_status?.includes(status.value),
|
selected: filters?.inbox_status?.includes(status.value),
|
||||||
})),
|
})),
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
@ -63,7 +63,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
|
|||||||
if (!workspaceSlug || !projectId || !data) return;
|
if (!workspaceSlug || !projectId || !data) return;
|
||||||
|
|
||||||
await issueServices
|
await issueServices
|
||||||
.deleteIssue(workspaceSlug as string, projectId as string, data.id, user)
|
.deleteIssue(workspaceSlug as string, data.project, data.id, user)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (issueView === "calendar") {
|
if (issueView === "calendar") {
|
||||||
const calendarFetchKey = cycleId
|
const calendarFetchKey = cycleId
|
||||||
@ -72,7 +72,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
|
|||||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
|
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
|
||||||
: viewId
|
: viewId
|
||||||
? VIEW_ISSUES(viewId.toString(), calendarParams)
|
? VIEW_ISSUES(viewId.toString(), calendarParams)
|
||||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), calendarParams);
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, calendarParams);
|
||||||
|
|
||||||
mutate<IIssue[]>(
|
mutate<IIssue[]>(
|
||||||
calendarFetchKey,
|
calendarFetchKey,
|
||||||
@ -86,7 +86,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
|
|||||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams)
|
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams)
|
||||||
: viewId
|
: viewId
|
||||||
? VIEW_ISSUES(viewId.toString(), spreadsheetParams)
|
? VIEW_ISSUES(viewId.toString(), spreadsheetParams)
|
||||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", spreadsheetParams);
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, spreadsheetParams);
|
||||||
if (data.parent) {
|
if (data.parent) {
|
||||||
mutate<ISubIssueResponse>(
|
mutate<ISubIssueResponse>(
|
||||||
SUB_ISSUES(data.parent.toString()),
|
SUB_ISSUES(data.parent.toString()),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export * from "./attachment";
|
export * from "./attachment";
|
||||||
export * from "./comment";
|
export * from "./comment";
|
||||||
|
export * from "./my-issues";
|
||||||
export * from "./sidebar-select";
|
export * from "./sidebar-select";
|
||||||
export * from "./view-select";
|
export * from "./view-select";
|
||||||
export * from "./activity";
|
export * from "./activity";
|
||||||
@ -9,7 +10,6 @@ export * from "./form";
|
|||||||
export * from "./gantt-chart";
|
export * from "./gantt-chart";
|
||||||
export * from "./main-content";
|
export * from "./main-content";
|
||||||
export * from "./modal";
|
export * from "./modal";
|
||||||
export * from "./my-issues-list-item";
|
|
||||||
export * from "./parent-issues-list-modal";
|
export * from "./parent-issues-list-modal";
|
||||||
export * from "./sidebar";
|
export * from "./sidebar";
|
||||||
export * from "./sub-issues-list";
|
export * from "./sub-issues-list";
|
||||||
|
@ -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 { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -39,12 +41,14 @@ export const ViewStateSelect: React.FC<Props> = ({
|
|||||||
user,
|
user,
|
||||||
isNotAllowed,
|
isNotAllowed,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [fetchStates, setFetchStates] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { data: stateGroups } = useSWR(
|
const { data: stateGroups } = useSWR(
|
||||||
workspaceSlug && issue ? STATES_LIST(issue.project) : null,
|
workspaceSlug && issue && fetchStates ? STATES_LIST(issue.project) : null,
|
||||||
workspaceSlug && issue
|
workspaceSlug && issue && fetchStates
|
||||||
? () => stateService.getStates(workspaceSlug as string, issue.project)
|
? () => stateService.getStates(workspaceSlug as string, issue.project)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
@ -61,7 +65,7 @@ export const ViewStateSelect: React.FC<Props> = ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const selectedOption = states?.find((s) => s.id === issue.state);
|
const selectedOption = issue.state_detail;
|
||||||
|
|
||||||
const stateLabel = (
|
const stateLabel = (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -126,6 +130,7 @@ export const ViewStateSelect: React.FC<Props> = ({
|
|||||||
{...(customButton ? { customButton: stateLabel } : { label: stateLabel })}
|
{...(customButton ? { customButton: stateLabel } : { label: stateLabel })}
|
||||||
position={position}
|
position={position}
|
||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed}
|
||||||
|
onOpen={() => setFetchStates(true)}
|
||||||
noChevron
|
noChevron
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -244,20 +244,7 @@ export const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen, user })
|
|||||||
<EmojiIconPicker
|
<EmojiIconPicker
|
||||||
label={
|
label={
|
||||||
<div className="h-[44px] w-[44px] grid place-items-center rounded-md bg-custom-background-80 outline-none text-lg">
|
<div className="h-[44px] w-[44px] grid place-items-center rounded-md bg-custom-background-80 outline-none text-lg">
|
||||||
{value ? (
|
{value ? renderEmoji(value) : "Icon"}
|
||||||
typeof value === "object" ? (
|
|
||||||
<span
|
|
||||||
style={{ color: value.color }}
|
|
||||||
className="material-symbols-rounded text-lg"
|
|
||||||
>
|
|
||||||
{value.name}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
renderEmoji(value)
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
"Icon"
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
@ -176,12 +176,7 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({
|
|||||||
{renderEmoji(project.emoji)}
|
{renderEmoji(project.emoji)}
|
||||||
</span>
|
</span>
|
||||||
) : project.icon_prop ? (
|
) : project.icon_prop ? (
|
||||||
<span
|
renderEmoji(project.icon_prop)
|
||||||
style={{ color: project.icon_prop.color }}
|
|
||||||
className="material-symbols-rounded text-lg"
|
|
||||||
>
|
|
||||||
{project.icon_prop.name}
|
|
||||||
</span>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-3.5 mb-7 break-words">
|
<p className="mt-3.5 mb-7 break-words">
|
||||||
|
@ -150,12 +150,7 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
|||||||
</span>
|
</span>
|
||||||
) : project.icon_prop ? (
|
) : project.icon_prop ? (
|
||||||
<div className="h-7 w-7 grid place-items-center">
|
<div className="h-7 w-7 grid place-items-center">
|
||||||
<span
|
{renderEmoji(project.icon_prop)}
|
||||||
style={{ color: project.icon_prop.color }}
|
|
||||||
className="material-symbols-rounded text-lg"
|
|
||||||
>
|
|
||||||
{project.icon_prop.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||||
|
@ -39,6 +39,7 @@ export const CustomSearchSelect = ({
|
|||||||
noChevron = false,
|
noChevron = false,
|
||||||
onChange,
|
onChange,
|
||||||
options,
|
options,
|
||||||
|
onOpen,
|
||||||
optionsClassName = "",
|
optionsClassName = "",
|
||||||
position = "left",
|
position = "left",
|
||||||
selfPositioned = false,
|
selfPositioned = false,
|
||||||
@ -67,115 +68,119 @@ export const CustomSearchSelect = ({
|
|||||||
className={`${selfPositioned ? "" : "relative"} flex-shrink-0 text-left ${className}`}
|
className={`${selfPositioned ? "" : "relative"} flex-shrink-0 text-left ${className}`}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{({ open }: any) => (
|
{({ open }: any) => {
|
||||||
<>
|
if (open && onOpen) onOpen();
|
||||||
{customButton ? (
|
|
||||||
<Combobox.Button as="div">{customButton}</Combobox.Button>
|
return (
|
||||||
) : (
|
<>
|
||||||
<Combobox.Button
|
{customButton ? (
|
||||||
type="button"
|
<Combobox.Button as="div">{customButton}</Combobox.Button>
|
||||||
className={`flex items-center justify-between gap-1 w-full rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none ${
|
) : (
|
||||||
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
|
<Combobox.Button
|
||||||
} ${
|
type="button"
|
||||||
disabled
|
className={`flex items-center justify-between gap-1 w-full rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none ${
|
||||||
? "cursor-not-allowed text-custom-text-200"
|
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
|
||||||
: "cursor-pointer hover:bg-custom-background-80"
|
} ${
|
||||||
} ${buttonClassName}`}
|
disabled
|
||||||
>
|
? "cursor-not-allowed text-custom-text-200"
|
||||||
{label}
|
: "cursor-pointer hover:bg-custom-background-80"
|
||||||
{!noChevron && !disabled && (
|
} ${buttonClassName}`}
|
||||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
</Combobox.Button>
|
|
||||||
)}
|
|
||||||
<Transition
|
|
||||||
show={open}
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="transition ease-out duration-200"
|
|
||||||
enterFrom="opacity-0 translate-y-1"
|
|
||||||
enterTo="opacity-100 translate-y-0"
|
|
||||||
leave="transition ease-in duration-150"
|
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
|
||||||
leaveTo="opacity-0 translate-y-1"
|
|
||||||
>
|
|
||||||
<Combobox.Options
|
|
||||||
className={`absolute z-10 min-w-[10rem] border border-custom-border-300 p-2 rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
|
|
||||||
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
|
||||||
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
|
|
||||||
width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width
|
|
||||||
} ${optionsClassName}`}
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center justify-start rounded-sm border-[0.6px] border-custom-border-200 bg-custom-background-90 px-2">
|
|
||||||
<MagnifyingGlassIcon className="h-3 w-3 text-custom-text-200" />
|
|
||||||
<Combobox.Input
|
|
||||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
|
||||||
value={query}
|
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
|
||||||
placeholder="Type to search..."
|
|
||||||
displayValue={(assigned: any) => assigned?.name}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`mt-2 space-y-1 ${
|
|
||||||
maxHeight === "lg"
|
|
||||||
? "max-h-60"
|
|
||||||
: maxHeight === "md"
|
|
||||||
? "max-h-48"
|
|
||||||
: maxHeight === "rg"
|
|
||||||
? "max-h-36"
|
|
||||||
: maxHeight === "sm"
|
|
||||||
? "max-h-28"
|
|
||||||
: ""
|
|
||||||
} overflow-y-scroll`}
|
|
||||||
>
|
>
|
||||||
{filteredOptions ? (
|
{label}
|
||||||
filteredOptions.length > 0 ? (
|
{!noChevron && !disabled && (
|
||||||
filteredOptions.map((option) => (
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
<Combobox.Option
|
)}
|
||||||
key={option.value}
|
</Combobox.Button>
|
||||||
value={option.value}
|
)}
|
||||||
className={({ active, selected }) =>
|
<Transition
|
||||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
show={open}
|
||||||
active || selected ? "bg-custom-background-80" : ""
|
as={React.Fragment}
|
||||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
enter="transition ease-out duration-200"
|
||||||
}
|
enterFrom="opacity-0 translate-y-1"
|
||||||
>
|
enterTo="opacity-100 translate-y-0"
|
||||||
{({ active, selected }) => (
|
leave="transition ease-in duration-150"
|
||||||
<>
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
{option.content}
|
leaveTo="opacity-0 translate-y-1"
|
||||||
{multiple ? (
|
>
|
||||||
<div
|
<Combobox.Options
|
||||||
className={`flex items-center justify-center rounded border border-custom-border-400 p-0.5 ${
|
className={`absolute z-10 min-w-[10rem] border border-custom-border-300 p-2 rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
|
||||||
active || selected ? "opacity-100" : "opacity-0"
|
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
|
||||||
}`}
|
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
|
||||||
>
|
width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width
|
||||||
|
} ${optionsClassName}`}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-start rounded-sm border-[0.6px] border-custom-border-200 bg-custom-background-90 px-2">
|
||||||
|
<MagnifyingGlassIcon className="h-3 w-3 text-custom-text-200" />
|
||||||
|
<Combobox.Input
|
||||||
|
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Type to search..."
|
||||||
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`mt-2 space-y-1 ${
|
||||||
|
maxHeight === "lg"
|
||||||
|
? "max-h-60"
|
||||||
|
: maxHeight === "md"
|
||||||
|
? "max-h-48"
|
||||||
|
: maxHeight === "rg"
|
||||||
|
? "max-h-36"
|
||||||
|
: maxHeight === "sm"
|
||||||
|
? "max-h-28"
|
||||||
|
: ""
|
||||||
|
} overflow-y-scroll`}
|
||||||
|
>
|
||||||
|
{filteredOptions ? (
|
||||||
|
filteredOptions.length > 0 ? (
|
||||||
|
filteredOptions.map((option) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
className={({ active, selected }) =>
|
||||||
|
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||||
|
active || selected ? "bg-custom-background-80" : ""
|
||||||
|
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ active, selected }) => (
|
||||||
|
<>
|
||||||
|
{option.content}
|
||||||
|
{multiple ? (
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-center rounded border border-custom-border-400 p-0.5 ${
|
||||||
|
active || selected ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
|
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
) : (
|
</>
|
||||||
<CheckIcon
|
)}
|
||||||
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
|
</Combobox.Option>
|
||||||
/>
|
))
|
||||||
)}
|
) : (
|
||||||
</>
|
<span className="flex items-center gap-2 p-1">
|
||||||
)}
|
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||||
</Combobox.Option>
|
</span>
|
||||||
))
|
)
|
||||||
) : (
|
) : (
|
||||||
<span className="flex items-center gap-2 p-1">
|
<p className="text-center text-custom-text-200">Loading...</p>
|
||||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
)}
|
||||||
</span>
|
</div>
|
||||||
)
|
{footerOption}
|
||||||
) : (
|
</Combobox.Options>
|
||||||
<p className="text-center text-custom-text-200">Loading...</p>
|
</Transition>
|
||||||
)}
|
</>
|
||||||
</div>
|
);
|
||||||
{footerOption}
|
}}
|
||||||
</Combobox.Options>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Combobox>
|
</Combobox>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
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;
|
label?: string | JSX.Element;
|
||||||
maxHeight?: "sm" | "rg" | "md" | "lg";
|
maxHeight?: "sm" | "rg" | "md" | "lg";
|
||||||
noChevron?: boolean;
|
noChevron?: boolean;
|
||||||
|
onOpen?: () => void;
|
||||||
optionsClassName?: string;
|
optionsClassName?: string;
|
||||||
position?: "right" | "left";
|
position?: "right" | "left";
|
||||||
selfPositioned?: boolean;
|
selfPositioned?: boolean;
|
||||||
|
@ -2,6 +2,8 @@ import { Fragment, useState } from "react";
|
|||||||
|
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { Loader } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||||
import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/20/solid";
|
import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/20/solid";
|
||||||
@ -10,9 +12,6 @@ type MultiLevelDropdownProps = {
|
|||||||
label: string;
|
label: string;
|
||||||
options: {
|
options: {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
|
||||||
value: any;
|
|
||||||
selected?: boolean;
|
|
||||||
children?: {
|
children?: {
|
||||||
id: string;
|
id: string;
|
||||||
label: string | JSX.Element;
|
label: string | JSX.Element;
|
||||||
@ -20,6 +19,11 @@ type MultiLevelDropdownProps = {
|
|||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
element?: JSX.Element;
|
element?: JSX.Element;
|
||||||
}[];
|
}[];
|
||||||
|
hasChildren: boolean;
|
||||||
|
label: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
selected?: boolean;
|
||||||
|
value: any;
|
||||||
}[];
|
}[];
|
||||||
onSelect: (value: any) => void;
|
onSelect: (value: any) => void;
|
||||||
direction?: "left" | "right";
|
direction?: "left" | "right";
|
||||||
@ -69,15 +73,15 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
as="button"
|
as="button"
|
||||||
onClick={(e: any) => {
|
onClick={(e: any) => {
|
||||||
if (option.children) {
|
if (option.hasChildren) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (option.onClick) option.onClick();
|
||||||
|
|
||||||
if (openChildFor === option.id) setOpenChildFor(null);
|
if (openChildFor === option.id) setOpenChildFor(null);
|
||||||
else setOpenChildFor(option.id);
|
else setOpenChildFor(option.id);
|
||||||
} else {
|
} else onSelect(option.value);
|
||||||
onSelect(option.value);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
@ -90,18 +94,18 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
|||||||
direction === "right" ? "justify-between" : ""
|
direction === "right" ? "justify-between" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{direction === "left" && option.children && (
|
{direction === "left" && option.hasChildren && (
|
||||||
<ChevronLeftIcon className="h-4 w-4" aria-hidden="true" />
|
<ChevronLeftIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
)}
|
)}
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
{direction === "right" && option.children && (
|
{direction === "right" && option.hasChildren && (
|
||||||
<ChevronRightIcon className="h-4 w-4" aria-hidden="true" />
|
<ChevronRightIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
{option.children && option.id === openChildFor && (
|
{option.hasChildren && option.id === openChildFor && (
|
||||||
<div
|
<div
|
||||||
className={`absolute top-0 w-36 origin-top-right select-none overflow-y-scroll rounded-md bg-custom-background-90 shadow-lg focus:outline-none ${
|
className={`absolute top-0 w-36 origin-top-right select-none overflow-y-scroll rounded-md bg-custom-background-90 shadow-lg focus:outline-none ${
|
||||||
direction === "left"
|
direction === "left"
|
||||||
@ -119,29 +123,38 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
|||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="space-y-1 p-1">
|
{option.children ? (
|
||||||
{option.children.map((child) => {
|
<div className="space-y-1 p-1">
|
||||||
if (child.element) return child.element;
|
{option.children.map((child) => {
|
||||||
else
|
if (child.element) return child.element;
|
||||||
return (
|
else
|
||||||
<button
|
return (
|
||||||
key={child.id}
|
<button
|
||||||
type="button"
|
key={child.id}
|
||||||
onClick={() => onSelect(child.value)}
|
type="button"
|
||||||
className={`${
|
onClick={() => onSelect(child.value)}
|
||||||
child.selected ? "bg-custom-background-80" : ""
|
className={`${
|
||||||
} flex w-full items-center justify-between break-words rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80`}
|
child.selected ? "bg-custom-background-80" : ""
|
||||||
>
|
} flex w-full items-center justify-between break-words rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80`}
|
||||||
{child.label}{" "}
|
>
|
||||||
<CheckIcon
|
{child.label}{" "}
|
||||||
className={`h-3.5 w-3.5 opacity-0 ${
|
<CheckIcon
|
||||||
child.selected ? "opacity-100" : ""
|
className={`h-3.5 w-3.5 opacity-0 ${
|
||||||
}`}
|
child.selected ? "opacity-100" : ""
|
||||||
/>
|
}`}
|
||||||
</button>
|
/>
|
||||||
);
|
</button>
|
||||||
})}
|
);
|
||||||
</div>
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Loader className="p-1 space-y-2">
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,14 +1,28 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// react-hook-form
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
// services
|
||||||
|
import stateService from "services/state.service";
|
||||||
|
// hooks
|
||||||
|
import useProjectMembers from "hooks/use-project-members";
|
||||||
|
// components
|
||||||
|
import { FiltersList } from "components/core";
|
||||||
|
import { SelectFilters } from "components/views";
|
||||||
// ui
|
// ui
|
||||||
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
||||||
// components
|
// helpers
|
||||||
import { FilterList } from "components/core";
|
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||||
|
import { getStatesList } from "helpers/state.helper";
|
||||||
// types
|
// types
|
||||||
import { IView } from "types";
|
import { IQuery, IView } from "types";
|
||||||
// components
|
import issuesService from "services/issues.service";
|
||||||
import { SelectFilters } from "components/views";
|
// fetch-keys
|
||||||
|
import { PROJECT_ISSUE_LABELS, STATES_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleFormSubmit: (values: IView) => Promise<void>;
|
handleFormSubmit: (values: IView) => Promise<void>;
|
||||||
@ -30,6 +44,9 @@ export const ViewForm: React.FC<Props> = ({
|
|||||||
data,
|
data,
|
||||||
preLoadedData,
|
preLoadedData,
|
||||||
}) => {
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
@ -42,6 +59,26 @@ export const ViewForm: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
const filters = watch("query");
|
const filters = watch("query");
|
||||||
|
|
||||||
|
const { data: stateGroups } = useSWR(
|
||||||
|
workspaceSlug && projectId && (filters.state ?? []).length > 0
|
||||||
|
? STATES_LIST(projectId as string)
|
||||||
|
: null,
|
||||||
|
workspaceSlug && (filters.state ?? []).length > 0
|
||||||
|
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
const states = getStatesList(stateGroups ?? {});
|
||||||
|
|
||||||
|
const { data: labels } = useSWR(
|
||||||
|
workspaceSlug && projectId && (filters.labels ?? []).length > 0
|
||||||
|
? PROJECT_ISSUE_LABELS(projectId.toString())
|
||||||
|
: null,
|
||||||
|
workspaceSlug && projectId && (filters.labels ?? []).length > 0
|
||||||
|
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString());
|
||||||
|
|
||||||
const handleCreateUpdateView = async (formData: IView) => {
|
const handleCreateUpdateView = async (formData: IView) => {
|
||||||
await handleFormSubmit(formData);
|
await handleFormSubmit(formData);
|
||||||
|
|
||||||
@ -50,6 +87,18 @@ export const ViewForm: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearAllFilters = () => {
|
||||||
|
setValue("query", {
|
||||||
|
assignees: null,
|
||||||
|
created_by: null,
|
||||||
|
labels: null,
|
||||||
|
priority: null,
|
||||||
|
state: null,
|
||||||
|
target_date: null,
|
||||||
|
type: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset({
|
reset({
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
@ -106,23 +155,38 @@ export const ViewForm: React.FC<Props> = ({
|
|||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
const key = option.key as keyof typeof filters;
|
const key = option.key as keyof typeof filters;
|
||||||
|
|
||||||
if (!filters?.[key]?.includes(option.value))
|
if (key === "target_date") {
|
||||||
|
const valueExists = checkIfArraysHaveSameElements(
|
||||||
|
filters?.target_date ?? [],
|
||||||
|
option.value
|
||||||
|
);
|
||||||
|
|
||||||
setValue("query", {
|
setValue("query", {
|
||||||
...filters,
|
target_date: valueExists ? null : option.value,
|
||||||
[key]: [...((filters?.[key] as any[]) ?? []), option.value],
|
} as IQuery);
|
||||||
});
|
} else {
|
||||||
else {
|
if (!filters?.[key]?.includes(option.value))
|
||||||
setValue("query", {
|
setValue("query", {
|
||||||
...filters,
|
...filters,
|
||||||
[key]: (filters?.[key] as any[])?.filter((item) => item !== option.value),
|
[key]: [...((filters?.[key] as any[]) ?? []), option.value],
|
||||||
});
|
});
|
||||||
|
else {
|
||||||
|
setValue("query", {
|
||||||
|
...filters,
|
||||||
|
[key]: (filters?.[key] as any[])?.filter((item) => item !== option.value),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<FilterList
|
<FiltersList
|
||||||
filters={filters}
|
filters={filters}
|
||||||
|
labels={labels}
|
||||||
|
members={members?.map((m) => m.member)}
|
||||||
|
states={states}
|
||||||
|
clearAllFilters={clearAllFilters}
|
||||||
setFilters={(query: any) => {
|
setFilters={(query: any) => {
|
||||||
setValue("query", {
|
setValue("query", {
|
||||||
...filters,
|
...filters,
|
||||||
|
@ -83,121 +83,116 @@ export const SelectFilters: React.FC<Props> = ({
|
|||||||
id: "priority",
|
id: "priority",
|
||||||
label: "Priority",
|
label: "Priority",
|
||||||
value: PRIORITIES,
|
value: PRIORITIES,
|
||||||
children: [
|
hasChildren: true,
|
||||||
...PRIORITIES.map((priority) => ({
|
children: PRIORITIES.map((priority) => ({
|
||||||
id: priority === null ? "null" : priority,
|
id: priority === null ? "null" : priority,
|
||||||
label: (
|
label: (
|
||||||
<div className="flex items-center gap-2 capitalize">
|
<div className="flex items-center gap-2 capitalize">
|
||||||
{getPriorityIcon(priority)} {priority ?? "None"}
|
{getPriorityIcon(priority)} {priority ?? "None"}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
value: {
|
value: {
|
||||||
key: "priority",
|
key: "priority",
|
||||||
value: priority === null ? "null" : priority,
|
value: priority === null ? "null" : priority,
|
||||||
},
|
},
|
||||||
selected: filters?.priority?.includes(priority === null ? "null" : priority),
|
selected: filters?.priority?.includes(priority === null ? "null" : priority),
|
||||||
})),
|
})),
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "state",
|
id: "state",
|
||||||
label: "State",
|
label: "State",
|
||||||
value: statesList,
|
value: statesList,
|
||||||
children: [
|
hasChildren: true,
|
||||||
...statesList.map((state) => ({
|
children: statesList.map((state) => ({
|
||||||
id: state.id,
|
id: state.id,
|
||||||
label: (
|
label: (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{getStateGroupIcon(state.group, "16", "16", state.color)} {state.name}
|
{getStateGroupIcon(state.group, "16", "16", state.color)} {state.name}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
value: {
|
value: {
|
||||||
key: "state",
|
key: "state",
|
||||||
value: state.id,
|
value: state.id,
|
||||||
},
|
},
|
||||||
selected: filters?.state?.includes(state.id),
|
selected: filters?.state?.includes(state.id),
|
||||||
})),
|
})),
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "assignees",
|
id: "assignees",
|
||||||
label: "Assignees",
|
label: "Assignees",
|
||||||
value: members,
|
value: members,
|
||||||
children: [
|
hasChildren: true,
|
||||||
...(members?.map((member) => ({
|
children: members?.map((member) => ({
|
||||||
id: member.member.id,
|
id: member.member.id,
|
||||||
label: (
|
label: (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Avatar user={member.member} />
|
<Avatar user={member.member} />
|
||||||
{member.member.first_name && member.member.first_name !== ""
|
{member.member.first_name && member.member.first_name !== ""
|
||||||
? member.member.first_name
|
? member.member.first_name
|
||||||
: member.member.email}
|
: member.member.email}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
value: {
|
value: {
|
||||||
key: "assignees",
|
key: "assignees",
|
||||||
value: member.member.id,
|
value: member.member.id,
|
||||||
},
|
},
|
||||||
selected: filters?.assignees?.includes(member.member.id),
|
selected: filters?.assignees?.includes(member.member.id),
|
||||||
})) ?? []),
|
})),
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "created_by",
|
id: "created_by",
|
||||||
label: "Created by",
|
label: "Created by",
|
||||||
value: members,
|
value: members,
|
||||||
children: [
|
hasChildren: true,
|
||||||
...(members?.map((member) => ({
|
children: members?.map((member) => ({
|
||||||
id: member.member.id,
|
id: member.member.id,
|
||||||
label: (
|
label: (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Avatar user={member.member} />
|
<Avatar user={member.member} />
|
||||||
{member.member.first_name && member.member.first_name !== ""
|
{member.member.first_name && member.member.first_name !== ""
|
||||||
? member.member.first_name
|
? member.member.first_name
|
||||||
: member.member.email}
|
: member.member.email}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
value: {
|
value: {
|
||||||
key: "created_by",
|
key: "created_by",
|
||||||
value: member.member.id,
|
value: member.member.id,
|
||||||
},
|
},
|
||||||
selected: filters?.created_by?.includes(member.member.id),
|
selected: filters?.created_by?.includes(member.member.id),
|
||||||
})) ?? []),
|
})),
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "labels",
|
id: "labels",
|
||||||
label: "Labels",
|
label: "Labels",
|
||||||
value: issueLabels,
|
value: issueLabels,
|
||||||
children: [
|
hasChildren: true,
|
||||||
...(issueLabels?.map((label) => ({
|
children: issueLabels?.map((label) => ({
|
||||||
id: label.id,
|
id: label.id,
|
||||||
label: (
|
label: (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className="h-2 w-2 rounded-full"
|
className="h-2 w-2 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor:
|
backgroundColor: label.color && label.color !== "" ? label.color : "#000000",
|
||||||
label.color && label.color !== "" ? label.color : "#000000",
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
{label.name}
|
||||||
{label.name}
|
</div>
|
||||||
</div>
|
),
|
||||||
),
|
value: {
|
||||||
value: {
|
key: "labels",
|
||||||
key: "labels",
|
value: label.id,
|
||||||
value: label.id,
|
},
|
||||||
},
|
selected: filters?.labels?.includes(label.id),
|
||||||
selected: filters?.labels?.includes(label.id),
|
})),
|
||||||
})) ?? []),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "target_date",
|
id: "target_date",
|
||||||
label: "Due date",
|
label: "Due date",
|
||||||
value: DUE_DATES,
|
value: DUE_DATES,
|
||||||
|
hasChildren: true,
|
||||||
children: [
|
children: [
|
||||||
...(DUE_DATES?.map((option) => ({
|
...DUE_DATES.map((option) => ({
|
||||||
id: option.name,
|
id: option.name,
|
||||||
label: option.name,
|
label: option.name,
|
||||||
value: {
|
value: {
|
||||||
@ -205,7 +200,7 @@ export const SelectFilters: React.FC<Props> = ({
|
|||||||
value: option.value,
|
value: option.value,
|
||||||
},
|
},
|
||||||
selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], option.value),
|
selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], option.value),
|
||||||
})) ?? []),
|
})),
|
||||||
{
|
{
|
||||||
id: "custom",
|
id: "custom",
|
||||||
label: "Custom",
|
label: "Custom",
|
||||||
|
@ -37,6 +37,29 @@ const inboxParamsToKey = (params: any) => {
|
|||||||
return `${priorityKey}_${inboxStatusKey}`;
|
return `${priorityKey}_${inboxStatusKey}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const myIssuesParamsToKey = (params: any) => {
|
||||||
|
const { assignees, created_by, labels, priority, state_group, target_date } = params;
|
||||||
|
|
||||||
|
let assigneesKey = assignees ? assignees.split(",") : [];
|
||||||
|
let createdByKey = created_by ? created_by.split(",") : [];
|
||||||
|
let stateGroupKey = state_group ? state_group.split(",") : [];
|
||||||
|
let priorityKey = priority ? priority.split(",") : [];
|
||||||
|
let labelsKey = labels ? labels.split(",") : [];
|
||||||
|
const targetDateKey = target_date ?? "";
|
||||||
|
const type = params.type ? params.type.toUpperCase() : "NULL";
|
||||||
|
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
|
||||||
|
const orderBy = params.order_by ? params.order_by.toUpperCase() : "NULL";
|
||||||
|
|
||||||
|
// sorting each keys in ascending order
|
||||||
|
assigneesKey = assigneesKey.sort().join("_");
|
||||||
|
createdByKey = createdByKey.sort().join("_");
|
||||||
|
stateGroupKey = stateGroupKey.sort().join("_");
|
||||||
|
priorityKey = priorityKey.sort().join("_");
|
||||||
|
labelsKey = labelsKey.sort().join("_");
|
||||||
|
|
||||||
|
return `${assigneesKey}_${createdByKey}_${stateGroupKey}_${priorityKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const CURRENT_USER = "CURRENT_USER";
|
export const CURRENT_USER = "CURRENT_USER";
|
||||||
export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
|
export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
|
||||||
export const USER_WORKSPACES = "USER_WORKSPACES";
|
export const USER_WORKSPACES = "USER_WORKSPACES";
|
||||||
@ -97,6 +120,8 @@ export const PROJECT_ISSUE_BY_STATE = (projectId: string) =>
|
|||||||
`PROJECT_ISSUE_BY_STATE_${projectId.toUpperCase()}`;
|
`PROJECT_ISSUE_BY_STATE_${projectId.toUpperCase()}`;
|
||||||
export const PROJECT_ISSUE_LABELS = (projectId: string) =>
|
export const PROJECT_ISSUE_LABELS = (projectId: string) =>
|
||||||
`PROJECT_ISSUE_LABELS_${projectId.toUpperCase()}`;
|
`PROJECT_ISSUE_LABELS_${projectId.toUpperCase()}`;
|
||||||
|
export const WORKSPACE_LABELS = (workspaceSlug: string) =>
|
||||||
|
`WORKSPACE_LABELS_${workspaceSlug.toUpperCase()}`;
|
||||||
export const PROJECT_GITHUB_REPOSITORY = (projectId: string) =>
|
export const PROJECT_GITHUB_REPOSITORY = (projectId: string) =>
|
||||||
`PROJECT_GITHUB_REPOSITORY_${projectId.toUpperCase()}`;
|
`PROJECT_GITHUB_REPOSITORY_${projectId.toUpperCase()}`;
|
||||||
|
|
||||||
@ -123,9 +148,15 @@ export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string, params?: any) => {
|
|||||||
export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAILS_${cycleId.toUpperCase()}`;
|
export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAILS_${cycleId.toUpperCase()}`;
|
||||||
|
|
||||||
export const STATES_LIST = (projectId: string) => `STATES_LIST_${projectId.toUpperCase()}`;
|
export const STATES_LIST = (projectId: string) => `STATES_LIST_${projectId.toUpperCase()}`;
|
||||||
export const STATE_DETAILS = "STATE_DETAILS";
|
|
||||||
|
|
||||||
export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug.toUpperCase()}`;
|
export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug.toUpperCase()}`;
|
||||||
|
export const USER_ISSUES = (workspaceSlug: string, params: any) => {
|
||||||
|
if (!params) return `USER_ISSUES_${workspaceSlug.toUpperCase()}`;
|
||||||
|
|
||||||
|
const paramsKey = myIssuesParamsToKey(params);
|
||||||
|
|
||||||
|
return `USER_ISSUES_${paramsKey}`;
|
||||||
|
};
|
||||||
export const USER_ACTIVITY = "USER_ACTIVITY";
|
export const USER_ACTIVITY = "USER_ACTIVITY";
|
||||||
export const USER_WORKSPACE_DASHBOARD = (workspaceSlug: string) =>
|
export const USER_WORKSPACE_DASHBOARD = (workspaceSlug: string) =>
|
||||||
`USER_WORKSPACE_DASHBOARD_${workspaceSlug.toUpperCase()}`;
|
`USER_WORKSPACE_DASHBOARD_${workspaceSlug.toUpperCase()}`;
|
||||||
|
@ -2,8 +2,10 @@ export const GROUP_BY_OPTIONS: Array<{
|
|||||||
name: string;
|
name: string;
|
||||||
key: TIssueGroupByOptions;
|
key: TIssueGroupByOptions;
|
||||||
}> = [
|
}> = [
|
||||||
{ name: "State", key: "state" },
|
{ name: "States", key: "state" },
|
||||||
|
{ name: "State Groups", key: "state_detail.group" },
|
||||||
{ name: "Priority", key: "priority" },
|
{ name: "Priority", key: "priority" },
|
||||||
|
{ name: "Project", key: "project" },
|
||||||
{ name: "Labels", key: "labels" },
|
{ name: "Labels", key: "labels" },
|
||||||
{ name: "Created by", key: "created_by" },
|
{ name: "Created by", key: "created_by" },
|
||||||
{ name: "None", key: null },
|
{ name: "None", key: null },
|
||||||
|
@ -91,8 +91,6 @@ export const initialState: StateType = {
|
|||||||
assignees: null,
|
assignees: null,
|
||||||
labels: null,
|
labels: null,
|
||||||
state: null,
|
state: null,
|
||||||
issue__assignees__id: null,
|
|
||||||
issue__labels__id: null,
|
|
||||||
created_by: null,
|
created_by: null,
|
||||||
target_date: null,
|
target_date: null,
|
||||||
},
|
},
|
||||||
|
@ -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,
|
priority: filters?.priority ? filters?.priority.join(",") : undefined,
|
||||||
type: filters?.type ? filters?.type : undefined,
|
type: filters?.type ? filters?.type : undefined,
|
||||||
labels: filters?.labels ? filters?.labels.join(",") : undefined,
|
labels: filters?.labels ? filters?.labels.join(",") : undefined,
|
||||||
issue__assignees__id: filters?.issue__assignees__id
|
|
||||||
? filters?.issue__assignees__id.join(",")
|
|
||||||
: undefined,
|
|
||||||
issue__labels__id: filters?.issue__labels__id
|
|
||||||
? filters?.issue__labels__id.join(",")
|
|
||||||
: undefined,
|
|
||||||
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
|
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
|
||||||
target_date: calendarDateRange,
|
target_date: calendarDateRange,
|
||||||
};
|
};
|
||||||
|
@ -57,12 +57,6 @@ const useIssuesView = () => {
|
|||||||
priority: filters?.priority ? filters?.priority.join(",") : undefined,
|
priority: filters?.priority ? filters?.priority.join(",") : undefined,
|
||||||
type: filters?.type ? filters?.type : undefined,
|
type: filters?.type ? filters?.type : undefined,
|
||||||
labels: filters?.labels ? filters?.labels.join(",") : undefined,
|
labels: filters?.labels ? filters?.labels.join(",") : undefined,
|
||||||
issue__assignees__id: filters?.issue__assignees__id
|
|
||||||
? filters?.issue__assignees__id.join(",")
|
|
||||||
: undefined,
|
|
||||||
issue__labels__id: filters?.issue__labels__id
|
|
||||||
? filters?.issue__labels__id.join(",")
|
|
||||||
: undefined,
|
|
||||||
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
|
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
|
||||||
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
|
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
|
||||||
sub_issue: showSubIssues,
|
sub_issue: showSubIssues,
|
||||||
|
@ -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
|
// hooks
|
||||||
import useUser from "./use-user";
|
import useUser from "./use-user";
|
||||||
|
|
||||||
const useProjectMembers = (workspaceSlug: string, projectId: string) => {
|
const useProjectMembers = (workspaceSlug: string | undefined, projectId: string | undefined) => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
// fetching project members
|
// fetching project members
|
||||||
const { data: members } = useSWR(PROJECT_MEMBERS(projectId), () =>
|
const { data: members } = useSWR(
|
||||||
projectService.projectMembers(workspaceSlug, projectId)
|
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => projectService.projectMembers(workspaceSlug, projectId)
|
||||||
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasJoined = members?.some((item: any) => item.member.id === (user as any)?.id);
|
const hasJoined = members?.some((item: any) => item.member.id === (user as any)?.id);
|
||||||
|
@ -42,12 +42,6 @@ const useSpreadsheetIssuesView = () => {
|
|||||||
priority: filters?.priority ? filters?.priority.join(",") : undefined,
|
priority: filters?.priority ? filters?.priority.join(",") : undefined,
|
||||||
type: filters?.type ? filters?.type : undefined,
|
type: filters?.type ? filters?.type : undefined,
|
||||||
labels: filters?.labels ? filters?.labels.join(",") : undefined,
|
labels: filters?.labels ? filters?.labels.join(",") : undefined,
|
||||||
issue__assignees__id: filters?.issue__assignees__id
|
|
||||||
? filters?.issue__assignees__id.join(",")
|
|
||||||
: undefined,
|
|
||||||
issue__labels__id: filters?.issue__labels__id
|
|
||||||
? filters?.issue__labels__id.join(",")
|
|
||||||
: undefined,
|
|
||||||
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
|
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
|
||||||
sub_issue: "false",
|
sub_issue: "false",
|
||||||
};
|
};
|
||||||
|
@ -6,12 +6,14 @@ import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
|||||||
// hooks
|
// hooks
|
||||||
import useUser from "./use-user";
|
import useUser from "./use-user";
|
||||||
|
|
||||||
const useWorkspaceMembers = (workspaceSlug: string) => {
|
const useWorkspaceMembers = (workspaceSlug: string | undefined, fetchCondition?: boolean) => {
|
||||||
|
fetchCondition = fetchCondition ?? true;
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
const { data: workspaceMembers, error: workspaceMemberErrors } = useSWR(
|
const { data: workspaceMembers, error: workspaceMemberErrors } = useSWR(
|
||||||
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug) : null,
|
workspaceSlug && fetchCondition ? WORKSPACE_MEMBERS(workspaceSlug) : null,
|
||||||
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug) : null
|
workspaceSlug && fetchCondition ? () => workspaceService.workspaceMembers(workspaceSlug) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasJoined = workspaceMembers?.some((item: any) => item.member.id === (user as any)?.id);
|
const hasJoined = workspaceMembers?.some((item: any) => item.member.id === (user as any)?.id);
|
||||||
|
@ -1,40 +1,67 @@
|
|||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// headless ui
|
|
||||||
import { Disclosure, Popover, Transition } from "@headlessui/react";
|
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDownIcon, PlusIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
// images
|
|
||||||
import emptyMyIssues from "public/empty-state/my-issues.svg";
|
|
||||||
// layouts
|
// layouts
|
||||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||||
// hooks
|
// hooks
|
||||||
import useIssues from "hooks/use-issues";
|
import useProjects from "hooks/use-projects";
|
||||||
// ui
|
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
|
||||||
import { Spinner, PrimaryButton, EmptyState } from "components/ui";
|
|
||||||
import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs";
|
|
||||||
// hooks
|
|
||||||
import useMyIssuesProperties from "hooks/use-my-issues-filter";
|
|
||||||
// types
|
|
||||||
import { IIssue, Properties } from "types";
|
|
||||||
// components
|
// components
|
||||||
import { MyIssuesListItem } from "components/issues";
|
import { MyIssuesView, MyIssuesViewOptions } from "components/issues";
|
||||||
// helpers
|
// ui
|
||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { PrimaryButton } from "components/ui";
|
||||||
|
import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs";
|
||||||
// types
|
// types
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
import useProjects from "hooks/use-projects";
|
import useUser from "hooks/use-user";
|
||||||
|
|
||||||
const MyIssuesPage: NextPage = () => {
|
const MyIssuesPage: NextPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { myIssues } = useIssues(workspaceSlug as string);
|
|
||||||
const { projects } = useProjects();
|
const { projects } = useProjects();
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
const [properties, setProperties] = useMyIssuesProperties(workspaceSlug as string);
|
const { filters, setFilters } = useMyIssuesFilters(workspaceSlug?.toString());
|
||||||
|
|
||||||
|
const tabsList = [
|
||||||
|
{
|
||||||
|
key: "assigned",
|
||||||
|
label: "Assigned to me",
|
||||||
|
selected: (filters?.assignees ?? []).length > 0,
|
||||||
|
onClick: () => {
|
||||||
|
setFilters({
|
||||||
|
assignees: [user?.id ?? ""],
|
||||||
|
created_by: null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "created",
|
||||||
|
label: "Created by me",
|
||||||
|
selected: (filters?.created_by ?? []).length > 0,
|
||||||
|
onClick: () => {
|
||||||
|
setFilters({
|
||||||
|
created_by: [user?.id ?? ""],
|
||||||
|
assignees: null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!filters || !user) return;
|
||||||
|
|
||||||
|
if (!filters.assignees && !filters.created_by) {
|
||||||
|
setFilters({
|
||||||
|
assignees: [user.id],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [filters, setFilters, user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceAuthorizationLayout
|
<WorkspaceAuthorizationLayout
|
||||||
@ -45,59 +72,7 @@ const MyIssuesPage: NextPage = () => {
|
|||||||
}
|
}
|
||||||
right={
|
right={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{myIssues && myIssues.length > 0 && (
|
<MyIssuesViewOptions />
|
||||||
<Popover className="relative">
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<Popover.Button
|
|
||||||
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-background-90 hover:text-custom-text-100 focus:outline-none ${
|
|
||||||
open ? "bg-custom-background-90 text-custom-text-100" : "text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span>View</span>
|
|
||||||
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
|
||||||
</Popover.Button>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="transition ease-out duration-200"
|
|
||||||
enterFrom="opacity-0 translate-y-1"
|
|
||||||
enterTo="opacity-100 translate-y-0"
|
|
||||||
leave="transition ease-in duration-150"
|
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
|
||||||
leaveTo="opacity-0 translate-y-1"
|
|
||||||
>
|
|
||||||
<Popover.Panel className="absolute right-1/2 z-10 mr-5 mt-1 w-screen max-w-xs translate-x-1/2 transform overflow-hidden rounded-lg bg-custom-background-100 p-3 shadow-lg">
|
|
||||||
<div className="space-y-2 py-3">
|
|
||||||
<h4 className="text-sm text-custom-text-200">Properties</h4>
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
{Object.keys(properties).map((key) => {
|
|
||||||
if (key === "estimate" || key === "created_on" || key === "updated_on")
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={key}
|
|
||||||
type="button"
|
|
||||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
|
||||||
properties[key as keyof Properties]
|
|
||||||
? "border-custom-primary bg-custom-primary text-white"
|
|
||||||
: "border-custom-border-200"
|
|
||||||
}`}
|
|
||||||
onClick={() => setProperties(key as keyof Properties)}
|
|
||||||
>
|
|
||||||
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popover.Panel>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
)}
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -111,87 +86,26 @@ const MyIssuesPage: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex h-full w-full flex-col space-y-5">
|
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||||
{myIssues ? (
|
<div className="border-b border-custom-border-300">
|
||||||
<>
|
<div className="flex items-center overflow-x-scroll">
|
||||||
{myIssues.length > 0 ? (
|
{tabsList.map((tab) => (
|
||||||
<Disclosure as="div" defaultOpen>
|
<button
|
||||||
{({ open }) => (
|
key={tab.key}
|
||||||
<div>
|
type="button"
|
||||||
<div className="flex items-center px-4 py-2.5 bg-custom-background-90">
|
onClick={tab.onClick}
|
||||||
<Disclosure.Button>
|
className={`border-b-2 p-4 text-sm font-medium outline-none whitespace-nowrap ${
|
||||||
<div className="flex items-center gap-x-2">
|
tab.selected
|
||||||
<h2 className="font-medium leading-5">My Issues</h2>
|
? "border-custom-primary-100 text-custom-primary-100"
|
||||||
<span className="rounded-full bg-custom-background-80 py-0.5 px-3 text-sm text-custom-text-200">
|
: "border-transparent"
|
||||||
{myIssues.length}
|
}`}
|
||||||
</span>
|
>
|
||||||
</div>
|
{tab.label}
|
||||||
</Disclosure.Button>
|
</button>
|
||||||
</div>
|
))}
|
||||||
<Transition
|
|
||||||
show={open}
|
|
||||||
enter="transition duration-100 ease-out"
|
|
||||||
enterFrom="transform opacity-0"
|
|
||||||
enterTo="transform opacity-100"
|
|
||||||
leave="transition duration-75 ease-out"
|
|
||||||
leaveFrom="transform opacity-100"
|
|
||||||
leaveTo="transform opacity-0"
|
|
||||||
>
|
|
||||||
<Disclosure.Panel>
|
|
||||||
{myIssues.map((issue: IIssue) => (
|
|
||||||
<MyIssuesListItem
|
|
||||||
key={issue.id}
|
|
||||||
issue={issue}
|
|
||||||
properties={properties}
|
|
||||||
projectId={issue.project}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Disclosure.Panel>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Disclosure>
|
|
||||||
) : (
|
|
||||||
<EmptyState
|
|
||||||
title={
|
|
||||||
projects
|
|
||||||
? projects.length > 0
|
|
||||||
? "You don't have any issue assigned to you yet"
|
|
||||||
: "Issues assigned to you will appear here"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
projects
|
|
||||||
? projects.length > 0
|
|
||||||
? "Keep track of your work in a single place."
|
|
||||||
: "Let's create your first project and add issues that you want to accomplish."
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
image={emptyMyIssues}
|
|
||||||
buttonText={projects ? (projects.length > 0 ? "New Issue" : "New Project") : ""}
|
|
||||||
buttonIcon={<PlusIcon className="h-4 w-4" />}
|
|
||||||
onClick={() => {
|
|
||||||
let e: KeyboardEvent;
|
|
||||||
|
|
||||||
if (projects && projects.length > 0)
|
|
||||||
e = new KeyboardEvent("keydown", {
|
|
||||||
key: "c",
|
|
||||||
});
|
|
||||||
else
|
|
||||||
e = new KeyboardEvent("keydown", {
|
|
||||||
key: "p",
|
|
||||||
});
|
|
||||||
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
<MyIssuesView />
|
||||||
</div>
|
</div>
|
||||||
</WorkspaceAuthorizationLayout>
|
</WorkspaceAuthorizationLayout>
|
||||||
);
|
);
|
||||||
|
@ -12,7 +12,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
|||||||
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
||||||
// components
|
// components
|
||||||
import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "components/core";
|
import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "components/core";
|
||||||
import { CycleDetailsSidebar } from "components/cycles";
|
import { CycleDetailsSidebar, TransferIssues, TransferIssuesModal } from "components/cycles";
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
import cycleServices from "services/cycles.service";
|
import cycleServices from "services/cycles.service";
|
||||||
@ -36,6 +36,7 @@ const SingleCycle: React.FC = () => {
|
|||||||
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
||||||
const [cycleSidebar, setCycleSidebar] = useState(true);
|
const [cycleSidebar, setCycleSidebar] = useState(true);
|
||||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||||
|
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||||
@ -157,16 +158,22 @@ const SingleCycle: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<TransferIssuesModal
|
||||||
|
handleClose={() => setTransferIssuesModal(false)}
|
||||||
|
isOpen={transferIssuesModal}
|
||||||
|
/>
|
||||||
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
||||||
<div
|
<div
|
||||||
className={`h-full flex flex-col ${cycleSidebar ? "mr-[24rem]" : ""} ${
|
className={`h-full flex flex-col ${cycleSidebar ? "mr-[24rem]" : ""} ${
|
||||||
analyticsModal ? "mr-[50%]" : ""
|
analyticsModal ? "mr-[50%]" : ""
|
||||||
} duration-300`}
|
} duration-300`}
|
||||||
>
|
>
|
||||||
|
{cycleStatus === "completed" && (
|
||||||
|
<TransferIssues handleClick={() => setTransferIssuesModal(true)} />
|
||||||
|
)}
|
||||||
<IssuesView
|
<IssuesView
|
||||||
type="cycle"
|
|
||||||
openIssuesListModal={openIssuesListModal}
|
openIssuesListModal={openIssuesListModal}
|
||||||
isCompleted={cycleStatus === "completed" ?? false}
|
disableUserActions={cycleStatus === "completed" ?? false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CycleDetailsSidebar
|
<CycleDetailsSidebar
|
||||||
|
@ -168,7 +168,7 @@ const SingleModule: React.FC = () => {
|
|||||||
analyticsModal ? "mr-[50%]" : ""
|
analyticsModal ? "mr-[50%]" : ""
|
||||||
} duration-300`}
|
} duration-300`}
|
||||||
>
|
>
|
||||||
<IssuesView type="module" openIssuesListModal={openIssuesListModal} />
|
<IssuesView openIssuesListModal={openIssuesListModal} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ModuleDetailsSidebar
|
<ModuleDetailsSidebar
|
||||||
|
@ -195,22 +195,7 @@ const GeneralSettings: NextPage = () => {
|
|||||||
name="emoji_and_icon"
|
name="emoji_and_icon"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<EmojiIconPicker
|
<EmojiIconPicker
|
||||||
label={
|
label={value ? renderEmoji(value) : "Icon"}
|
||||||
value ? (
|
|
||||||
typeof value === "object" ? (
|
|
||||||
<span
|
|
||||||
style={{ color: value.color }}
|
|
||||||
className="material-symbols-rounded text-lg"
|
|
||||||
>
|
|
||||||
{value.name}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
renderEmoji(value)
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
"Icon"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
|
@ -248,6 +248,14 @@ class ProjectIssuesServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getWorkspaceLabels(workspaceSlug: string): Promise<IIssueLabels[]> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/labels/`)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getIssueLabels(workspaceSlug: string, projectId: string): Promise<IIssueLabels[]> {
|
async getIssueLabels(workspaceSlug: string, projectId: string): Promise<IIssueLabels[]> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
|
@ -4,6 +4,7 @@ import trackEventServices from "services/track-event.service";
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
ICurrentUserResponse,
|
ICurrentUserResponse,
|
||||||
|
IIssue,
|
||||||
IUser,
|
IUser,
|
||||||
IUserActivityResponse,
|
IUserActivityResponse,
|
||||||
IUserWorkspaceDashboard,
|
IUserWorkspaceDashboard,
|
||||||
@ -26,8 +27,18 @@ class UserService extends APIService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async userIssues(workspaceSlug: string): Promise<any> {
|
async userIssues(
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/my-issues/`)
|
workspaceSlug: string,
|
||||||
|
params: any
|
||||||
|
): Promise<
|
||||||
|
| {
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
}
|
||||||
|
| IIssue[]
|
||||||
|
> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/my-issues/`, {
|
||||||
|
params,
|
||||||
|
})
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
IProductUpdateResponse,
|
IProductUpdateResponse,
|
||||||
ICurrentUserResponse,
|
ICurrentUserResponse,
|
||||||
IWorkspaceBulkInviteFormData,
|
IWorkspaceBulkInviteFormData,
|
||||||
|
IWorkspaceViewProps,
|
||||||
} from "types";
|
} from "types";
|
||||||
|
|
||||||
const trackEvent =
|
const trackEvent =
|
||||||
@ -169,7 +170,10 @@ class WorkspaceService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateWorkspaceView(workspaceSlug: string, data: any): Promise<any> {
|
async updateWorkspaceView(
|
||||||
|
workspaceSlug: string,
|
||||||
|
data: { view_props: IWorkspaceViewProps }
|
||||||
|
): Promise<any> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/workspace-views/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/workspace-views/`, data)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
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;
|
target_date: string[] | null;
|
||||||
state: string[] | null;
|
state: string[] | null;
|
||||||
labels: string[] | null;
|
labels: string[] | null;
|
||||||
issue__assignees__id: string[] | null;
|
|
||||||
issue__labels__id: string[] | null;
|
|
||||||
priority: string[] | null;
|
priority: string[] | null;
|
||||||
created_by: string[] | null;
|
created_by: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TIssueViewOptions = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart";
|
export type TIssueViewOptions = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart";
|
||||||
|
|
||||||
export type TIssueGroupByOptions = "state" | "priority" | "labels" | "created_by" | null;
|
export type TIssueGroupByOptions =
|
||||||
|
| "state"
|
||||||
|
| "priority"
|
||||||
|
| "labels"
|
||||||
|
| "created_by"
|
||||||
|
| "state_detail.group"
|
||||||
|
| "project"
|
||||||
|
| null;
|
||||||
|
|
||||||
export type TIssueOrderByOptions =
|
export type TIssueOrderByOptions =
|
||||||
| "-created_at"
|
| "-created_at"
|
||||||
@ -279,3 +284,14 @@ export interface IIssueAttachment {
|
|||||||
updated_by: string;
|
updated_by: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IIssueViewProps {
|
||||||
|
groupedIssues: { [key: string]: IIssue[] } | undefined;
|
||||||
|
groupByProperty: TIssueGroupByOptions;
|
||||||
|
isEmpty: boolean;
|
||||||
|
issueView: TIssueViewOptions;
|
||||||
|
orderBy: TIssueOrderByOptions;
|
||||||
|
params: any;
|
||||||
|
properties: Properties;
|
||||||
|
showEmptyGroups: boolean;
|
||||||
|
}
|
||||||
|
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 {
|
export interface IQuery {
|
||||||
state: string[] | null;
|
|
||||||
parent: string[] | null;
|
|
||||||
priority: string[] | null;
|
|
||||||
labels: string[] | null;
|
|
||||||
assignees: string[] | null;
|
assignees: string[] | null;
|
||||||
created_by: string[] | null;
|
created_by: string[] | null;
|
||||||
name: string | null;
|
labels: string[] | null;
|
||||||
created_at: [
|
priority: string[] | null;
|
||||||
{
|
state: string[] | null;
|
||||||
datetime: string;
|
|
||||||
timeline: "before";
|
|
||||||
},
|
|
||||||
{
|
|
||||||
datetime: string;
|
|
||||||
timeline: "after";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
updated_at: string[] | null;
|
|
||||||
start_date: string[] | null;
|
|
||||||
target_date: string[] | null;
|
target_date: string[] | null;
|
||||||
completed_at: string[] | null;
|
type: "active" | "backlog" | null;
|
||||||
type: string;
|
|
||||||
}
|
}
|
||||||
|
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 {
|
export interface IWorkspace {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
@ -54,6 +62,19 @@ export type Properties = {
|
|||||||
updated_on: boolean;
|
updated_on: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface IWorkspaceViewProps {
|
||||||
|
properties: Properties;
|
||||||
|
issueView: TIssueViewOptions;
|
||||||
|
groupByProperty: TIssueGroupByOptions;
|
||||||
|
orderBy: TIssueOrderByOptions;
|
||||||
|
filters: Partial<
|
||||||
|
IIssueFilterOptions & {
|
||||||
|
state_group: string[] | null;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
showEmptyGroups: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IWorkspaceMember {
|
export interface IWorkspaceMember {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
user: IUserLite;
|
user: IUserLite;
|
||||||
@ -61,7 +82,7 @@ export interface IWorkspaceMember {
|
|||||||
member: IUserLite;
|
member: IUserLite;
|
||||||
role: 5 | 10 | 15 | 20;
|
role: 5 | 10 | 15 | 20;
|
||||||
company_role: string | null;
|
company_role: string | null;
|
||||||
view_props: Properties;
|
view_props: IWorkspaceViewProps;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
created_by: string;
|
created_by: string;
|
||||||
|
Loading…
Reference in New Issue
Block a user