forked from github/plane
feat: group by created by option (#516)
This commit is contained in:
parent
6c6f9a5bfd
commit
472767ab67
@ -1,5 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
// icons
|
||||
@ -8,7 +15,10 @@ import { getStateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IState } from "types";
|
||||
import { IIssueLabels, IState } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
currentState?: IState | null;
|
||||
groupTitle: string;
|
||||
@ -26,8 +36,25 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
setIsCollapsed,
|
||||
isCompleted = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { groupedByIssues, groupByProperty: selectedGroup } = useIssuesView();
|
||||
|
||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: members } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
let bgColor = "#000000";
|
||||
if (selectedGroup === "state") bgColor = currentState?.color ?? "#000000";
|
||||
|
||||
@ -40,6 +67,28 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
? (bgColor = "#22c55e")
|
||||
: (bgColor = "#ff0000");
|
||||
|
||||
const getGroupTitle = () => {
|
||||
let title = addSpaceIfCamelCase(groupTitle);
|
||||
|
||||
switch (selectedGroup) {
|
||||
case "state":
|
||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
||||
break;
|
||||
case "labels":
|
||||
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
||||
break;
|
||||
case "created_by":
|
||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
||||
title =
|
||||
member?.first_name && member.first_name !== ""
|
||||
? `${member.first_name} ${member.last_name}`
|
||||
: member?.email ?? "";
|
||||
break;
|
||||
}
|
||||
|
||||
return title;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex justify-between px-1 ${
|
||||
@ -59,9 +108,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
|
||||
}}
|
||||
>
|
||||
{selectedGroup === "state"
|
||||
? addSpaceIfCamelCase(currentState?.name ?? "")
|
||||
: addSpaceIfCamelCase(groupTitle)}
|
||||
{getGroupTitle()}
|
||||
</h2>
|
||||
<span className="ml-0.5 rounded-full bg-gray-100 py-1 px-3 text-sm">
|
||||
{groupedByIssues?.[groupTitle].length ?? 0}
|
||||
|
@ -108,7 +108,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
key={issue.id}
|
||||
draggableId={issue.id}
|
||||
index={index}
|
||||
isDragDisabled={isNotAllowed}
|
||||
isDragDisabled={isNotAllowed || selectedGroup === "created_by"}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<SingleBoardIssue
|
||||
|
@ -37,7 +37,7 @@ import {
|
||||
import { handleIssuesMutation } from "constants/issue";
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue, Properties, UserAuth } from "types";
|
||||
import { IIssue, Properties, TIssueGroupByOptions, UserAuth } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
@ -53,7 +53,7 @@ type Props = {
|
||||
properties: Properties;
|
||||
groupTitle?: string;
|
||||
index: number;
|
||||
selectedGroup: "priority" | "state" | "labels" | null;
|
||||
selectedGroup: TIssueGroupByOptions;
|
||||
editIssue: () => void;
|
||||
makeIssueCopy: () => void;
|
||||
removeIssue?: (() => void) | null;
|
||||
|
@ -11,6 +11,7 @@ import issuesService from "services/issues.service";
|
||||
import stateService from "services/state.service";
|
||||
import projectService from "services/project.service";
|
||||
import modulesService from "services/modules.service";
|
||||
import viewsService from "services/views.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
@ -29,6 +30,7 @@ import {
|
||||
TrashIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// types
|
||||
@ -48,9 +50,9 @@ import {
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
PROJECT_MEMBERS,
|
||||
STATE_LIST,
|
||||
VIEW_DETAILS,
|
||||
} from "constants/fetch-keys";
|
||||
import { getPriorityIcon } from "components/icons/priority-icon";
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
|
||||
type Props = {
|
||||
type?: "issue" | "cycle" | "module";
|
||||
@ -116,6 +118,18 @@ export const IssuesView: React.FC<Props> = ({
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: viewDetails } = useSWR(
|
||||
workspaceSlug && projectId && viewId ? VIEW_DETAILS(viewId as string) : null,
|
||||
workspaceSlug && projectId && viewId
|
||||
? () =>
|
||||
viewsService.getViewDetails(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
viewId as string
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const handleDeleteIssue = useCallback(
|
||||
(issue: IIssue) => {
|
||||
setDeleteIssueModal(true);
|
||||
@ -391,6 +405,8 @@ export const IssuesView: React.FC<Props> = ({
|
||||
(key) => filters[key as keyof IIssueFilterOptions] === null
|
||||
);
|
||||
|
||||
const isUpdatingView = JSON.stringify(filters) === JSON.stringify(viewDetails?.query_data);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateViewModal
|
||||
@ -540,27 +556,49 @@ export const IssuesView: React.FC<Props> = ({
|
||||
})}
|
||||
</div>
|
||||
|
||||
{Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && (
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
if (viewId) {
|
||||
setFilters({}, true);
|
||||
setToastAlert({
|
||||
title: "View updated",
|
||||
message: "Your view has been updated",
|
||||
type: "success",
|
||||
});
|
||||
} else
|
||||
setCreateViewModal({
|
||||
query: filters,
|
||||
});
|
||||
}}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
{!viewId && <PlusIcon className="h-4 w-4" />}
|
||||
Save view
|
||||
</PrimaryButton>
|
||||
)}
|
||||
{viewId
|
||||
? isUpdatingView && (
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
if (viewId) {
|
||||
setFilters({}, true);
|
||||
setToastAlert({
|
||||
title: "View updated",
|
||||
message: "Your view has been updated",
|
||||
type: "success",
|
||||
});
|
||||
} else
|
||||
setCreateViewModal({
|
||||
query: filters,
|
||||
});
|
||||
}}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
Update view
|
||||
</PrimaryButton>
|
||||
)
|
||||
: Object.keys(filters).length > 0 &&
|
||||
nullFilters.length !== Object.keys(filters).length && (
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
if (viewId) {
|
||||
setFilters({}, true);
|
||||
setToastAlert({
|
||||
title: "View updated",
|
||||
message: "Your view has been updated",
|
||||
type: "success",
|
||||
});
|
||||
} else
|
||||
setCreateViewModal({
|
||||
query: filters,
|
||||
});
|
||||
}}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Save view
|
||||
</PrimaryButton>
|
||||
)}
|
||||
</div>
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<StrictModeDroppable droppableId="trashBox">
|
||||
|
@ -1,19 +1,27 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// headless ui
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
// components
|
||||
import { SingleListIssue } from "components/core";
|
||||
// ui
|
||||
import { CustomMenu } from "components/ui";
|
||||
// icons
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue, IProjectMember, IState, UserAuth } from "types";
|
||||
import { CustomMenu } from "components/ui";
|
||||
import { IIssue, IIssueLabels, IState, TIssueGroupByOptions, UserAuth } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
type?: "issue" | "cycle" | "module";
|
||||
@ -23,7 +31,7 @@ type Props = {
|
||||
groupedByIssues: {
|
||||
[key: string]: IIssue[];
|
||||
};
|
||||
selectedGroup: "priority" | "state" | "labels" | null;
|
||||
selectedGroup: TIssueGroupByOptions;
|
||||
addIssueToState: () => void;
|
||||
makeIssueCopy: (issue: IIssue) => void;
|
||||
handleEditIssue: (issue: IIssue) => void;
|
||||
@ -55,6 +63,42 @@ export const SingleList: React.FC<Props> = ({
|
||||
|
||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||
|
||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: members } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const getGroupTitle = () => {
|
||||
let title = addSpaceIfCamelCase(groupTitle);
|
||||
|
||||
switch (selectedGroup) {
|
||||
case "state":
|
||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
||||
break;
|
||||
case "labels":
|
||||
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
||||
break;
|
||||
case "created_by":
|
||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
||||
title =
|
||||
member?.first_name && member.first_name !== ""
|
||||
? `${member.first_name} ${member.last_name}`
|
||||
: member?.email ?? "";
|
||||
break;
|
||||
}
|
||||
|
||||
return title;
|
||||
};
|
||||
|
||||
return (
|
||||
<Disclosure key={groupTitle} as="div" defaultOpen>
|
||||
{({ open }) => (
|
||||
@ -75,9 +119,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
)}
|
||||
{selectedGroup !== null ? (
|
||||
<h2 className="text-xl font-semibold capitalize leading-6 text-gray-800">
|
||||
{selectedGroup === "state"
|
||||
? addSpaceIfCamelCase(currentState?.name ?? "")
|
||||
: addSpaceIfCamelCase(groupTitle)}
|
||||
{getGroupTitle()}
|
||||
</h2>
|
||||
) : (
|
||||
<h2 className="font-medium leading-5">All Issues</h2>
|
||||
|
@ -28,6 +28,7 @@ export * from "./priority-icon";
|
||||
export * from "./question-mark-circle-icon";
|
||||
export * from "./setting-icon";
|
||||
export * from "./signal-cellular-icon";
|
||||
export * from "./stacked-layers-icon";
|
||||
export * from "./started-state-icon";
|
||||
export * from "./state-group-icon";
|
||||
export * from "./tag-icon";
|
||||
|
24
apps/app/components/icons/stacked-layers-icon.tsx
Normal file
24
apps/app/components/icons/stacked-layers-icon.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const StackedLayersIcon: React.FC<Props> = ({
|
||||
width = "24",
|
||||
height = "24",
|
||||
className,
|
||||
color = "#858e96",
|
||||
}) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18.0398 13.4374C18.1224 13.5807 18.1448 13.7508 18.1022 13.9106C18.0596 14.0703 17.9554 14.2067 17.8125 14.2898L10.3125 18.6648C10.2169 18.7205 10.1083 18.7499 9.99766 18.7499C9.88703 18.7499 9.77838 18.7205 9.68281 18.6648L2.18281 14.2898C2.04195 14.2051 1.94009 14.0684 1.8993 13.9092C1.8585 13.75 1.88205 13.5812 1.96484 13.4392C2.04764 13.2972 2.18301 13.1936 2.34166 13.1507C2.50031 13.1078 2.66946 13.1292 2.8125 13.2101L10 17.4015L17.1875 13.2101C17.3308 13.1275 17.5009 13.1051 17.6606 13.1477C17.8204 13.1903 17.9567 13.2945 18.0398 13.4374ZM17.1875 9.46009L10 13.6515L2.8125 9.46009C2.67019 9.38924 2.50623 9.37528 2.35399 9.42105C2.20175 9.46682 2.07267 9.56888 1.99303 9.70646C1.91338 9.84405 1.88916 10.0068 1.92529 10.1616C1.96142 10.3164 2.05518 10.4517 2.1875 10.5398L9.6875 14.9148C9.78307 14.9705 9.89171 14.9999 10.0023 14.9999C10.113 14.9999 10.2216 14.9705 10.3172 14.9148L17.8172 10.5398C17.8892 10.499 17.9524 10.4444 18.0032 10.379C18.0539 10.3136 18.0912 10.2388 18.1128 10.1589C18.1344 10.079 18.1399 9.99559 18.129 9.91354C18.1181 9.8315 18.091 9.75243 18.0493 9.68094C18.0076 9.60944 17.9521 9.54694 17.8861 9.49706C17.82 9.44718 17.7447 9.41092 17.6646 9.39037C17.5844 9.36983 17.5009 9.36541 17.419 9.37738C17.3371 9.38935 17.2584 9.41746 17.1875 9.46009ZM1.875 6.24994C1.87525 6.14047 1.90425 6.03299 1.95909 5.93824C2.01393 5.8435 2.0927 5.76483 2.1875 5.71009L9.6875 1.33509C9.78307 1.27936 9.89171 1.25 10.0023 1.25C10.113 1.25 10.2216 1.27936 10.3172 1.33509L17.8172 5.71009C17.9115 5.76514 17.9898 5.84395 18.0442 5.93867C18.0986 6.03339 18.1272 6.14071 18.1272 6.24994C18.1272 6.35917 18.0986 6.46649 18.0442 6.56121C17.9898 6.65593 17.9115 6.73474 17.8172 6.78978L10.3172 11.1648C10.2216 11.2205 10.113 11.2499 10.0023 11.2499C9.89171 11.2499 9.78307 11.2205 9.6875 11.1648L2.1875 6.78978C2.0927 6.73505 2.01393 6.65637 1.95909 6.56163C1.90425 6.46689 1.87525 6.35941 1.875 6.24994ZM3.74063 6.24994L10 9.9015L16.2594 6.24994L10 2.59838L3.74063 6.24994Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
);
|
@ -1,10 +1,11 @@
|
||||
export const GROUP_BY_OPTIONS: Array<{
|
||||
name: string;
|
||||
key: "state" | "priority" | "labels" | null;
|
||||
key: TIssueGroupByOptions;
|
||||
}> = [
|
||||
{ name: "State", key: "state" },
|
||||
{ name: "Priority", key: "priority" },
|
||||
{ name: "Labels", key: "labels" },
|
||||
{ name: "Created by", key: "created_by" },
|
||||
{ name: "None", key: null },
|
||||
];
|
||||
|
||||
@ -36,12 +37,12 @@ export const FILTER_ISSUE_OPTIONS: Array<{
|
||||
},
|
||||
];
|
||||
|
||||
import { IIssue } from "types";
|
||||
import { IIssue, TIssueGroupByOptions } from "types";
|
||||
|
||||
type THandleIssuesMutation = (
|
||||
formData: Partial<IIssue>,
|
||||
oldGroupTitle: string,
|
||||
selectedGroupBy: "state" | "priority" | "labels" | null,
|
||||
selectedGroupBy: TIssueGroupByOptions,
|
||||
issueIndex: number,
|
||||
prevData?:
|
||||
| {
|
||||
|
@ -10,7 +10,7 @@ import ToastAlert from "components/toast-alert";
|
||||
import projectService from "services/project.service";
|
||||
import viewsService from "services/views.service";
|
||||
// types
|
||||
import { IIssueFilterOptions, IProjectMember } from "types";
|
||||
import { IIssueFilterOptions, IProjectMember, TIssueGroupByOptions } from "types";
|
||||
// fetch-keys
|
||||
import { USER_PROJECT_VIEW, VIEW_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
@ -18,7 +18,7 @@ export const issueViewContext = createContext<ContextType>({} as ContextType);
|
||||
|
||||
type IssueViewProps = {
|
||||
issueView: "list" | "kanban";
|
||||
groupByProperty: "state" | "priority" | "labels" | null;
|
||||
groupByProperty: TIssueGroupByOptions;
|
||||
orderBy: "created_at" | "updated_at" | "priority" | "sort_order";
|
||||
showEmptyGroups: boolean;
|
||||
filters: IIssueFilterOptions;
|
||||
@ -37,7 +37,7 @@ type ReducerActionType = {
|
||||
};
|
||||
|
||||
type ContextType = IssueViewProps & {
|
||||
setGroupByProperty: (property: "state" | "priority" | "labels" | null) => void;
|
||||
setGroupByProperty: (property: TIssueGroupByOptions) => void;
|
||||
setOrderBy: (property: "created_at" | "updated_at" | "priority" | "sort_order") => void;
|
||||
setShowEmptyGroups: (property: boolean) => void;
|
||||
setFilters: (filters: Partial<IIssueFilterOptions>, saveToServer?: boolean) => void;
|
||||
@ -49,7 +49,7 @@ type ContextType = IssueViewProps & {
|
||||
|
||||
type StateType = {
|
||||
issueView: "list" | "kanban";
|
||||
groupByProperty: "state" | "priority" | "labels" | null;
|
||||
groupByProperty: TIssueGroupByOptions;
|
||||
orderBy: "created_at" | "updated_at" | "priority" | "sort_order";
|
||||
showEmptyGroups: boolean;
|
||||
filters: IIssueFilterOptions;
|
||||
@ -167,11 +167,11 @@ const saveDataToServer = async (workspaceSlug: string, projectID: string, state:
|
||||
|
||||
const sendFilterDataToServer = async (
|
||||
workspaceSlug: string,
|
||||
projectID: string,
|
||||
projectId: string,
|
||||
viewId: string,
|
||||
state: any
|
||||
) => {
|
||||
await viewsService.patchView(workspaceSlug, projectID, viewId, {
|
||||
await viewsService.patchView(workspaceSlug, projectId, viewId, {
|
||||
...state,
|
||||
});
|
||||
};
|
||||
@ -283,7 +283,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
||||
}, [workspaceSlug, projectId, state, mutateMyViewProps]);
|
||||
|
||||
const setGroupByProperty = useCallback(
|
||||
(property: "state" | "priority" | "labels" | null) => {
|
||||
(property: TIssueGroupByOptions) => {
|
||||
dispatch({
|
||||
type: "SET_GROUP_BY_PROPERTY",
|
||||
payload: {
|
||||
|
@ -12,17 +12,17 @@ import viewsService from "services/views.service";
|
||||
import AppLayout from "layouts/app-layout";
|
||||
// contexts
|
||||
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
||||
// components
|
||||
import { CreateUpdateViewModal, DeleteViewModal } from "components/views";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// types
|
||||
import { UserAuth } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_DETAILS, VIEW_DETAILS, VIEW_ISSUES } from "constants/fetch-keys";
|
||||
import { PROJECT_DETAILS, VIEWS_LIST, VIEW_DETAILS } from "constants/fetch-keys";
|
||||
import { IssuesFilterView, IssuesView } from "components/core";
|
||||
import { HeaderButton } from "components/ui";
|
||||
import { CustomMenu, HeaderButton } from "components/ui";
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
import { StackedLayersIcon } from "components/icons";
|
||||
|
||||
const SingleView: React.FC<UserAuth> = (props) => {
|
||||
const router = useRouter();
|
||||
@ -35,6 +35,13 @@ const SingleView: React.FC<UserAuth> = (props) => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: views } = useSWR(
|
||||
workspaceSlug && projectId ? VIEWS_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => viewsService.getViews(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: viewDetails } = useSWR(
|
||||
workspaceSlug && projectId && viewId ? VIEW_DETAILS(viewId as string) : null,
|
||||
workspaceSlug && projectId && viewId
|
||||
@ -58,6 +65,28 @@ const SingleView: React.FC<UserAuth> = (props) => {
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
}
|
||||
left={
|
||||
<CustomMenu
|
||||
label={
|
||||
<>
|
||||
<StackedLayersIcon height={12} width={12} />
|
||||
{viewDetails?.name && truncateText(viewDetails.name, 40)}
|
||||
</>
|
||||
}
|
||||
className="ml-1.5"
|
||||
width="auto"
|
||||
>
|
||||
{views?.map((view) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={view.id}
|
||||
renderAs="a"
|
||||
href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}
|
||||
>
|
||||
{truncateText(view.name, 40)}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
}
|
||||
right={
|
||||
<div className="flex items-center gap-2">
|
||||
<IssuesFilterView />
|
||||
|
@ -28,6 +28,7 @@ import { DeleteViewModal, CreateUpdateViewModal } from "components/views";
|
||||
// types
|
||||
import { IView } from "types";
|
||||
import type { NextPage, GetServerSidePropsContext } from "next";
|
||||
import { StackedLayersIcon } from "components/icons";
|
||||
|
||||
const ProjectViews: NextPage = () => {
|
||||
const [isCreateViewModalOpen, setIsCreateViewModalOpen] = useState(false);
|
||||
@ -59,7 +60,7 @@ const ProjectViews: NextPage = () => {
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
|
||||
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Cycles`} />
|
||||
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Views`} />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
right={
|
||||
@ -82,29 +83,35 @@ const ProjectViews: NextPage = () => {
|
||||
/>
|
||||
{views ? (
|
||||
views.length > 0 ? (
|
||||
<div className="rounded-[10px] border">
|
||||
{views.map((view) => (
|
||||
<div
|
||||
className="flex items-center justify-between border-b bg-white p-4 first:rounded-t-[10px] last:rounded-b-[10px]"
|
||||
key={view.id}
|
||||
>
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}>
|
||||
<a>{view.name}</a>
|
||||
</Link>
|
||||
<CustomMenu width="auto" verticalEllipsis>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setSelectedView(view);
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-2 text-gray-800">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
))}
|
||||
<div className="space-y-5">
|
||||
<h3 className="text-3xl font-semibold text-black">Views</h3>
|
||||
<div className="rounded-[10px] border">
|
||||
{views.map((view) => (
|
||||
<div
|
||||
className="flex items-center justify-between border-b bg-white p-4 first:rounded-t-[10px] last:rounded-b-[10px]"
|
||||
key={view.id}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<StackedLayersIcon height={18} width={18} />
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}>
|
||||
<a>{view.name}</a>
|
||||
</Link>
|
||||
</div>
|
||||
<CustomMenu width="auto" verticalEllipsis>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setSelectedView(view);
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-2 text-gray-800">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
|
4
apps/app/types/issues.d.ts
vendored
4
apps/app/types/issues.d.ts
vendored
@ -226,8 +226,10 @@ export interface IIssueFilterOptions {
|
||||
priority: string[] | null;
|
||||
}
|
||||
|
||||
export type TIssueGroupByOptions = "state" | "priority" | "labels" | "created_by" | null;
|
||||
|
||||
export interface IIssueViewOptions {
|
||||
group_by: "state" | "priority" | "labels" | null;
|
||||
group_by: TIssueGroupByOptions;
|
||||
order_by: "created_at" | "updated_at" | "priority" | "sort_order";
|
||||
filters: IIssueFilterOptions;
|
||||
}
|
||||
|
4
apps/app/types/projects.d.ts
vendored
4
apps/app/types/projects.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
import type { IIssueFilterOptions, IUserLite, IWorkspace } from "./";
|
||||
import type { IIssueFilterOptions, IUserLite, IWorkspace, TIssueGroupByOptions } from "./";
|
||||
|
||||
export interface IProject {
|
||||
cover_image: string | null;
|
||||
@ -36,7 +36,7 @@ export interface IFavoriteProject {
|
||||
|
||||
type ProjectViewTheme = {
|
||||
issueView: "list" | "kanban";
|
||||
groupByProperty: "state" | "priority" | "labels" | null;
|
||||
groupByProperty: TIssueGroupByOptions;
|
||||
orderBy: "created_at" | "updated_at" | "priority" | "sort_order";
|
||||
filters: IIssueFilterOptions;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user