diff --git a/apps/app/components/core/board-view/board-header.tsx b/apps/app/components/core/board-view/board-header.tsx index f1e481a51..6b8986093 100644 --- a/apps/app/components/core/board-view/board-header.tsx +++ b/apps/app/components/core/board-view/board-header.tsx @@ -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 = ({ setIsCollapsed, isCompleted = false, }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + const { groupedByIssues, groupByProperty: selectedGroup } = useIssuesView(); + const { data: issueLabels } = useSWR( + 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 = ({ ? (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 (
= ({ writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb", }} > - {selectedGroup === "state" - ? addSpaceIfCamelCase(currentState?.name ?? "") - : addSpaceIfCamelCase(groupTitle)} + {getGroupTitle()} {groupedByIssues?.[groupTitle].length ?? 0} diff --git a/apps/app/components/core/board-view/single-board.tsx b/apps/app/components/core/board-view/single-board.tsx index b6f466d47..f408bc38b 100644 --- a/apps/app/components/core/board-view/single-board.tsx +++ b/apps/app/components/core/board-view/single-board.tsx @@ -108,7 +108,7 @@ export const SingleBoard: React.FC = ({ key={issue.id} draggableId={issue.id} index={index} - isDragDisabled={isNotAllowed} + isDragDisabled={isNotAllowed || selectedGroup === "created_by"} > {(provided, snapshot) => ( void; makeIssueCopy: () => void; removeIssue?: (() => void) | null; diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx index 46a92d43c..96d4e0f83 100644 --- a/apps/app/components/core/issues-view.tsx +++ b/apps/app/components/core/issues-view.tsx @@ -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 = ({ : 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 = ({ (key) => filters[key as keyof IIssueFilterOptions] === null ); + const isUpdatingView = JSON.stringify(filters) === JSON.stringify(viewDetails?.query_data); + return ( <> = ({ })}
- {Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && ( - { - 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 && } - Save view - - )} + {viewId + ? isUpdatingView && ( + { + 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 + + ) + : Object.keys(filters).length > 0 && + nullFilters.length !== Object.keys(filters).length && ( + { + 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" + > + + Save view + + )} diff --git a/apps/app/components/core/list-view/single-list.tsx b/apps/app/components/core/list-view/single-list.tsx index 9e5ba27d5..cc2bd99d7 100644 --- a/apps/app/components/core/list-view/single-list.tsx +++ b/apps/app/components/core/list-view/single-list.tsx @@ -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 = ({ const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); + const { data: issueLabels } = useSWR( + 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 ( {({ open }) => ( @@ -75,9 +119,7 @@ export const SingleList: React.FC = ({ )} {selectedGroup !== null ? (

- {selectedGroup === "state" - ? addSpaceIfCamelCase(currentState?.name ?? "") - : addSpaceIfCamelCase(groupTitle)} + {getGroupTitle()}

) : (

All Issues

diff --git a/apps/app/components/icons/index.ts b/apps/app/components/icons/index.ts index beb6bef61..0da820635 100644 --- a/apps/app/components/icons/index.ts +++ b/apps/app/components/icons/index.ts @@ -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"; diff --git a/apps/app/components/icons/stacked-layers-icon.tsx b/apps/app/components/icons/stacked-layers-icon.tsx new file mode 100644 index 000000000..61e003d0a --- /dev/null +++ b/apps/app/components/icons/stacked-layers-icon.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const StackedLayersIcon: React.FC = ({ + width = "24", + height = "24", + className, + color = "#858e96", +}) => ( + + + +); diff --git a/apps/app/constants/issue.ts b/apps/app/constants/issue.ts index d7983cf4f..e219213c1 100644 --- a/apps/app/constants/issue.ts +++ b/apps/app/constants/issue.ts @@ -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, oldGroupTitle: string, - selectedGroupBy: "state" | "priority" | "labels" | null, + selectedGroupBy: TIssueGroupByOptions, issueIndex: number, prevData?: | { diff --git a/apps/app/contexts/issue-view.context.tsx b/apps/app/contexts/issue-view.context.tsx index fd3341160..e081eebaf 100644 --- a/apps/app/contexts/issue-view.context.tsx +++ b/apps/app/contexts/issue-view.context.tsx @@ -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({} 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, 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: { diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx index df01b2129..c266e03b4 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx @@ -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 = (props) => { const router = useRouter(); @@ -35,6 +35,13 @@ const SingleView: React.FC = (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 = (props) => { /> } + left={ + + + {viewDetails?.name && truncateText(viewDetails.name, 40)} + + } + className="ml-1.5" + width="auto" + > + {views?.map((view) => ( + + {truncateText(view.name, 40)} + + ))} + + } right={
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx index 56b007a04..0541229ac 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx @@ -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={ - + } right={ @@ -82,29 +83,35 @@ const ProjectViews: NextPage = () => { /> {views ? ( views.length > 0 ? ( -
- {views.map((view) => ( -
- - {view.name} - - - { - setSelectedView(view); - }} - > - - - Delete - - - -
- ))} +
+

Views

+
+ {views.map((view) => ( +
+
+ + + {view.name} + +
+ + { + setSelectedView(view); + }} + > + + + Delete + + + +
+ ))} +
) : (