From 55083b732bac408561185a812f7c0598476fb882 Mon Sep 17 00:00:00 2001 From: rahulramesha Date: Fri, 19 Jul 2024 17:45:54 +0530 Subject: [PATCH 1/5] Changes required to enable Publish Views --- packages/types/src/view-props.d.ts | 2 + packages/types/src/views.d.ts | 12 + space/app/views/[anchor]/layout.tsx | 75 +++++ space/app/views/[anchor]/page.tsx | 30 ++ space/ce/components/issue-layouts/root.tsx | 9 + space/ce/components/navbar/index.tsx | 8 + space/ce/hooks/store/index.ts | 1 + space/ce/hooks/store/use-published-view.ts | 5 + .../components/issues/issue-layouts/index.ts | 4 +- .../issues/issue-layouts/issue-layout-HOC.tsx | 33 ++ .../issue-layouts/kanban/base-kanban-root.tsx | 73 +++++ .../issues/issue-layouts/kanban/block.tsx | 132 ++++---- .../issue-layouts/kanban/blocks-list.tsx | 45 +++ .../issues/issue-layouts/kanban/column.tsx | 70 ----- .../issues/issue-layouts/kanban/default.tsx | 130 ++++++++ .../issues/issue-layouts/kanban/header.tsx | 32 -- .../kanban/headers/group-by-card.tsx | 35 +++ .../kanban/headers/sub-group-by-card.tsx | 35 +++ .../issues/issue-layouts/kanban/index.ts | 3 +- .../issue-layouts/kanban/kanban-group.tsx | 117 +++++++ .../issues/issue-layouts/kanban/root.tsx | 34 -- .../issues/issue-layouts/kanban/swimlanes.tsx | 294 ++++++++++++++++++ .../issue-layouts/list/base-list-root.tsx | 59 ++++ .../issues/issue-layouts/list/block.tsx | 132 ++++---- .../issues/issue-layouts/list/blocks-list.tsx | 25 ++ .../issues/issue-layouts/list/default.tsx | 86 +++++ .../issues/issue-layouts/list/group.tsx | 59 ---- .../issues/issue-layouts/list/header.tsx | 33 -- .../list/headers/group-by-card.tsx | 34 ++ .../issues/issue-layouts/list/index.ts | 3 +- .../issues/issue-layouts/list/list-group.tsx | 129 ++++++++ .../issues/issue-layouts/list/root.tsx | 34 -- .../properties/all-properties.tsx | 183 +++++++++++ .../issues/issue-layouts/properties/cycle.tsx | 35 +++ .../issue-layouts/properties/due-date.tsx | 33 +- .../issues/issue-layouts/properties/index.ts | 4 + .../issue-layouts/properties/labels.tsx | 63 +++- .../issue-layouts/properties/member.tsx | 74 +++++ .../issue-layouts/properties/modules.tsx | 47 +++ .../issue-layouts/properties/priority.tsx | 20 +- .../issues/issue-layouts/properties/state.tsx | 26 +- .../components/issues/issue-layouts/utils.tsx | 240 ++++++++++++++ .../with-display-properties-HOC.tsx | 26 ++ .../issues/peek-overview/layout.tsx | 35 ++- space/core/constants/issue.ts | 13 +- space/core/hooks/store/index.ts | 3 + space/core/hooks/store/use-cycle.ts | 11 + space/core/hooks/store/use-member.ts | 11 + space/core/hooks/store/use-module.ts | 11 + space/core/services/cycle.service.ts | 17 + space/core/services/member.service.ts | 17 + space/core/services/module.service.ts | 17 + space/core/store/cycle.store.ts | 40 +++ space/core/store/helpers/base-issues.store.ts | 39 +-- space/core/store/helpers/filter.helpers.ts | 10 + space/core/store/issue-detail.store.ts | 43 ++- space/core/store/issue.store.ts | 2 +- space/core/store/members.store.ts | 63 ++++ space/core/store/module.store.ts | 63 ++++ space/core/store/root.store.ts | 12 + space/core/types/cycle.d.ts | 5 + space/core/types/issue.d.ts | 6 + space/core/types/member.d.ts | 10 + space/core/types/modules.d.ts | 4 + space/ee/components/issue-layouts/root.tsx | 9 + space/ee/components/navbar/index.tsx | 8 + space/ee/hooks/store/index.ts | 1 + space/ee/hooks/store/use-published-view.ts | 5 + space/helpers/string.helper.ts | 4 + space/styles/globals.css | 87 ++++++ web/ce/components/views/publish/index.ts | 2 + web/ce/components/views/publish/modal.tsx | 12 + .../views/publish/use-view-publish.tsx | 7 + web/ce/services/project/view.service.ts | 37 +++ web/core/components/views/quick-actions.tsx | 9 + web/core/store/project-view.store.ts | 108 ++++++- web/ee/components/views/publish/index.ts | 2 + web/ee/components/views/publish/modal.tsx | 12 + .../views/publish/use-view-publish.tsx | 7 + web/ee/services/project/view.service.ts | 61 ++++ 80 files changed, 2834 insertions(+), 493 deletions(-) create mode 100644 space/app/views/[anchor]/layout.tsx create mode 100644 space/app/views/[anchor]/page.tsx create mode 100644 space/ce/components/issue-layouts/root.tsx create mode 100644 space/ce/components/navbar/index.tsx create mode 100644 space/ce/hooks/store/index.ts create mode 100644 space/ce/hooks/store/use-published-view.ts create mode 100644 space/core/components/issues/issue-layouts/issue-layout-HOC.tsx create mode 100644 space/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx create mode 100644 space/core/components/issues/issue-layouts/kanban/blocks-list.tsx delete mode 100644 space/core/components/issues/issue-layouts/kanban/column.tsx create mode 100644 space/core/components/issues/issue-layouts/kanban/default.tsx delete mode 100644 space/core/components/issues/issue-layouts/kanban/header.tsx create mode 100644 space/core/components/issues/issue-layouts/kanban/headers/group-by-card.tsx create mode 100644 space/core/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx create mode 100644 space/core/components/issues/issue-layouts/kanban/kanban-group.tsx delete mode 100644 space/core/components/issues/issue-layouts/kanban/root.tsx create mode 100644 space/core/components/issues/issue-layouts/kanban/swimlanes.tsx create mode 100644 space/core/components/issues/issue-layouts/list/base-list-root.tsx create mode 100644 space/core/components/issues/issue-layouts/list/blocks-list.tsx create mode 100644 space/core/components/issues/issue-layouts/list/default.tsx delete mode 100644 space/core/components/issues/issue-layouts/list/group.tsx delete mode 100644 space/core/components/issues/issue-layouts/list/header.tsx create mode 100644 space/core/components/issues/issue-layouts/list/headers/group-by-card.tsx create mode 100644 space/core/components/issues/issue-layouts/list/list-group.tsx delete mode 100644 space/core/components/issues/issue-layouts/list/root.tsx create mode 100644 space/core/components/issues/issue-layouts/properties/all-properties.tsx create mode 100644 space/core/components/issues/issue-layouts/properties/cycle.tsx create mode 100644 space/core/components/issues/issue-layouts/properties/member.tsx create mode 100644 space/core/components/issues/issue-layouts/properties/modules.tsx create mode 100644 space/core/components/issues/issue-layouts/utils.tsx create mode 100644 space/core/components/issues/issue-layouts/with-display-properties-HOC.tsx create mode 100644 space/core/hooks/store/use-cycle.ts create mode 100644 space/core/hooks/store/use-member.ts create mode 100644 space/core/hooks/store/use-module.ts create mode 100644 space/core/services/cycle.service.ts create mode 100644 space/core/services/member.service.ts create mode 100644 space/core/services/module.service.ts create mode 100644 space/core/store/cycle.store.ts create mode 100644 space/core/store/members.store.ts create mode 100644 space/core/store/module.store.ts create mode 100644 space/core/types/cycle.d.ts create mode 100644 space/core/types/member.d.ts create mode 100644 space/core/types/modules.d.ts create mode 100644 space/ee/components/issue-layouts/root.tsx create mode 100644 space/ee/components/navbar/index.tsx create mode 100644 space/ee/hooks/store/index.ts create mode 100644 space/ee/hooks/store/use-published-view.ts create mode 100644 web/ce/components/views/publish/index.ts create mode 100644 web/ce/components/views/publish/modal.tsx create mode 100644 web/ce/components/views/publish/use-view-publish.tsx create mode 100644 web/ee/components/views/publish/index.ts create mode 100644 web/ee/components/views/publish/modal.tsx create mode 100644 web/ee/components/views/publish/use-view-publish.tsx create mode 100644 web/ee/services/project/view.service.ts diff --git a/packages/types/src/view-props.d.ts b/packages/types/src/view-props.d.ts index 82302dda156..819cd56ead6 100644 --- a/packages/types/src/view-props.d.ts +++ b/packages/types/src/view-props.d.ts @@ -202,4 +202,6 @@ export interface IssuePaginationOptions { before?: string; after?: string; groupedBy?: TIssueGroupByOptions; + subGroupedBy?: TIssueGroupByOptions; + orderBy?: TIssueOrderByOptions; } diff --git a/packages/types/src/views.d.ts b/packages/types/src/views.d.ts index 1c61ab69c6d..54e1a395c32 100644 --- a/packages/types/src/views.d.ts +++ b/packages/types/src/views.d.ts @@ -25,9 +25,21 @@ export interface IProjectView { workspace: string; logo_props: TLogoProps | undefined; is_locked: boolean; + anchor?: string; owned_by: string; } +export type TPublishViewSettings = { + is_comments_enabled: boolean; + is_reactions_enabled: boolean; + is_votes_enabled: boolean; +}; + +export type TPublishViewDetails = TPublishViewSettings & { + id: string; + anchor: string; +}; + export type TViewFiltersSortKey = "name" | "created_at" | "updated_at"; export type TViewFiltersSortBy = "asc" | "desc"; diff --git a/space/app/views/[anchor]/layout.tsx b/space/app/views/[anchor]/layout.tsx new file mode 100644 index 00000000000..81d0c992d0d --- /dev/null +++ b/space/app/views/[anchor]/layout.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { observer } from "mobx-react"; +import Image from "next/image"; +import useSWR from "swr"; +// components +import { LogoSpinner } from "@/components/common"; +// hooks +import { usePublish, usePublishList } from "@/hooks/store"; +// Plane web +import { ViewNavbarRoot } from "@/plane-web/components/navbar"; +import { useView } from "@/plane-web/hooks/store"; +// assets +import planeLogo from "@/public/plane-logo.svg"; + +type Props = { + children: React.ReactNode; + params: { + anchor: string; + }; +}; + +const IssuesLayout = observer((props: Props) => { + const { children, params } = props; + // params + const { anchor } = params; + // store hooks + const { fetchPublishSettings } = usePublishList(); + const { viewData, fetchViewDetails } = useView(); + const publishSettings = usePublish(anchor); + // fetch publish settings + useSWR( + anchor ? `PUBLISH_SETTINGS_${anchor}` : null, + anchor + ? async () => { + await fetchPublishSettings(anchor); + } + : null + ); + // fetch view data + useSWR( + anchor ? `VIEW_DETAILS_${anchor}` : null, + anchor + ? async () => { + await fetchViewDetails(anchor); + } + : null + ); + + if (!publishSettings || !viewData) return ; + + return ( +
+
+ +
+
{children}
+ +
+ Plane logo +
+
+ Powered by Plane Publish +
+
+
+ ); +}); + +export default IssuesLayout; diff --git a/space/app/views/[anchor]/page.tsx b/space/app/views/[anchor]/page.tsx new file mode 100644 index 00000000000..21bb5a96574 --- /dev/null +++ b/space/app/views/[anchor]/page.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { observer } from "mobx-react"; +import { useSearchParams } from "next/navigation"; +// hooks +import { usePublish } from "@/hooks/store"; +// plane-web +import { ViewLayoutsRoot } from "@/plane-web/components/issue-layouts/root"; + +type Props = { + params: { + anchor: string; + }; +}; + +const IssuesPage = observer((props: Props) => { + const { params } = props; + const { anchor } = params; + // params + const searchParams = useSearchParams(); + const peekId = searchParams.get("peekId") || undefined; + + const publishSettings = usePublish(anchor); + + if (!publishSettings) return null; + + return ; +}); + +export default IssuesPage; diff --git a/space/ce/components/issue-layouts/root.tsx b/space/ce/components/issue-layouts/root.tsx new file mode 100644 index 00000000000..1a04c6be4e2 --- /dev/null +++ b/space/ce/components/issue-layouts/root.tsx @@ -0,0 +1,9 @@ +import { PublishStore } from "@/store/publish/publish.store"; + +type Props = { + peekId: string | undefined; + publishSettings: PublishStore; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const ViewLayoutsRoot = (props: Props) => <>; diff --git a/space/ce/components/navbar/index.tsx b/space/ce/components/navbar/index.tsx new file mode 100644 index 00000000000..6e6fa444149 --- /dev/null +++ b/space/ce/components/navbar/index.tsx @@ -0,0 +1,8 @@ +import { PublishStore } from "@/store/publish/publish.store"; + +type Props = { + publishSettings: PublishStore; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const ViewNavbarRoot = (props: Props) => <>; diff --git a/space/ce/hooks/store/index.ts b/space/ce/hooks/store/index.ts new file mode 100644 index 00000000000..a5fc99eef89 --- /dev/null +++ b/space/ce/hooks/store/index.ts @@ -0,0 +1 @@ +export * from "./use-published-view"; diff --git a/space/ce/hooks/store/use-published-view.ts b/space/ce/hooks/store/use-published-view.ts new file mode 100644 index 00000000000..170d934da20 --- /dev/null +++ b/space/ce/hooks/store/use-published-view.ts @@ -0,0 +1,5 @@ +export const useView = () => ({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + fetchViewDetails: (anchor: string) => {}, + viewData: {}, +}); diff --git a/space/core/components/issues/issue-layouts/index.ts b/space/core/components/issues/issue-layouts/index.ts index 5ab6813cdfe..2115cf42d1e 100644 --- a/space/core/components/issues/issue-layouts/index.ts +++ b/space/core/components/issues/issue-layouts/index.ts @@ -1,4 +1,4 @@ -export * from "./kanban"; -export * from "./list"; +export * from "./kanban/base-kanban-root"; +export * from "./list/base-list-root"; export * from "./properties"; export * from "./root"; diff --git a/space/core/components/issues/issue-layouts/issue-layout-HOC.tsx b/space/core/components/issues/issue-layouts/issue-layout-HOC.tsx new file mode 100644 index 00000000000..cbb8aa551d6 --- /dev/null +++ b/space/core/components/issues/issue-layouts/issue-layout-HOC.tsx @@ -0,0 +1,33 @@ +import { observer } from "mobx-react"; +import { TLoader } from "@plane/types"; +import { LogoSpinner } from "@/components/common"; + +interface Props { + children: string | JSX.Element | JSX.Element[]; + getGroupIssueCount: ( + groupId: string | undefined, + subGroupId: string | undefined, + isSubGroupCumulative: boolean + ) => number | undefined; + getIssueLoader: (groupId?: string | undefined, subGroupId?: string | undefined) => TLoader; +} + +export const IssueLayoutHOC = observer((props: Props) => { + const { getIssueLoader, getGroupIssueCount } = props; + + const issueCount = getGroupIssueCount(undefined, undefined, false); + + if (getIssueLoader() === "init-loader" || issueCount === undefined) { + return ( +
+ +
+ ); + } + + if (getGroupIssueCount(undefined, undefined, false) === 0) { + return
No Issues Found
; + } + + return <>{props.children}; +}); diff --git a/space/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/space/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx new file mode 100644 index 00000000000..7020797eb0e --- /dev/null +++ b/space/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useCallback, useRef } from "react"; +import debounce from "lodash/debounce"; +import { observer } from "mobx-react"; +// types +import { IIssueDisplayProperties } from "@plane/types"; +// components +import { IssueLayoutHOC } from "@/components/issues/issue-layouts/issue-layout-HOC"; +// hooks +import { useIssue } from "@/hooks/store"; + +import { KanBan } from "./default"; + +type Props = { + anchor: string; +}; +export const IssueKanbanLayoutRoot: React.FC = observer((props: Props) => { + const { anchor } = props; + // store hooks + const { groupedIssueIds, getIssueLoader, fetchNextPublicIssues, getGroupIssueCount, getPaginationData } = useIssue(); + + const displayProperties: IIssueDisplayProperties = { + key: true, + state: true, + labels: true, + priority: true, + due_date: true, + }; + + const fetchMoreIssues = useCallback( + (groupId?: string, subgroupId?: string) => { + if (getIssueLoader(groupId, subgroupId) !== "pagination") { + fetchNextPublicIssues(anchor, groupId, subgroupId); + } + }, + [fetchNextPublicIssues] + ); + + const debouncedFetchMoreIssues = debounce( + (groupId?: string, subgroupId?: string) => fetchMoreIssues(groupId, subgroupId), + 300, + { leading: true, trailing: false } + ); + + const scrollableContainerRef = useRef(null); + + return ( + +
+
+
+ +
+
+
+
+ ); +}); diff --git a/space/core/components/issues/issue-layouts/kanban/block.tsx b/space/core/components/issues/issue-layouts/kanban/block.tsx index e6f2733ab97..b1d0f93aa02 100644 --- a/space/core/components/issues/issue-layouts/kanban/block.tsx +++ b/space/core/components/issues/issue-layouts/kanban/block.tsx @@ -1,81 +1,101 @@ "use client"; -import { FC } from "react"; +import { MutableRefObject } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -import { useSearchParams } from "next/navigation"; +import { useParams, useSearchParams } from "next/navigation"; +// plane +import { cn } from "@plane/editor"; +import { IIssueDisplayProperties } from "@plane/types"; +import { Tooltip } from "@plane/ui"; // components -import { IssueBlockDueDate, IssueBlockPriority, IssueBlockState } from "@/components/issues"; +import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/with-display-properties-HOC"; // helpers import { queryParamGenerator } from "@/helpers/query-param-generator"; // hooks -import { useIssue, useIssueDetails, usePublish } from "@/hooks/store"; -// interfaces +import { useIssueDetails, usePublish } from "@/hooks/store"; -type Props = { - anchor: string; +import { IIssue } from "@/types/issue"; +import { IssueProperties } from "../properties/all-properties"; +import { getIssueBlockId } from "../utils"; + +interface IssueBlockProps { issueId: string; -}; + groupId: string; + subGroupId: string; + displayProperties: IIssueDisplayProperties | undefined; + scrollableContainerRef?: MutableRefObject; +} + +interface IssueDetailsBlockProps { + issue: IIssue; + displayProperties: IIssueDisplayProperties | undefined; +} + +const KanbanIssueDetailsBlock: React.FC = observer((props) => { + const { issue, displayProperties } = props; + const { anchor } = useParams(); + // hooks + const { project_details } = usePublish(anchor.toString()); + + return ( +
+ +
+
+ {project_details?.identifier}-{issue.sequence_id} +
+
+
-export const IssueKanBanBlock: FC = observer((props) => { - const { anchor, issueId } = props; - const { getIssueById } = useIssue(); +
+ + {issue.name} + +
+ + +
+ ); +}); + +export const KanbanIssueBlock: React.FC = observer((props) => { + const { issueId, groupId, subGroupId, displayProperties } = props; const searchParams = useSearchParams(); // query params const board = searchParams.get("board"); - const state = searchParams.get("state"); - const priority = searchParams.get("priority"); - const labels = searchParams.get("labels"); - // store hooks - const { project_details } = usePublish(anchor); - const { setPeekId } = useIssueDetails(); - - const { queryParam } = queryParamGenerator({ board, peekId: issueId, priority, state, labels }); + // hooks + const { setPeekId, getIsIssuePeeked, getIssueById } = useIssueDetails(); - const handleBlockClick = () => { + const handleIssuePeekOverview = () => { setPeekId(issueId); }; + const { queryParam } = queryParamGenerator(board ? { board, peekId: issueId } : { peekId: issueId }); + const issue = getIssueById(issueId); - if (!issue) return <>; + if (!issue) return null; return ( - - {/* id */} -
- {project_details?.identifier}-{issue?.sequence_id} -
- - {/* name */} -
- {issue.name} -
- -
- {/* priority */} - {issue?.priority && ( -
- -
+
+ - -
- )} - {/* due date */} - {issue?.target_date && ( -
- -
- )} -
- + href={`?${queryParam}`} + onClick={handleIssuePeekOverview} + > + + + ); }); + +KanbanIssueBlock.displayName = "KanbanIssueBlock"; diff --git a/space/core/components/issues/issue-layouts/kanban/blocks-list.tsx b/space/core/components/issues/issue-layouts/kanban/blocks-list.tsx new file mode 100644 index 00000000000..c0a58325b5d --- /dev/null +++ b/space/core/components/issues/issue-layouts/kanban/blocks-list.tsx @@ -0,0 +1,45 @@ +import { MutableRefObject } from "react"; +import { observer } from "mobx-react"; +//types +import { IIssueDisplayProperties } from "@plane/types"; +// components +import { KanbanIssueBlock } from "./block"; + +interface IssueBlocksListProps { + subGroupId: string; + groupId: string; + issueIds: string[]; + displayProperties: IIssueDisplayProperties | undefined; + scrollableContainerRef?: MutableRefObject; +} + +export const KanbanIssueBlocksList: React.FC = observer((props) => { + const { subGroupId, groupId, issueIds, displayProperties, scrollableContainerRef } = props; + + return ( + <> + {issueIds && issueIds.length > 0 ? ( + <> + {issueIds.map((issueId) => { + if (!issueId) return null; + + let draggableId = issueId; + if (groupId) draggableId = `${draggableId}__${groupId}`; + if (subGroupId) draggableId = `${draggableId}__${subGroupId}`; + + return ( + + ); + })} + + ) : null} + + ); +}); diff --git a/space/core/components/issues/issue-layouts/kanban/column.tsx b/space/core/components/issues/issue-layouts/kanban/column.tsx deleted file mode 100644 index f8b2a9eaeb2..00000000000 --- a/space/core/components/issues/issue-layouts/kanban/column.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { useCallback, useRef, useState } from "react"; -import { observer } from "mobx-react"; -// components -import { Icon } from "@/components/ui"; -// hooks -import { useIssue } from "@/hooks/store"; -import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; -// components -import { IssueKanBanBlock } from "./block"; -import { IssueKanBanHeader } from "./header"; - -type Props = { - anchor: string; - stateId: string; - issueIds: string[]; -}; - -export const Column = observer((props: Props) => { - const { anchor, stateId, issueIds } = props; - - const containerRef = useRef(null); - const [intersectionElement, setIntersectionElement] = useState(null); - - const { fetchNextPublicIssues, getPaginationData, getIssueLoader, getGroupIssueCount } = useIssue(); - - const loadMoreIssuesInThisGroup = useCallback(() => { - fetchNextPublicIssues(anchor, stateId); - }, [fetchNextPublicIssues, anchor, stateId]); - - const isPaginating = !!getIssueLoader(stateId); - const nextPageResults = getPaginationData(stateId, undefined)?.nextPageResults; - - useIntersectionObserver( - containerRef, - isPaginating ? null : intersectionElement, - loadMoreIssuesInThisGroup, - `0% 100% 100% 100%` - ); - - const groupIssueCount = getGroupIssueCount(stateId, undefined, false); - const shouldLoadMore = - nextPageResults === undefined && groupIssueCount !== undefined - ? issueIds?.length < groupIssueCount - : !!nextPageResults; - - return ( -
-
- -
-
- {issueIds && issueIds.length > 0 ? ( -
- {issueIds.map((issueId) => ( - - ))} - {shouldLoadMore && ( -
- )} -
- ) : ( -
- - No issues in this state -
- )} -
-
- ); -}); diff --git a/space/core/components/issues/issue-layouts/kanban/default.tsx b/space/core/components/issues/issue-layouts/kanban/default.tsx new file mode 100644 index 00000000000..88b20427d03 --- /dev/null +++ b/space/core/components/issues/issue-layouts/kanban/default.tsx @@ -0,0 +1,130 @@ +import { MutableRefObject } from "react"; +import isNil from "lodash/isNil"; +import { observer } from "mobx-react"; +// types +import { + GroupByColumnTypes, + IGroupByColumn, + TGroupedIssues, + IIssueDisplayProperties, + TSubGroupedIssues, + TIssueGroupByOptions, + TPaginationData, + TLoader, +} from "@plane/types"; +// hooks +import { useMember, useModule, useStates, useLabel, useCycle } from "@/hooks/store"; +// +import { getGroupByColumns } from "../utils"; +// components +import { HeaderGroupByCard } from "./headers/group-by-card"; +import { KanbanGroup } from "./kanban-group"; + +export interface IKanBan { + groupedIssueIds: TGroupedIssues | TSubGroupedIssues; + displayProperties: IIssueDisplayProperties | undefined; + subGroupBy: TIssueGroupByOptions | undefined; + groupBy: TIssueGroupByOptions | undefined; + subGroupId?: string; + loadMoreIssues: (groupId?: string, subGroupId?: string) => void; + getGroupIssueCount: ( + groupId: string | undefined, + subGroupId: string | undefined, + isSubGroupCumulative: boolean + ) => number | undefined; + getPaginationData: (groupId: string | undefined, subGroupId: string | undefined) => TPaginationData | undefined; + getIssueLoader: (groupId?: string | undefined, subGroupId?: string | undefined) => TLoader; + scrollableContainerRef?: MutableRefObject; + showEmptyGroup?: boolean; +} + +export const KanBan: React.FC = observer((props) => { + const { + groupedIssueIds, + displayProperties, + subGroupBy, + groupBy, + subGroupId = "null", + loadMoreIssues, + getGroupIssueCount, + getPaginationData, + getIssueLoader, + scrollableContainerRef, + showEmptyGroup = true, + } = props; + + const member = useMember(); + const label = useLabel(); + const cycle = useCycle(); + const modules = useModule(); + const state = useStates(); + + const groupList = getGroupByColumns(groupBy as GroupByColumnTypes, cycle, modules, label, state, member); + + if (!groupList) return null; + + const visibilityGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => { + if (subGroupBy) { + const groupVisibility = { + showGroup: true, + showIssues: true, + }; + if (!showEmptyGroup) { + groupVisibility.showGroup = (getGroupIssueCount(_list.id, undefined, false) ?? 0) > 0; + } + return groupVisibility; + } else { + const groupVisibility = { + showGroup: true, + showIssues: true, + }; + return groupVisibility; + } + }; + + return ( +
+ {groupList && + groupList.length > 0 && + groupList.map((subList: IGroupByColumn) => { + const groupByVisibilityToggle = visibilityGroupBy(subList); + + if (groupByVisibilityToggle.showGroup === false) return <>; + return ( +
+ {isNil(subGroupBy) && ( +
+ +
+ )} + + {groupByVisibilityToggle.showIssues && ( + + )} +
+ ); + })} +
+ ); +}); diff --git a/space/core/components/issues/issue-layouts/kanban/header.tsx b/space/core/components/issues/issue-layouts/kanban/header.tsx deleted file mode 100644 index 2c7f91dc7dd..00000000000 --- a/space/core/components/issues/issue-layouts/kanban/header.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -import { observer } from "mobx-react"; -// ui -import { StateGroupIcon } from "@plane/ui"; -// hooks -import { useIssue, useStates } from "@/hooks/store"; - -type Props = { - stateId: string; -}; - -export const IssueKanBanHeader: React.FC = observer((props) => { - const { stateId } = props; - - const { getStateById } = useStates(); - const { getGroupIssueCount } = useIssue(); - - const state = getStateById(stateId); - - return ( -
-
- -
-
{state?.name ?? "State"}
- - {getGroupIssueCount(stateId, undefined, false) ?? 0} - -
- ); -}); diff --git a/space/core/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/space/core/components/issues/issue-layouts/kanban/headers/group-by-card.tsx new file mode 100644 index 00000000000..a36d9f92299 --- /dev/null +++ b/space/core/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -0,0 +1,35 @@ +"use client"; + +import React, { FC } from "react"; +import { observer } from "mobx-react"; +import { Circle } from "lucide-react"; +// types +import { TIssueGroupByOptions } from "@plane/types"; + +interface IHeaderGroupByCard { + groupBy: TIssueGroupByOptions | undefined; + icon?: React.ReactNode; + title: string; + count: number; +} + +export const HeaderGroupByCard: FC = observer((props) => { + const { icon, title, count } = props; + + return ( + <> +
+
+ {icon ? icon : } +
+ +
+
+ {title} +
+
{count || 0}
+
+
+ + ); +}); diff --git a/space/core/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx b/space/core/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx new file mode 100644 index 00000000000..2e91624d111 --- /dev/null +++ b/space/core/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx @@ -0,0 +1,35 @@ +import React, { FC } from "react"; +import { observer } from "mobx-react"; +import { Circle, ChevronDown, ChevronUp } from "lucide-react"; +// mobx + +interface IHeaderSubGroupByCard { + icon?: React.ReactNode; + title: string; + count: number; + isExpanded: boolean; + toggleExpanded: () => void; +} + +export const HeaderSubGroupByCard: FC = observer((props) => { + const { icon, title, count, isExpanded, toggleExpanded } = props; + return ( +
toggleExpanded()} + > +
+ {isExpanded ? : } +
+ +
+ {icon ? icon : } +
+ +
+
{title}
+
{count || 0}
+
+
+ ); +}); diff --git a/space/core/components/issues/issue-layouts/kanban/index.ts b/space/core/components/issues/issue-layouts/kanban/index.ts index 62874fbda4e..068da7360b3 100644 --- a/space/core/components/issues/issue-layouts/kanban/index.ts +++ b/space/core/components/issues/issue-layouts/kanban/index.ts @@ -1,3 +1,2 @@ export * from "./block"; -export * from "./header"; -export * from "./root"; +export * from "./blocks-list"; diff --git a/space/core/components/issues/issue-layouts/kanban/kanban-group.tsx b/space/core/components/issues/issue-layouts/kanban/kanban-group.tsx new file mode 100644 index 00000000000..0842e46acf6 --- /dev/null +++ b/space/core/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { MutableRefObject, forwardRef, useCallback, useRef, useState } from "react"; +import { observer } from "mobx-react"; +//types +import { + TGroupedIssues, + IIssueDisplayProperties, + TSubGroupedIssues, + TIssueGroupByOptions, + TPaginationData, + TLoader, +} from "@plane/types"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; +// +import { KanbanIssueBlocksList } from "."; + +interface IKanbanGroup { + groupId: string; + groupedIssueIds: TGroupedIssues | TSubGroupedIssues; + displayProperties: IIssueDisplayProperties | undefined; + subGroupBy: TIssueGroupByOptions | undefined; + subGroupId: string; + loadMoreIssues: (groupId?: string, subGroupId?: string) => void; + getGroupIssueCount: ( + groupId: string | undefined, + subGroupId: string | undefined, + isSubGroupCumulative: boolean + ) => number | undefined; + getPaginationData: (groupId: string | undefined, subGroupId: string | undefined) => TPaginationData | undefined; + getIssueLoader: (groupId?: string | undefined, subGroupId?: string | undefined) => TLoader; + scrollableContainerRef?: MutableRefObject; +} + +// Loader components +const KanbanIssueBlockLoader = forwardRef((props, ref) => ( + +)); +KanbanIssueBlockLoader.displayName = "KanbanIssueBlockLoader"; + +export const KanbanGroup = observer((props: IKanbanGroup) => { + const { + groupId, + subGroupId, + subGroupBy, + displayProperties, + groupedIssueIds, + loadMoreIssues, + getGroupIssueCount, + getPaginationData, + getIssueLoader, + scrollableContainerRef, + } = props; + + // hooks + const [intersectionElement, setIntersectionElement] = useState(null); + const columnRef = useRef(null); + + const containerRef = subGroupBy && scrollableContainerRef ? scrollableContainerRef : columnRef; + + const loadMoreIssuesInThisGroup = useCallback(() => { + loadMoreIssues(groupId, subGroupId === "null" ? undefined : subGroupId); + }, [loadMoreIssues, groupId, subGroupId]); + + const isPaginating = !!getIssueLoader(groupId, subGroupId); + + useIntersectionObserver( + containerRef, + isPaginating ? null : intersectionElement, + loadMoreIssuesInThisGroup, + `0% 100% 100% 100%` + ); + + const isSubGroup = !!subGroupId && subGroupId !== "null"; + + const issueIds = isSubGroup + ? (groupedIssueIds as TSubGroupedIssues)?.[groupId]?.[subGroupId] ?? [] + : (groupedIssueIds as TGroupedIssues)?.[groupId] ?? []; + + const groupIssueCount = getGroupIssueCount(groupId, subGroupId, false) ?? 0; + const nextPageResults = getPaginationData(groupId, subGroupId)?.nextPageResults; + + const loadMore = isPaginating ? ( + + ) : ( +
+ {" "} + Load More ↓ +
+ ); + + const shouldLoadMore = nextPageResults === undefined ? issueIds?.length < groupIssueCount : !!nextPageResults; + + return ( +
+ + + {shouldLoadMore && (isSubGroup ? <>{loadMore} : )} +
+ ); +}); diff --git a/space/core/components/issues/issue-layouts/kanban/root.tsx b/space/core/components/issues/issue-layouts/kanban/root.tsx deleted file mode 100644 index 5f43fc42a0d..00000000000 --- a/space/core/components/issues/issue-layouts/kanban/root.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { observer } from "mobx-react"; -// mobx hook -import { TGroupedIssues } from "@plane/types"; -// hooks -import { useIssue } from "@/hooks/store"; -import { Column } from "./column"; - -type Props = { - anchor: string; -}; - -export const IssueKanbanLayoutRoot: FC = observer((props) => { - const { anchor } = props; - // store hooks - const { groupedIssueIds } = useIssue(); - - const groupedIssues = groupedIssueIds as TGroupedIssues | undefined; - - if (!groupedIssues) return <>; - - const issueGroupIds = Object.keys(groupedIssues); - - return ( -
- {issueGroupIds?.map((stateId) => { - const issueIds = groupedIssues[stateId]; - return ; - })} -
- ); -}); diff --git a/space/core/components/issues/issue-layouts/kanban/swimlanes.tsx b/space/core/components/issues/issue-layouts/kanban/swimlanes.tsx new file mode 100644 index 00000000000..902dff670d0 --- /dev/null +++ b/space/core/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -0,0 +1,294 @@ +import { MutableRefObject, useState } from "react"; +import { observer } from "mobx-react"; +// types +import { + GroupByColumnTypes, + IGroupByColumn, + TGroupedIssues, + IIssueDisplayProperties, + TSubGroupedIssues, + TIssueGroupByOptions, + TIssueOrderByOptions, + TPaginationData, + TLoader, +} from "@plane/types"; +// hooks +import { useMember, useModule, useStates, useLabel, useCycle } from "@/hooks/store"; +// +import { getGroupByColumns } from "../utils"; +import { KanBan } from "./default"; +import { HeaderGroupByCard } from "./headers/group-by-card"; +import { HeaderSubGroupByCard } from "./headers/sub-group-by-card"; + +export interface IKanBanSwimLanes { + groupedIssueIds: TGroupedIssues | TSubGroupedIssues; + displayProperties: IIssueDisplayProperties | undefined; + subGroupBy: TIssueGroupByOptions | undefined; + groupBy: TIssueGroupByOptions | undefined; + loadMoreIssues: (groupId?: string, subGroupId?: string) => void; + getGroupIssueCount: ( + groupId: string | undefined, + subGroupId: string | undefined, + isSubGroupCumulative: boolean + ) => number | undefined; + getPaginationData: (groupId: string | undefined, subGroupId: string | undefined) => TPaginationData | undefined; + getIssueLoader: (groupId?: string | undefined, subGroupId?: string | undefined) => TLoader; + showEmptyGroup: boolean; + scrollableContainerRef?: MutableRefObject; + orderBy: TIssueOrderByOptions | undefined; +} + +export const KanBanSwimLanes: React.FC = observer((props) => { + const { + groupedIssueIds, + displayProperties, + subGroupBy, + groupBy, + orderBy, + loadMoreIssues, + getGroupIssueCount, + getPaginationData, + getIssueLoader, + showEmptyGroup, + scrollableContainerRef, + } = props; + + const member = useMember(); + const label = useLabel(); + const cycle = useCycle(); + const modules = useModule(); + const state = useStates(); + + const groupByList = getGroupByColumns(groupBy as GroupByColumnTypes, cycle, modules, label, state, member); + const subGroupByList = getGroupByColumns(subGroupBy as GroupByColumnTypes, cycle, modules, label, state, member); + + if (!groupByList || !subGroupByList) return null; + + return ( +
+
+ +
+ + {subGroupBy && ( + + )} +
+ ); +}); + +interface ISubGroupSwimlaneHeader { + subGroupBy: TIssueGroupByOptions | undefined; + groupBy: TIssueGroupByOptions | undefined; + groupList: IGroupByColumn[]; + showEmptyGroup: boolean; + getGroupIssueCount: ( + groupId: string | undefined, + subGroupId: string | undefined, + isSubGroupCumulative: boolean + ) => number | undefined; +} + +const visibilitySubGroupByGroupCount = (subGroupIssueCount: number, showEmptyGroup: boolean): boolean => { + let subGroupHeaderVisibility = true; + + if (showEmptyGroup) subGroupHeaderVisibility = true; + else { + if (subGroupIssueCount > 0) subGroupHeaderVisibility = true; + else subGroupHeaderVisibility = false; + } + + return subGroupHeaderVisibility; +}; + +const SubGroupSwimlaneHeader: React.FC = observer( + ({ subGroupBy, groupBy, groupList, showEmptyGroup, getGroupIssueCount }) => ( +
+ {groupList && + groupList.length > 0 && + groupList.map((group: IGroupByColumn) => { + const groupCount = getGroupIssueCount(group.id, undefined, false) ?? 0; + + const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup); + + if (subGroupByVisibilityToggle === false) return <>; + + return ( +
+ +
+ ); + })} +
+ ) +); + +interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { + groupedIssueIds: TGroupedIssues | TSubGroupedIssues; + showEmptyGroup: boolean; + displayProperties: IIssueDisplayProperties | undefined; + orderBy: TIssueOrderByOptions | undefined; + getGroupIssueCount: ( + groupId: string | undefined, + subGroupId: string | undefined, + isSubGroupCumulative: boolean + ) => number | undefined; + getPaginationData: (groupId: string | undefined, subGroupId: string | undefined) => TPaginationData | undefined; + getIssueLoader: (groupId?: string | undefined, subGroupId?: string | undefined) => TLoader; + scrollableContainerRef?: MutableRefObject; + loadMoreIssues: (groupId?: string, subGroupId?: string) => void; +} + +const SubGroupSwimlane: React.FC = observer((props) => { + const { + groupedIssueIds, + subGroupBy, + groupBy, + groupList, + displayProperties, + loadMoreIssues, + getGroupIssueCount, + getPaginationData, + getIssueLoader, + showEmptyGroup, + scrollableContainerRef, + } = props; + + return ( +
+ {groupList && + groupList.length > 0 && + groupList.map((group: IGroupByColumn) => ( + + ))} +
+ ); +}); + +interface ISubGroup { + groupedIssueIds: TGroupedIssues | TSubGroupedIssues; + showEmptyGroup: boolean; + displayProperties: IIssueDisplayProperties | undefined; + groupBy: TIssueGroupByOptions | undefined; + subGroupBy: TIssueGroupByOptions | undefined; + group: IGroupByColumn; + getGroupIssueCount: ( + groupId: string | undefined, + subGroupId: string | undefined, + isSubGroupCumulative: boolean + ) => number | undefined; + getPaginationData: (groupId: string | undefined, subGroupId: string | undefined) => TPaginationData | undefined; + getIssueLoader: (groupId?: string | undefined, subGroupId?: string | undefined) => TLoader; + scrollableContainerRef?: MutableRefObject; + loadMoreIssues: (groupId?: string, subGroupId?: string) => void; +} + +const SubGroup: React.FC = observer((props) => { + const { + groupedIssueIds, + subGroupBy, + groupBy, + group, + displayProperties, + loadMoreIssues, + getGroupIssueCount, + getPaginationData, + getIssueLoader, + showEmptyGroup, + scrollableContainerRef, + } = props; + + const [isExpanded, setIsExpanded] = useState(true); + + const toggleExpanded = () => { + setIsExpanded((prevState) => !prevState); + }; + + const visibilitySubGroupBy = ( + _list: IGroupByColumn, + subGroupCount: number + ): { showGroup: boolean; showIssues: boolean } => { + const subGroupVisibility = { + showGroup: true, + showIssues: true, + }; + if (showEmptyGroup) subGroupVisibility.showGroup = true; + else { + if (subGroupCount > 0) subGroupVisibility.showGroup = true; + else subGroupVisibility.showGroup = false; + } + return subGroupVisibility; + }; + + const issueCount = getGroupIssueCount(undefined, group.id, true) ?? 0; + const subGroupByVisibilityToggle = visibilitySubGroupBy(group, issueCount); + if (subGroupByVisibilityToggle.showGroup === false) return <>; + + return ( + <> +
+
+
+ +
+
+ + {subGroupByVisibilityToggle.showIssues && isExpanded && ( +
+ +
+ )} +
+ + ); +}); diff --git a/space/core/components/issues/issue-layouts/list/base-list-root.tsx b/space/core/components/issues/issue-layouts/list/base-list-root.tsx new file mode 100644 index 00000000000..ad0952b299a --- /dev/null +++ b/space/core/components/issues/issue-layouts/list/base-list-root.tsx @@ -0,0 +1,59 @@ +import { useCallback } from "react"; +import { observer } from "mobx-react"; +// types +import { IIssueDisplayProperties, TGroupedIssues } from "@plane/types"; +// constants +// components +import { IssueLayoutHOC } from "@/components/issues/issue-layouts/issue-layout-HOC"; +// hooks +import { useIssue } from "@/hooks/store"; +import { List } from "./default"; + +type Props = { + anchor: string; +}; + +export const IssuesListLayoutRoot = observer((props: Props) => { + const { anchor } = props; + // store hooks + const { + groupedIssueIds: storeGroupedIssueIds, + fetchNextPublicIssues, + getGroupIssueCount, + getPaginationData, + getIssueLoader, + } = useIssue(); + + const groupedIssueIds = storeGroupedIssueIds as TGroupedIssues | undefined; + // auth + const displayProperties: IIssueDisplayProperties = { + key: true, + state: true, + priority: true, + due_date: true, + labels: true, + }; + const loadMoreIssues = useCallback( + (groupId?: string) => { + fetchNextPublicIssues(anchor, groupId); + }, + [fetchNextPublicIssues] + ); + + return ( + +
+ +
+
+ ); +}); diff --git a/space/core/components/issues/issue-layouts/list/block.tsx b/space/core/components/issues/issue-layouts/list/block.tsx index 905db2cc481..39a298448ac 100644 --- a/space/core/components/issues/issue-layouts/list/block.tsx +++ b/space/core/components/issues/issue-layouts/list/block.tsx @@ -1,88 +1,90 @@ "use client"; -import { FC } from "react"; + +import { useRef } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -import { useSearchParams } from "next/navigation"; -// components -import { IssueBlockDueDate, IssueBlockLabels, IssueBlockPriority, IssueBlockState } from "@/components/issues"; +import { useParams, useSearchParams } from "next/navigation"; +// types +import { cn } from "@plane/editor"; +import { IIssueDisplayProperties } from "@plane/types"; +import { Tooltip } from "@plane/ui"; // helpers import { queryParamGenerator } from "@/helpers/query-param-generator"; -// hook -import { useIssue, useIssueDetails, usePublish } from "@/hooks/store"; +// hooks +import { useIssueDetails, usePublish } from "@/hooks/store"; +// +import { IssueProperties } from "../properties/all-properties"; -type IssueListBlockProps = { - anchor: string; +interface IssueBlockProps { issueId: string; -}; + groupId: string; + displayProperties: IIssueDisplayProperties | undefined; +} -export const IssueListLayoutBlock: FC = observer((props) => { - const { anchor, issueId } = props; - const { getIssueById } = useIssue(); - // query params +export const IssueBlock = observer((props: IssueBlockProps) => { + const { anchor } = useParams(); + const { issueId, displayProperties } = props; const searchParams = useSearchParams(); - const board = searchParams.get("board") || undefined; - const state = searchParams.get("state") || undefined; - const priority = searchParams.get("priority") || undefined; - const labels = searchParams.get("labels") || undefined; - // store hooks - const { setPeekId } = useIssueDetails(); - const { project_details } = usePublish(anchor); + // query params + const board = searchParams.get("board"); + // ref + const issueRef = useRef(null); + // hooks + const { project_details } = usePublish(anchor.toString()); + const { getIsIssuePeeked, setPeekId, getIssueById } = useIssueDetails(); - const { queryParam } = queryParamGenerator({ board, peekId: issueId, priority, state, labels }); - const handleBlockClick = () => { + const handleIssuePeekOverview = () => { setPeekId(issueId); }; + const { queryParam } = queryParamGenerator(board ? { board, peekId: issueId } : { peekId: issueId }); + const issue = getIssueById(issueId); - if (!issue) return <>; + if (!issue) return null; + + const projectIdentifier = project_details?.identifier; return ( - -
- {/* id */} -
- {project_details?.identifier}-{issue?.sequence_id} -
- {/* name */} -
- {issue.name} -
-
- -
- {/* priority */} - {issue?.priority && ( -
- -
- )} - - {/* state */} - {issue?.state_id && ( -
- -
- )} - - {/* labels */} - {issue?.label_ids && issue?.label_ids.length > 0 && ( -
- +
+
+
+ {displayProperties && displayProperties?.key && ( +
+ {projectIdentifier}-{issue.sequence_id} +
+ )}
- )} - {/* due date */} - {issue?.target_date && ( -
- -
- )} + + +

{issue.name}

+
+ +
+
+
+
- +
); }); diff --git a/space/core/components/issues/issue-layouts/list/blocks-list.tsx b/space/core/components/issues/issue-layouts/list/blocks-list.tsx new file mode 100644 index 00000000000..19d690759c8 --- /dev/null +++ b/space/core/components/issues/issue-layouts/list/blocks-list.tsx @@ -0,0 +1,25 @@ +import { FC, MutableRefObject } from "react"; +// types +import { IIssueDisplayProperties, TGroupedIssues } from "@plane/types"; +import { IssueBlock } from "./block"; + +interface Props { + issueIds: TGroupedIssues | any; + groupId: string; + displayProperties: IIssueDisplayProperties | undefined; + containerRef: MutableRefObject; +} + +export const IssueBlocksList: FC = (props) => { + const { issueIds, groupId, displayProperties } = props; + + return ( +
+ {issueIds && + issueIds.length > 0 && + issueIds.map((issueId: string) => ( + + ))} +
+ ); +}; diff --git a/space/core/components/issues/issue-layouts/list/default.tsx b/space/core/components/issues/issue-layouts/list/default.tsx new file mode 100644 index 00000000000..0f61dcf1f2a --- /dev/null +++ b/space/core/components/issues/issue-layouts/list/default.tsx @@ -0,0 +1,86 @@ +import { useRef } from "react"; +import { observer } from "mobx-react"; +// types +import { + GroupByColumnTypes, + TGroupedIssues, + IIssueDisplayProperties, + TIssueGroupByOptions, + IGroupByColumn, + TPaginationData, + TLoader, +} from "@plane/types"; +// hooks +import { useMember, useModule, useStates, useLabel, useCycle } from "@/hooks/store"; +// +import { getGroupByColumns } from "../utils"; +import { ListGroup } from "./list-group"; + +export interface IList { + groupedIssueIds: TGroupedIssues; + groupBy: TIssueGroupByOptions | undefined; + displayProperties: IIssueDisplayProperties | undefined; + showEmptyGroup?: boolean; + loadMoreIssues: (groupId?: string) => void; + getGroupIssueCount: ( + groupId: string | undefined, + subGroupId: string | undefined, + isSubGroupCumulative: boolean + ) => number | undefined; + getPaginationData: (groupId: string | undefined, subGroupId: string | undefined) => TPaginationData | undefined; + getIssueLoader: (groupId?: string | undefined, subGroupId?: string | undefined) => TLoader; +} + +export const List: React.FC = observer((props) => { + const { + groupedIssueIds, + groupBy, + displayProperties, + showEmptyGroup, + loadMoreIssues, + getGroupIssueCount, + getPaginationData, + getIssueLoader, + } = props; + + const containerRef = useRef(null); + + const member = useMember(); + const label = useLabel(); + const cycle = useCycle(); + const modules = useModule(); + const state = useStates(); + + const groupList = getGroupByColumns(groupBy as GroupByColumnTypes, cycle, modules, label, state, member, true); + + if (!groupList) return null; + + return ( +
+ {groupList && ( + <> +
+ {groupList.map((group: IGroupByColumn) => ( + + ))} +
+ + )} +
+ ); +}); diff --git a/space/core/components/issues/issue-layouts/list/group.tsx b/space/core/components/issues/issue-layouts/list/group.tsx deleted file mode 100644 index 3e2d48385d4..00000000000 --- a/space/core/components/issues/issue-layouts/list/group.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useCallback } from "react"; -import { observer } from "mobx-react"; -// hooks -import { useIssue } from "@/hooks/store"; -// components -import { IssueListLayoutBlock } from "./block"; -import { IssueListLayoutHeader } from "./header"; - -type Props = { - anchor: string; - stateId: string; - issueIds: string[]; -}; - -export const Group = observer((props: Props) => { - const { anchor, stateId, issueIds } = props; - - const { fetchNextPublicIssues, getPaginationData, getIssueLoader, getGroupIssueCount } = useIssue(); - - const loadMoreIssuesInThisGroup = useCallback(() => { - fetchNextPublicIssues(anchor, stateId); - }, [stateId]); - - const isPaginating = !!getIssueLoader(stateId); - const nextPageResults = getPaginationData(stateId, undefined)?.nextPageResults; - - const groupIssueCount = getGroupIssueCount(stateId, undefined, false); - const shouldLoadMore = - nextPageResults === undefined && groupIssueCount !== undefined - ? issueIds?.length < groupIssueCount - : !!nextPageResults; - - return ( -
- - {issueIds && issueIds.length > 0 ? ( -
- {issueIds.map((issueId) => ( - - ))} - {isPaginating ? ( -
- ) : ( - shouldLoadMore && ( -
- Load More ↓ -
- ) - )} -
- ) : ( -
No issues.
- )} -
- ); -}); diff --git a/space/core/components/issues/issue-layouts/list/header.tsx b/space/core/components/issues/issue-layouts/list/header.tsx deleted file mode 100644 index 73625da9e0d..00000000000 --- a/space/core/components/issues/issue-layouts/list/header.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client"; - -import React from "react"; -import { observer } from "mobx-react"; -// ui -import { StateGroupIcon } from "@plane/ui"; -// hooks -import { useIssue, useStates } from "@/hooks/store"; - -type Props = { - stateId: string; -}; - -export const IssueListLayoutHeader: React.FC = observer((props) => { - const { stateId } = props; - - const { getStateById } = useStates(); - const { getGroupIssueCount } = useIssue(); - - const state = getStateById(stateId); - - return ( -
-
- -
-
{state?.name}
-
- {getGroupIssueCount(stateId, undefined, false) ?? 0} -
-
- ); -}); diff --git a/space/core/components/issues/issue-layouts/list/headers/group-by-card.tsx b/space/core/components/issues/issue-layouts/list/headers/group-by-card.tsx new file mode 100644 index 00000000000..e92e9daeda4 --- /dev/null +++ b/space/core/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { observer } from "mobx-react"; +import { CircleDashed } from "lucide-react"; + +interface IHeaderGroupByCard { + groupID: string; + icon?: React.ReactNode; + title: string; + count: number; + toggleListGroup: (id: string) => void; +} + +export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => { + const { groupID, icon, title, count, toggleListGroup } = props; + + return ( + <> +
toggleListGroup(groupID)} + > +
+ {icon ?? } +
+ +
+
{title}
+
{count || 0}
+
+
+ + ); +}); diff --git a/space/core/components/issues/issue-layouts/list/index.ts b/space/core/components/issues/issue-layouts/list/index.ts index 62874fbda4e..068da7360b3 100644 --- a/space/core/components/issues/issue-layouts/list/index.ts +++ b/space/core/components/issues/issue-layouts/list/index.ts @@ -1,3 +1,2 @@ export * from "./block"; -export * from "./header"; -export * from "./root"; +export * from "./blocks-list"; diff --git a/space/core/components/issues/issue-layouts/list/list-group.tsx b/space/core/components/issues/issue-layouts/list/list-group.tsx new file mode 100644 index 00000000000..9dd2fdcec95 --- /dev/null +++ b/space/core/components/issues/issue-layouts/list/list-group.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { Fragment, MutableRefObject, forwardRef, useRef, useState } from "react"; +import { observer } from "mobx-react"; +import { cn } from "@plane/editor"; +// plane +import { IGroupByColumn, TIssueGroupByOptions, IIssueDisplayProperties, TPaginationData, TLoader } from "@plane/types"; +// hooks +import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; +// +import { IssueBlocksList } from "./blocks-list"; +import { HeaderGroupByCard } from "./headers/group-by-card"; + +interface Props { + groupIssueIds: string[] | undefined; + group: IGroupByColumn; + groupBy: TIssueGroupByOptions | undefined; + displayProperties: IIssueDisplayProperties | undefined; + containerRef: MutableRefObject; + showEmptyGroup?: boolean; + loadMoreIssues: (groupId?: string) => void; + getGroupIssueCount: ( + groupId: string | undefined, + subGroupId: string | undefined, + isSubGroupCumulative: boolean + ) => number | undefined; + getPaginationData: (groupId: string | undefined, subGroupId: string | undefined) => TPaginationData | undefined; + getIssueLoader: (groupId?: string | undefined, subGroupId?: string | undefined) => TLoader; +} + +// List loader component +const ListLoaderItemRow = forwardRef((props, ref) => ( +
+
+ + +
+
+ {[...Array(6)].map((_, index) => ( + + + + ))} +
+
+)); +ListLoaderItemRow.displayName = "ListLoaderItemRow"; + +export const ListGroup = observer((props: Props) => { + const { + groupIssueIds = [], + group, + groupBy, + displayProperties, + containerRef, + showEmptyGroup, + loadMoreIssues, + getGroupIssueCount, + getPaginationData, + getIssueLoader, + } = props; + const [isExpanded, setIsExpanded] = useState(true); + const groupRef = useRef(null); + + const [intersectionElement, setIntersectionElement] = useState(null); + + const groupIssueCount = getGroupIssueCount(group.id, undefined, false) ?? 0; + const nextPageResults = getPaginationData(group.id, undefined)?.nextPageResults; + const isPaginating = !!getIssueLoader(group.id); + + useIntersectionObserver(containerRef, isPaginating ? null : intersectionElement, loadMoreIssues, `100% 0% 100% 0%`); + + const shouldLoadMore = + nextPageResults === undefined && groupIssueCount !== undefined && groupIssueIds + ? groupIssueIds.length < groupIssueCount + : !!nextPageResults; + + const loadMore = isPaginating ? ( + + ) : ( +
loadMoreIssues(group.id)} + > + Load More ↓ +
+ ); + + const validateEmptyIssueGroups = (issueCount: number = 0) => { + if (!showEmptyGroup && issueCount <= 0) return false; + return true; + }; + + const toggleListGroup = () => { + setIsExpanded((prevState) => !prevState); + }; + + const shouldExpand = (!!groupIssueCount && isExpanded) || !groupBy; + + return validateEmptyIssueGroups(groupIssueCount) ? ( +
+
+ +
+ {shouldExpand && ( +
+ {groupIssueIds && ( + + )} + + {shouldLoadMore && (groupBy ? <>{loadMore} : )} +
+ )} +
+ ) : null; +}); diff --git a/space/core/components/issues/issue-layouts/list/root.tsx b/space/core/components/issues/issue-layouts/list/root.tsx deleted file mode 100644 index 215abe86f48..00000000000 --- a/space/core/components/issues/issue-layouts/list/root.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; -import { FC } from "react"; -import { observer } from "mobx-react"; -// types -import { TGroupedIssues } from "@plane/types"; -// mobx hook -import { useIssue } from "@/hooks/store"; -import { Group } from "./group"; - -type Props = { - anchor: string; -}; - -export const IssuesListLayoutRoot: FC = observer((props) => { - const { anchor } = props; - // store hooks - const { groupedIssueIds } = useIssue(); - - const groupedIssues = groupedIssueIds as TGroupedIssues | undefined; - - if (!groupedIssues) return <>; - - const issueGroupIds = Object.keys(groupedIssues); - - return ( - <> - {issueGroupIds?.map((stateId) => { - const issueIds = groupedIssues[stateId]; - - return ; - })} - - ); -}); diff --git a/space/core/components/issues/issue-layouts/properties/all-properties.tsx b/space/core/components/issues/issue-layouts/properties/all-properties.tsx new file mode 100644 index 00000000000..3c596cb53c9 --- /dev/null +++ b/space/core/components/issues/issue-layouts/properties/all-properties.tsx @@ -0,0 +1,183 @@ +"use client"; + +import { observer } from "mobx-react"; +import { Layers, Link, Paperclip } from "lucide-react"; +// types +import { cn } from "@plane/editor"; +import { IIssueDisplayProperties } from "@plane/types"; +import { Tooltip } from "@plane/ui"; +// ui +// components +import { + IssueBlockDate, + IssueBlockLabels, + IssueBlockPriority, + IssueBlockState, + IssueBlockMembers, + IssueBlockModules, + IssueBlockCycle, +} from "@/components/issues"; +import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/with-display-properties-HOC"; +// helpers +import { getDate } from "@/helpers/date-time.helper"; +//// hooks +import { IIssue } from "@/types/issue"; + +export interface IIssueProperties { + issue: IIssue; + displayProperties: IIssueDisplayProperties | undefined; + className: string; +} + +export const IssueProperties: React.FC = observer((props) => { + const { issue, displayProperties, className } = props; + + if (!displayProperties || !issue.project_id) return null; + + const minDate = getDate(issue.start_date); + minDate?.setDate(minDate.getDate()); + + const maxDate = getDate(issue.target_date); + maxDate?.setDate(maxDate.getDate()); + + return ( +
+ {/* basic properties */} + {/* state */} + {issue.state_id && ( + +
+ +
+
+ )} + + {/* priority */} + +
+ +
+
+ + {/* label */} + +
+ +
+
+ + {/* start date */} + {issue?.start_date && ( + +
+ +
+
+ )} + + {/* target/due date */} + {issue?.target_date && ( + +
+ +
+
+ )} + + {/* assignee */} + +
+ +
+
+ + {/* modules */} + {issue.module_ids && issue.module_ids.length > 0 && ( + +
+ +
+
+ )} + + {/* cycles */} + {issue.cycle_id && ( + +
+ +
+
+ )} + + {/* estimates */} + {/* {projectId && areEstimateEnabledByProjectId(projectId?.toString()) && ( + +
+ +
+
+ )} */} + + {/* extra render properties */} + {/* sub-issues */} + !!properties.sub_issue_count && !!issue.sub_issues_count} + > + +
+ +
{issue.sub_issues_count}
+
+
+
+ + {/* attachments */} + !!properties.attachment_count && !!issue.attachment_count} + > + +
+ +
{issue.attachment_count}
+
+
+
+ + {/* link */} + !!properties.link && !!issue.link_count} + > + +
+ +
{issue.link_count}
+
+
+
+
+ ); +}); diff --git a/space/core/components/issues/issue-layouts/properties/cycle.tsx b/space/core/components/issues/issue-layouts/properties/cycle.tsx new file mode 100644 index 00000000000..52c10578978 --- /dev/null +++ b/space/core/components/issues/issue-layouts/properties/cycle.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { observer } from "mobx-react"; +// ui +import { cn } from "@plane/editor"; +import { ContrastIcon, Tooltip } from "@plane/ui"; +//hooks +import { useCycle } from "@/hooks/store/use-cycle"; + +type Props = { + cycleId: string | undefined; + shouldShowBorder?: boolean; +}; + +export const IssueBlockCycle = observer(({ cycleId, shouldShowBorder = true }: Props) => { + const { getCycleById } = useCycle(); + + const cycle = getCycleById(cycleId); + + return ( + +
+
+ +
{cycle?.name ?? "No Cycle"}
+
+
+
+ ); +}); diff --git a/space/core/components/issues/issue-layouts/properties/due-date.tsx b/space/core/components/issues/issue-layouts/properties/due-date.tsx index 30a441bfeab..fd4875154b0 100644 --- a/space/core/components/issues/issue-layouts/properties/due-date.tsx +++ b/space/core/components/issues/issue-layouts/properties/due-date.tsx @@ -2,6 +2,7 @@ import { observer } from "mobx-react"; import { CalendarCheck2 } from "lucide-react"; +import { Tooltip } from "@plane/ui"; // helpers import { cn } from "@/helpers/common.helper"; import { renderFormattedDate } from "@/helpers/date-time.helper"; @@ -10,27 +11,31 @@ import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; import { useStates } from "@/hooks/store"; type Props = { - due_date: string; + due_date: string | undefined; stateId: string | undefined; + shouldHighLight?: boolean; + shouldShowBorder?: boolean; }; -export const IssueBlockDueDate = observer((props: Props) => { - const { due_date, stateId } = props; +export const IssueBlockDate = observer((props: Props) => { + const { due_date, stateId, shouldHighLight = true, shouldShowBorder = true } = props; const { getStateById } = useStates(); const state = getStateById(stateId); + const formattedDate = renderFormattedDate(due_date); + return ( -
- - {renderFormattedDate(due_date)} -
+ +
+ + {formattedDate ? formattedDate : "No Date"} +
+
); }); diff --git a/space/core/components/issues/issue-layouts/properties/index.ts b/space/core/components/issues/issue-layouts/properties/index.ts index de78f996697..44df8ad47a4 100644 --- a/space/core/components/issues/issue-layouts/properties/index.ts +++ b/space/core/components/issues/issue-layouts/properties/index.ts @@ -2,3 +2,7 @@ export * from "./due-date"; export * from "./labels"; export * from "./priority"; export * from "./state"; +export * from "./cycle"; +export * from "./member"; +export * from "./modules"; +export * from "./all-properties"; diff --git a/space/core/components/issues/issue-layouts/properties/labels.tsx b/space/core/components/issues/issue-layouts/properties/labels.tsx index be920dbab40..e124663e8d0 100644 --- a/space/core/components/issues/issue-layouts/properties/labels.tsx +++ b/space/core/components/issues/issue-layouts/properties/labels.tsx @@ -1,40 +1,69 @@ "use client"; import { observer } from "mobx-react"; +import { Tags } from "lucide-react"; import { Tooltip } from "@plane/ui"; import { useLabel } from "@/hooks/store"; type Props = { labelIds: string[]; + shouldShowLabel?: boolean; }; -export const IssueBlockLabels = observer(({ labelIds }: Props) => { +export const IssueBlockLabels = observer(({ labelIds, shouldShowLabel = false }: Props) => { const { getLabelsByIds } = useLabel(); const labels = getLabelsByIds(labelIds); - const labelsString = labels.map((label) => label.name).join(", "); + const labelsString = labels.length > 0 ? labels.map((label) => label.name).join(", ") : "No Labels"; - return ( -
- {labels.length === 1 ? ( + if (labels.length <= 0) + return ( +
-
-
-
{labels[0].name}
-
+ + {shouldShowLabel && No Labels}
+ + ); + + return ( +
+ {labels.length <= 2 ? ( + <> + {labels.map((label) => ( + +
+
+ +
{label?.name}
+
+
+
+ ))} + ) : ( - -
-
-
{labels.length} Labels
+
+ +
+ + {`${labels.length} Labels`}
-
- + +
)}
); diff --git a/space/core/components/issues/issue-layouts/properties/member.tsx b/space/core/components/issues/issue-layouts/properties/member.tsx new file mode 100644 index 00000000000..bac44d52322 --- /dev/null +++ b/space/core/components/issues/issue-layouts/properties/member.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { observer } from "mobx-react"; +// icons +import { LucideIcon, Users } from "lucide-react"; +// ui +import { cn } from "@plane/editor"; +import { Avatar, AvatarGroup } from "@plane/ui"; +// hooks +import { useMember } from "@/hooks/store/use-member"; +// +import { TPublicMember } from "@/types/member"; + +type Props = { + memberIds: string[]; + shouldShowBorder?: boolean; +}; + +type AvatarProps = { + showTooltip: boolean; + members: TPublicMember[]; + icon?: LucideIcon; +}; + +export const ButtonAvatars: React.FC = observer((props: AvatarProps) => { + const { showTooltip, members, icon: Icon } = props; + + if (Array.isArray(members)) { + if (members.length > 1) { + return ( + + {members.map((member) => { + if (!member) return; + return ; + })} + + ); + } else if (members.length === 1) { + return ( + + ); + } + } + + return Icon ? : ; +}); + +export const IssueBlockMembers = observer(({ memberIds, shouldShowBorder = true }: Props) => { + const { getMembersByIds } = useMember(); + + const members = getMembersByIds(memberIds); + + return ( +
+
+
+ + {!shouldShowBorder && members.length <= 1 && ( + {members?.[0]?.member__display_name ?? "No Assignees"} + )} +
+
+
+ ); +}); diff --git a/space/core/components/issues/issue-layouts/properties/modules.tsx b/space/core/components/issues/issue-layouts/properties/modules.tsx new file mode 100644 index 00000000000..eaa30d9908b --- /dev/null +++ b/space/core/components/issues/issue-layouts/properties/modules.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { observer } from "mobx-react"; +// planes +import { cn } from "@plane/editor"; +import { DiceIcon, Tooltip } from "@plane/ui"; +// hooks +import { useModule } from "@/hooks/store/use-module"; + +type Props = { + moduleIds: string[] | undefined; + shouldShowBorder?: boolean; +}; + +export const IssueBlockModules = observer(({ moduleIds, shouldShowBorder = true }: Props) => { + const { getModulesByIds } = useModule(); + + const modules = getModulesByIds(moduleIds ?? []); + + const modulesString = modules.map((module) => module.name).join(", "); + + return ( +
+ + {modules.length <= 1 ? ( +
+
+ +
{modules?.[0]?.name ?? "No Modules"}
+
+
+ ) : ( +
+
+
{modules.length} Modules
+
+
+ )} +
+
+ ); +}); diff --git a/space/core/components/issues/issue-layouts/properties/priority.tsx b/space/core/components/issues/issue-layouts/properties/priority.tsx index b91d56bb87b..efaa8ea36ac 100644 --- a/space/core/components/issues/issue-layouts/properties/priority.tsx +++ b/space/core/components/issues/issue-layouts/properties/priority.tsx @@ -2,17 +2,29 @@ // types import { TIssuePriorities } from "@plane/types"; +import { Tooltip } from "@plane/ui"; // constants import { issuePriorityFilter } from "@/constants/issue"; -export const IssueBlockPriority = ({ priority }: { priority: TIssuePriorities | null }) => { +export const IssueBlockPriority = ({ + priority, + shouldShowName = false, +}: { + priority: TIssuePriorities | null; + shouldShowName?: boolean; +}) => { const priority_detail = priority != null ? issuePriorityFilter(priority) : null; if (priority_detail === null) return <>; return ( -
- {priority_detail?.icon} -
+ +
+
+ {priority_detail?.icon} +
+ {shouldShowName && {priority_detail?.title}} +
+
); }; diff --git a/space/core/components/issues/issue-layouts/properties/state.tsx b/space/core/components/issues/issue-layouts/properties/state.tsx index 959c02ddba7..56a09bcd99f 100644 --- a/space/core/components/issues/issue-layouts/properties/state.tsx +++ b/space/core/components/issues/issue-layouts/properties/state.tsx @@ -2,26 +2,32 @@ import { observer } from "mobx-react"; // ui -import { StateGroupIcon } from "@plane/ui"; +import { cn } from "@plane/editor"; +import { StateGroupIcon, Tooltip } from "@plane/ui"; //hooks import { useStates } from "@/hooks/store"; type Props = { - stateId: string; + stateId: string | undefined; + shouldShowBorder?: boolean; }; -export const IssueBlockState = observer(({ stateId }: Props) => { +export const IssueBlockState = observer(({ stateId, shouldShowBorder = true }: Props) => { const { getStateById } = useStates(); const state = getStateById(stateId); - if (!state) return <>; - return ( -
-
- -
{state?.name}
+ +
+
+ +
{state?.name ?? "State"}
+
-
+ ); }); diff --git a/space/core/components/issues/issue-layouts/utils.tsx b/space/core/components/issues/issue-layouts/utils.tsx new file mode 100644 index 00000000000..edb67b4f711 --- /dev/null +++ b/space/core/components/issues/issue-layouts/utils.tsx @@ -0,0 +1,240 @@ +"use client"; + +import isNil from "lodash/isNil"; +import { ContrastIcon } from "lucide-react"; +// types +import { + GroupByColumnTypes, + IGroupByColumn, + TCycleGroups, + IIssueDisplayProperties, + TGroupedIssues, +} from "@plane/types"; +// ui +import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui"; +// components +// constants +import { ISSUE_PRIORITIES } from "@/constants/issue"; +// stores +import { ICycleStore } from "@/store/cycle.store"; +import { IIssueLabelStore } from "@/store/label.store"; +import { IIssueMemberStore } from "@/store/members.store"; +import { IIssueModuleStore } from "@/store/module.store"; +import { IStateStore } from "@/store/state.store"; + +export const HIGHLIGHT_CLASS = "highlight"; +export const HIGHLIGHT_WITH_LINE = "highlight-with-line"; + +export const getGroupByColumns = ( + groupBy: GroupByColumnTypes | null, + cycle: ICycleStore, + module: IIssueModuleStore, + label: IIssueLabelStore, + projectState: IStateStore, + member: IIssueMemberStore, + includeNone?: boolean +): IGroupByColumn[] | undefined => { + switch (groupBy) { + case "cycle": + return getCycleColumns(cycle); + case "module": + return getModuleColumns(module); + case "state": + return getStateColumns(projectState); + case "priority": + return getPriorityColumns(); + case "labels": + return getLabelsColumns(label) as any; + case "assignees": + return getAssigneeColumns(member) as any; + case "created_by": + return getCreatedByColumns(member) as any; + default: + if (includeNone) return [{ id: `All Issues`, name: `All Issues`, payload: {}, icon: undefined }]; + } +}; + +const getCycleColumns = (cycleStore: ICycleStore): IGroupByColumn[] | undefined => { + const { cycles } = cycleStore; + + if (!cycles) return; + + const cycleGroups: IGroupByColumn[] = []; + + cycles.map((cycle) => { + if (cycle) { + const cycleStatus = cycle?.status ? (cycle.status.toLocaleLowerCase() as TCycleGroups) : "draft"; + cycleGroups.push({ + id: cycle.id, + name: cycle.name, + icon: , + payload: { cycle_id: cycle.id }, + }); + } + }); + cycleGroups.push({ + id: "None", + name: "None", + icon: , + payload: { cycle_id: null }, + }); + + return cycleGroups; +}; + +const getModuleColumns = (moduleStore: IIssueModuleStore): IGroupByColumn[] | undefined => { + const { modules } = moduleStore; + + if (!modules) return; + + const moduleGroups: IGroupByColumn[] = []; + + modules.map((moduleInfo) => { + if (moduleInfo) + moduleGroups.push({ + id: moduleInfo.id, + name: moduleInfo.name, + icon: , + payload: { module_ids: [moduleInfo.id] }, + }); + }) as any; + moduleGroups.push({ + id: "None", + name: "None", + icon: , + payload: { module_ids: [] }, + }); + + return moduleGroups as any; +}; + +const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefined => { + const { states } = projectState; + if (!states) return; + + return states.map((state) => ({ + id: state.id, + name: state.name, + icon: ( +
+ +
+ ), + payload: { state_id: state.id }, + })) as any; +}; + +const getPriorityColumns = () => { + const priorities = ISSUE_PRIORITIES; + + return priorities.map((priority) => ({ + id: priority.key, + name: priority.title, + icon: , + payload: { priority: priority.key }, + })); +}; + +const getLabelsColumns = (label: IIssueLabelStore) => { + const { labels: storeLabels } = label; + + if (!storeLabels) return; + + const labels = [...storeLabels, { id: "None", name: "None", color: "#666" }]; + + return labels.map((label) => ({ + id: label.id, + name: label.name, + icon: ( +
+ ), + payload: label?.id === "None" ? {} : { label_ids: [label.id] }, + })); +}; + +const getAssigneeColumns = (member: IIssueMemberStore) => { + const { members } = member; + + if (!members) return; + + const assigneeColumns: any = members.map((member) => ({ + id: member.id, + name: member?.member__display_name || "", + icon: , + payload: { assignee_ids: [member.id] }, + })); + + assigneeColumns.push({ id: "None", name: "None", icon: , payload: {} }); + + return assigneeColumns; +}; + +const getCreatedByColumns = (member: IIssueMemberStore) => { + const { members } = member; + + if (!members) return; + + return members.map((member) => ({ + id: member.id, + name: member?.member__display_name || "", + icon: , + payload: {}, + })); +}; + +export const getDisplayPropertiesCount = ( + displayProperties: IIssueDisplayProperties, + ignoreFields?: (keyof IIssueDisplayProperties)[] +) => { + const propertyKeys = Object.keys(displayProperties) as (keyof IIssueDisplayProperties)[]; + + let count = 0; + + for (const propertyKey of propertyKeys) { + if (ignoreFields && ignoreFields.includes(propertyKey)) continue; + if (displayProperties[propertyKey]) count++; + } + + return count; +}; + +export const getIssueBlockId = ( + issueId: string | undefined, + groupId: string | undefined, + subGroupId?: string | undefined +) => `issue_${issueId}_${groupId}_${subGroupId}`; + +/** + * returns empty Array if groupId is None + * @param groupId + * @returns + */ +export const getGroupId = (groupId: string) => { + if (groupId === "None") return []; + return [groupId]; +}; + +/** + * method that removes Null or undefined Keys from object + * @param obj + * @returns + */ +export const removeNillKeys = (obj: T) => + Object.fromEntries(Object.entries(obj ?? {}).filter(([key, value]) => key && !isNil(value))); + +/** + * This Method returns if the the grouped values are subGrouped + * @param groupedIssueIds + * @returns + */ +export const isSubGrouped = (groupedIssueIds: TGroupedIssues) => { + if (!groupedIssueIds || Array.isArray(groupedIssueIds)) { + return false; + } + + if (Array.isArray(groupedIssueIds[Object.keys(groupedIssueIds)[0]])) { + return false; + } + + return true; +}; diff --git a/space/core/components/issues/issue-layouts/with-display-properties-HOC.tsx b/space/core/components/issues/issue-layouts/with-display-properties-HOC.tsx new file mode 100644 index 00000000000..51ce71a7723 --- /dev/null +++ b/space/core/components/issues/issue-layouts/with-display-properties-HOC.tsx @@ -0,0 +1,26 @@ +import { ReactNode } from "react"; +import { observer } from "mobx-react"; +import { IIssueDisplayProperties } from "@plane/types"; + +interface IWithDisplayPropertiesHOC { + displayProperties: IIssueDisplayProperties; + shouldRenderProperty?: (displayProperties: IIssueDisplayProperties) => boolean; + displayPropertyKey: keyof IIssueDisplayProperties | (keyof IIssueDisplayProperties)[]; + children: ReactNode; +} + +export const WithDisplayPropertiesHOC = observer( + ({ displayProperties, shouldRenderProperty, displayPropertyKey, children }: IWithDisplayPropertiesHOC) => { + let shouldDisplayPropertyFromFilters = false; + if (Array.isArray(displayPropertyKey)) + shouldDisplayPropertyFromFilters = displayPropertyKey.every((key) => !!displayProperties[key]); + else shouldDisplayPropertyFromFilters = !!displayProperties[displayPropertyKey]; + + const renderProperty = + shouldDisplayPropertyFromFilters && (shouldRenderProperty ? shouldRenderProperty(displayProperties) : true); + + if (!renderProperty) return null; + + return <>{children}; + } +); diff --git a/space/core/components/issues/peek-overview/layout.tsx b/space/core/components/issues/peek-overview/layout.tsx index fa138382759..d91d62a32a9 100644 --- a/space/core/components/issues/peek-overview/layout.tsx +++ b/space/core/components/issues/peek-overview/layout.tsx @@ -6,16 +6,17 @@ import { useRouter, useSearchParams } from "next/navigation"; import { Dialog, Transition } from "@headlessui/react"; // components import { FullScreenPeekView, SidePeekView } from "@/components/issues/peek-overview"; -// store +// hooks import { useIssue, useIssueDetails } from "@/hooks/store"; type TIssuePeekOverview = { anchor: string; peekId: string; + handlePeekClose?: () => void; }; export const IssuePeekOverview: FC = observer((props) => { - const { anchor, peekId } = props; + const { anchor, peekId, handlePeekClose } = props; const router = useRouter(); const searchParams = useSearchParams(); // query params @@ -34,23 +35,23 @@ export const IssuePeekOverview: FC = observer((props) => { useEffect(() => { if (anchor && peekId && issueStore.groupedIssueIds) { - if (!issueDetails) { - issueDetailStore.fetchIssueDetails(anchor, peekId.toString()); - } + issueDetailStore.fetchIssueDetails(anchor, peekId.toString()); } - }, [anchor, issueDetailStore, issueDetails, peekId, issueStore.groupedIssueIds]); + }, [anchor, issueDetailStore, peekId, issueStore.groupedIssueIds]); - const handleClose = () => { - issueDetailStore.setPeekId(null); - let queryParams: any = { - board, - }; - if (priority && priority.length > 0) queryParams = { ...queryParams, priority: priority }; - if (state && state.length > 0) queryParams = { ...queryParams, state: state }; - if (labels && labels.length > 0) queryParams = { ...queryParams, labels: labels }; - queryParams = new URLSearchParams(queryParams).toString(); - router.push(`/issues/${anchor}?${queryParams}`); - }; + const handleClose = + handlePeekClose ?? + (() => { + issueDetailStore.setPeekId(null); + let queryParams: any = { + board, + }; + if (priority && priority.length > 0) queryParams = { ...queryParams, priority: priority }; + if (state && state.length > 0) queryParams = { ...queryParams, state: state }; + if (labels && labels.length > 0) queryParams = { ...queryParams, labels: labels }; + queryParams = new URLSearchParams(queryParams).toString(); + router.push(`/issues/${anchor}?${queryParams}`); + }); useEffect(() => { if (peekId) { diff --git a/space/core/constants/issue.ts b/space/core/constants/issue.ts index e518e9ebb70..1d9ebbb19c6 100644 --- a/space/core/constants/issue.ts +++ b/space/core/constants/issue.ts @@ -75,4 +75,15 @@ export const issuePriorityFilter = (priorityKey: TIssuePriorities): TIssueFilter if (currentIssuePriority) return currentIssuePriority; return undefined; -}; \ No newline at end of file +}; + +export const ISSUE_PRIORITIES: { + key: TIssuePriorities; + title: string; +}[] = [ + { key: "urgent", title: "Urgent" }, + { key: "high", title: "High" }, + { key: "medium", title: "Medium" }, + { key: "low", title: "Low" }, + { key: "none", title: "None" }, +]; \ No newline at end of file diff --git a/space/core/hooks/store/index.ts b/space/core/hooks/store/index.ts index 87b9d7317df..f6f46eccbc2 100644 --- a/space/core/hooks/store/index.ts +++ b/space/core/hooks/store/index.ts @@ -7,3 +7,6 @@ export * from "./use-issue-details"; export * from "./use-issue-filter"; export * from "./use-state"; export * from "./use-label"; +export * from "./use-cycle"; +export * from "./use-module"; +export * from "./use-member"; diff --git a/space/core/hooks/store/use-cycle.ts b/space/core/hooks/store/use-cycle.ts new file mode 100644 index 00000000000..554a93c4664 --- /dev/null +++ b/space/core/hooks/store/use-cycle.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// lib +import { StoreContext } from "@/lib/store-provider"; +// store +import { ICycleStore } from "@/store/cycle.store"; + +export const useCycle = (): ICycleStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useCycle must be used within StoreProvider"); + return context.cycle; +}; diff --git a/space/core/hooks/store/use-member.ts b/space/core/hooks/store/use-member.ts new file mode 100644 index 00000000000..80aca3cfb6e --- /dev/null +++ b/space/core/hooks/store/use-member.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// lib +import { StoreContext } from "@/lib/store-provider"; +// store +import { IIssueMemberStore } from "@/store/members.store"; + +export const useMember = (): IIssueMemberStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useMember must be used within StoreProvider"); + return context.member; +}; diff --git a/space/core/hooks/store/use-module.ts b/space/core/hooks/store/use-module.ts new file mode 100644 index 00000000000..1749ca9ab07 --- /dev/null +++ b/space/core/hooks/store/use-module.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// lib +import { StoreContext } from "@/lib/store-provider"; +// store +import { IIssueModuleStore } from "@/store/module.store"; + +export const useModule = (): IIssueModuleStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useModule must be used within StoreProvider"); + return context.module; +}; diff --git a/space/core/services/cycle.service.ts b/space/core/services/cycle.service.ts new file mode 100644 index 00000000000..a556800f8e3 --- /dev/null +++ b/space/core/services/cycle.service.ts @@ -0,0 +1,17 @@ +import { API_BASE_URL } from "@/helpers/common.helper"; +import { APIService } from "@/services/api.service"; +import { TPublicCycle } from "@/types/cycle"; + +export class CycleService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getCycles(anchor: string): Promise { + return this.get(`api/public/anchor/${anchor}/cycles/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/space/core/services/member.service.ts b/space/core/services/member.service.ts new file mode 100644 index 00000000000..1c4758e42fd --- /dev/null +++ b/space/core/services/member.service.ts @@ -0,0 +1,17 @@ +import { API_BASE_URL } from "@/helpers/common.helper"; +import { APIService } from "@/services/api.service"; +import { TPublicMember } from "@/types/member"; + +export class MemberService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getAnchorMembers(anchor: string): Promise { + return this.get(`api/public/anchor/${anchor}/members/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/space/core/services/module.service.ts b/space/core/services/module.service.ts new file mode 100644 index 00000000000..153f3f67c47 --- /dev/null +++ b/space/core/services/module.service.ts @@ -0,0 +1,17 @@ +import { API_BASE_URL } from "@/helpers/common.helper"; +import { APIService } from "@/services/api.service"; +import { TPublicModule } from "@/types/modules"; + +export class ModuleService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getModules(anchor: string): Promise { + return this.get(`api/public/anchor/${anchor}/modules/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/space/core/store/cycle.store.ts b/space/core/store/cycle.store.ts new file mode 100644 index 00000000000..a7310290bf4 --- /dev/null +++ b/space/core/store/cycle.store.ts @@ -0,0 +1,40 @@ +import { action, makeObservable, observable, runInAction } from "mobx"; +import { TPublicCycle } from "@/types/cycle"; +import { CycleService } from "../services/cycle.service"; +import { CoreRootStore } from "./root.store"; + +export interface ICycleStore { + // observables + cycles: TPublicCycle[] | undefined; + // computed actions + getCycleById: (cycleId: string | undefined) => TPublicCycle | undefined; + // fetch actions + fetchCycles: (anchor: string) => Promise; +} + +export class CycleStore implements ICycleStore { + cycles: TPublicCycle[] | undefined = undefined; + cycleService: CycleService; + rootStore: CoreRootStore; + + constructor(_rootStore: CoreRootStore) { + makeObservable(this, { + // observables + cycles: observable, + // fetch action + fetchCycles: action, + }); + this.cycleService = new CycleService(); + this.rootStore = _rootStore; + } + + getCycleById = (cycleId: string | undefined) => this.cycles?.find((cycle) => cycle.id === cycleId); + + fetchCycles = async (anchor: string) => { + const cyclesResponse = await this.cycleService.getCycles(anchor); + runInAction(() => { + this.cycles = cyclesResponse; + }); + return cyclesResponse; + }; +} diff --git a/space/core/store/helpers/base-issues.store.ts b/space/core/store/helpers/base-issues.store.ts index c61a6702337..004aa06c630 100644 --- a/space/core/store/helpers/base-issues.store.ts +++ b/space/core/store/helpers/base-issues.store.ts @@ -1,6 +1,5 @@ import concat from "lodash/concat"; import get from "lodash/get"; -import isEmpty from "lodash/isEmpty"; import set from "lodash/set"; import uniq from "lodash/uniq"; import update from "lodash/update"; @@ -23,7 +22,6 @@ import { // services import IssueService from "@/services/issue.service"; import { IIssue, TIssuesResponse } from "@/types/issue"; -import { IIssueFilterStore } from "../issue-filters.store"; import { CoreRootStore } from "../root.store"; // constants // helpers @@ -39,14 +37,9 @@ export enum EIssueGroupedAction { export interface IBaseIssuesStore { // observable loader: Record; - issuesMap: Record; // Record defines issue_id as key and IIssue as value // actions addIssue(issues: IIssue[], shouldReplace?: boolean): void; // helper methods - getIssueById(issueId: string): undefined | IIssue; - - fetchIssueById(anchorId: string, issueId: string): Promise; - groupedIssueIds: TGroupedIssues | TSubGroupedIssues | undefined; // object to store Issue Ids based on group or subgroup groupedIssueCount: TGroupedIssueCount; // map of groupId/subgroup and issue count of that particular group/subgroup issuePaginationData: TIssuePaginationData; // map of groupId/subgroup and pagination Data of that particular group/subgroup @@ -79,7 +72,6 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore { loader: Record = {}; groupedIssueIds: TIssues | undefined = undefined; issuePaginationData: TIssuePaginationData = {}; - issuesMap: Record = {}; // Record defines issue_id as key and TIssue as value groupedIssueCount: TGroupedIssueCount = {}; // paginationOptions: IssuePaginationOptions | undefined = undefined; @@ -87,9 +79,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore { issueService; // root store rootIssueStore; - issueFilterStore; - constructor(_rootStore: CoreRootStore, issueFilterStore: IIssueFilterStore) { + constructor(_rootStore: CoreRootStore) { makeObservable(this, { // observable loader: observable, @@ -107,7 +98,6 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore { setLoader: action.bound, }); this.rootIssueStore = _rootStore; - this.issueFilterStore = issueFilterStore; this.issueService = new IssueService(); } @@ -141,35 +131,12 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore { if (issues && issues.length <= 0) return; runInAction(() => { issues.forEach((issue) => { - if (!this.issuesMap[issue.id] || shouldReplace) set(this.issuesMap, issue.id, issue); + if (!this.rootIssueStore.issueDetail.getIssueById(issue.id) || shouldReplace) + set(this.rootIssueStore.issueDetail.details, issue.id, issue); }); }); }; - /** - * @description This method will return the issue from the issuesMap - * @param {string} issueId - * @returns {IIssue | undefined} - */ - getIssueById = computedFn((issueId: string) => { - if (!issueId || isEmpty(this.issuesMap) || !this.issuesMap[issueId]) return undefined; - return this.issuesMap[issueId]; - }); - - fetchIssueById = async (anchorId: string, issueId: string) => { - try { - const issueDetails = await this.issueService.getIssueById(anchorId, issueId); - - runInAction(() => { - set(this.issuesMap, [issueId], issueDetails); - }); - - return issueDetails; - } catch (e) { - console.error("error fetching issue details"); - } - }; - /** * Store the pagination data required for next subsequent issue pagination calls * @param prevCursor cursor value of previous page diff --git a/space/core/store/helpers/filter.helpers.ts b/space/core/store/helpers/filter.helpers.ts index 001f08024fb..fd949efefd9 100644 --- a/space/core/store/helpers/filter.helpers.ts +++ b/space/core/store/helpers/filter.helpers.ts @@ -32,6 +32,16 @@ export const getPaginationParams = ( paginationParams.group_by = EIssueGroupByToServerOptions[options.groupedBy]; } + // If group by is specifically sent through options, like that for calendar layout, use that to group + if (options.subGroupedBy) { + paginationParams.sub_group_by = EIssueGroupByToServerOptions[options.subGroupedBy]; + } + + // If group by is specifically sent through options, like that for calendar layout, use that to group + if (options.orderBy) { + paginationParams.order_by = options.orderBy; + } + // If before and after dates are sent from option to filter by then, add them to filter the options if (options.after && options.before) { paginationParams["target_date"] = `${options.after};after,${options.before};before`; diff --git a/space/core/store/issue-detail.store.ts b/space/core/store/issue-detail.store.ts index f17e689caca..6a07a77782f 100644 --- a/space/core/store/issue-detail.store.ts +++ b/space/core/store/issue-detail.store.ts @@ -1,5 +1,7 @@ +import isEmpty from "lodash/isEmpty"; import set from "lodash/set"; import { makeObservable, observable, action, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; import { v4 as uuidv4 } from "uuid"; // services import IssueService from "@/services/issue.service"; @@ -17,7 +19,10 @@ export interface IIssueDetailStore { details: { [key: string]: IIssue; }; + // computed actions + getIsIssuePeeked: (issueID: string) => boolean; // actions + getIssueById: (issueId: string) => IIssue | undefined; setPeekId: (issueID: string | null) => void; setPeekMode: (mode: IPeekMode) => void; // issue actions @@ -88,6 +93,38 @@ export class IssueDetailStore implements IIssueDetailStore { this.peekMode = mode; }; + getIsIssuePeeked = (issueID: string) => this.peekId === issueID; + + /** + * @description This method will return the issue from the issuesMap + * @param {string} issueId + * @returns {IIssue | undefined} + */ + getIssueById = computedFn((issueId: string) => { + if (!issueId || isEmpty(this.details) || !this.details[issueId]) return undefined; + return this.details[issueId]; + }); + + /** + * Retrieves issue from API + * @param anchorId ] + * @param issueId + * @returns + */ + fetchIssueById = async (anchorId: string, issueId: string) => { + try { + const issueDetails = await this.issueService.getIssueById(anchorId, issueId); + + runInAction(() => { + set(this.details, [issueId], issueDetails); + }); + + return issueDetails; + } catch (e) { + console.error("error fetching issue details"); + } + }; + /** * @description fetc * @param {string} anchor @@ -98,7 +135,7 @@ export class IssueDetailStore implements IIssueDetailStore { this.loader = true; this.error = null; - const issueDetails = await this.rootStore.issue.fetchIssueById(anchor, issueID); + const issueDetails = await this.fetchIssueById(anchor, issueID); const commentsResponse = await this.issueService.getIssueComments(anchor, issueID); if (issueDetails) { @@ -120,11 +157,11 @@ export class IssueDetailStore implements IIssueDetailStore { addIssueComment = async (anchor: string, issueID: string, data: any) => { try { - const issueDetails = this.rootStore.issue.getIssueById(issueID); + const issueDetails = this.getIssueById(issueID); const issueCommentResponse = await this.issueService.createIssueComment(anchor, issueID, data); if (issueDetails) { runInAction(() => { - set(this.details, [issueID, "comments"], [...this.details[issueID].comments, issueCommentResponse]); + set(this.details, [issueID, "comments"], [...(issueDetails?.comments ?? []), issueCommentResponse]); }); } return issueCommentResponse; diff --git a/space/core/store/issue.store.ts b/space/core/store/issue.store.ts index 087ea392d10..ca5154df7a3 100644 --- a/space/core/store/issue.store.ts +++ b/space/core/store/issue.store.ts @@ -27,7 +27,7 @@ export class IssueStore extends BaseIssuesStore implements IIssueStore { issueService: IssueService; constructor(_rootStore: CoreRootStore) { - super(_rootStore, _rootStore.issueFilter); + super(_rootStore); makeObservable(this, { // actions fetchPublicIssues: action, diff --git a/space/core/store/members.store.ts b/space/core/store/members.store.ts new file mode 100644 index 00000000000..5ddb8c5c1aa --- /dev/null +++ b/space/core/store/members.store.ts @@ -0,0 +1,63 @@ +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { TPublicMember } from "@/types/member"; +import { MemberService } from "../services/member.service"; +import { CoreRootStore } from "./root.store"; + +export interface IIssueMemberStore { + // observables + members: TPublicMember[] | undefined; + // computed actions + getMemberById: (memberId: string | undefined) => TPublicMember | undefined; + getMembersByIds: (memberIds: string[]) => TPublicMember[]; + // fetch actions + fetchMembers: (anchor: string) => Promise; +} + +export class MemberStore implements IIssueMemberStore { + memberMap: Record = {}; + memberService: MemberService; + rootStore: CoreRootStore; + + constructor(_rootStore: CoreRootStore) { + makeObservable(this, { + // observables + memberMap: observable, + // computed + members: computed, + // fetch action + fetchMembers: action, + }); + this.memberService = new MemberService(); + this.rootStore = _rootStore; + } + + get members() { + return Object.values(this.memberMap); + } + + getMemberById = (memberId: string | undefined) => (memberId ? this.memberMap[memberId] : undefined); + + getMembersByIds = (memberIds: string[]) => { + const currMembers = []; + for (const memberId of memberIds) { + const member = this.getMemberById(memberId); + if (member) { + currMembers.push(member); + } + } + + return currMembers; + }; + + fetchMembers = async (anchor: string) => { + const membersResponse = await this.memberService.getAnchorMembers(anchor); + runInAction(() => { + this.memberMap = {}; + for (const member of membersResponse) { + set(this.memberMap, [member.member], member); + } + }); + return membersResponse; + }; +} diff --git a/space/core/store/module.store.ts b/space/core/store/module.store.ts new file mode 100644 index 00000000000..5078cc17204 --- /dev/null +++ b/space/core/store/module.store.ts @@ -0,0 +1,63 @@ +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { TPublicModule } from "@/types/modules"; +import { ModuleService } from "../services/module.service"; +import { CoreRootStore } from "./root.store"; + +export interface IIssueModuleStore { + // observables + modules: TPublicModule[] | undefined; + // computed actions + getModuleById: (moduleId: string | undefined) => TPublicModule | undefined; + getModulesByIds: (moduleIds: string[]) => TPublicModule[]; + // fetch actions + fetchModules: (anchor: string) => Promise; +} + +export class ModuleStore implements IIssueModuleStore { + moduleMap: Record = {}; + moduleService: ModuleService; + rootStore: CoreRootStore; + + constructor(_rootStore: CoreRootStore) { + makeObservable(this, { + // observables + moduleMap: observable, + // computed + modules: computed, + // fetch action + fetchModules: action, + }); + this.moduleService = new ModuleService(); + this.rootStore = _rootStore; + } + + get modules() { + return Object.values(this.moduleMap); + } + + getModuleById = (moduleId: string | undefined) => (moduleId ? this.moduleMap[moduleId] : undefined); + + getModulesByIds = (moduleIds: string[]) => { + const currModules = []; + for (const moduleId of moduleIds) { + const issueModule = this.getModuleById(moduleId); + if (issueModule) { + currModules.push(issueModule); + } + } + + return currModules; + }; + + fetchModules = async (anchor: string) => { + const modulesResponse = await this.moduleService.getModules(anchor); + runInAction(() => { + this.moduleMap = {}; + for (const issueModule of modulesResponse) { + set(this.moduleMap, [issueModule.id], issueModule); + } + }); + return modulesResponse; + }; +} diff --git a/space/core/store/root.store.ts b/space/core/store/root.store.ts index 3f3ad5bb558..de43001d2c9 100644 --- a/space/core/store/root.store.ts +++ b/space/core/store/root.store.ts @@ -4,9 +4,12 @@ import { IInstanceStore, InstanceStore } from "@/store/instance.store"; import { IssueDetailStore, IIssueDetailStore } from "@/store/issue-detail.store"; import { IssueStore, IIssueStore } from "@/store/issue.store"; import { IUserStore, UserStore } from "@/store/user.store"; +import { CycleStore, ICycleStore } from "./cycle.store"; import { IssueFilterStore, IIssueFilterStore } from "./issue-filters.store"; import { IIssueLabelStore, LabelStore } from "./label.store"; +import { IIssueMemberStore, MemberStore } from "./members.store"; import { IMentionsStore, MentionsStore } from "./mentions.store"; +import { IIssueModuleStore, ModuleStore } from "./module.store"; import { IPublishListStore, PublishListStore } from "./publish/publish_list.store"; import { IStateStore, StateStore } from "./state.store"; @@ -20,6 +23,9 @@ export class CoreRootStore { mentionStore: IMentionsStore; state: IStateStore; label: IIssueLabelStore; + module: IIssueModuleStore; + member: IIssueMemberStore; + cycle: ICycleStore; issueFilter: IIssueFilterStore; publishList: IPublishListStore; @@ -31,6 +37,9 @@ export class CoreRootStore { this.mentionStore = new MentionsStore(this); this.state = new StateStore(this); this.label = new LabelStore(this); + this.module = new ModuleStore(this); + this.member = new MemberStore(this); + this.cycle = new CycleStore(this); this.issueFilter = new IssueFilterStore(this); this.publishList = new PublishListStore(this); } @@ -51,6 +60,9 @@ export class CoreRootStore { this.mentionStore = new MentionsStore(this); this.state = new StateStore(this); this.label = new LabelStore(this); + this.module = new ModuleStore(this); + this.member = new MemberStore(this); + this.cycle = new CycleStore(this); this.issueFilter = new IssueFilterStore(this); this.publishList = new PublishListStore(this); } diff --git a/space/core/types/cycle.d.ts b/space/core/types/cycle.d.ts new file mode 100644 index 00000000000..edf8f31a8a1 --- /dev/null +++ b/space/core/types/cycle.d.ts @@ -0,0 +1,5 @@ +export type TPublicCycle = { + id: string; + name: string; + status: string; +}; diff --git a/space/core/types/issue.d.ts b/space/core/types/issue.d.ts index 00d6d505ebf..79c6257d5af 100644 --- a/space/core/types/issue.d.ts +++ b/space/core/types/issue.d.ts @@ -37,6 +37,8 @@ export interface IIssue extends Pick< TIssue, | "description_html" + | "created_at" + | "updated_at" | "created_by" | "id" | "name" @@ -51,6 +53,10 @@ export interface IIssue | "module_ids" | "label_ids" | "assignee_ids" + | "attachment_count" + | "sub_issues_count" + | "link_count" + | "estimate_point" > { comments: Comment[]; reaction_items: IIssueReaction[]; diff --git a/space/core/types/member.d.ts b/space/core/types/member.d.ts new file mode 100644 index 00000000000..721ccd98fc5 --- /dev/null +++ b/space/core/types/member.d.ts @@ -0,0 +1,10 @@ +export type TPublicMember = { + id: string; + member: string; + member__avatar: string; + member__first_name: string; + member__last_name: string; + member__display_name: string; + project: string; + workspace: string; +}; diff --git a/space/core/types/modules.d.ts b/space/core/types/modules.d.ts new file mode 100644 index 00000000000..8bc35ce6ff8 --- /dev/null +++ b/space/core/types/modules.d.ts @@ -0,0 +1,4 @@ +export type TPublicModule = { + id: string; + name: string; +}; diff --git a/space/ee/components/issue-layouts/root.tsx b/space/ee/components/issue-layouts/root.tsx new file mode 100644 index 00000000000..1a04c6be4e2 --- /dev/null +++ b/space/ee/components/issue-layouts/root.tsx @@ -0,0 +1,9 @@ +import { PublishStore } from "@/store/publish/publish.store"; + +type Props = { + peekId: string | undefined; + publishSettings: PublishStore; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const ViewLayoutsRoot = (props: Props) => <>; diff --git a/space/ee/components/navbar/index.tsx b/space/ee/components/navbar/index.tsx new file mode 100644 index 00000000000..6e6fa444149 --- /dev/null +++ b/space/ee/components/navbar/index.tsx @@ -0,0 +1,8 @@ +import { PublishStore } from "@/store/publish/publish.store"; + +type Props = { + publishSettings: PublishStore; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const ViewNavbarRoot = (props: Props) => <>; diff --git a/space/ee/hooks/store/index.ts b/space/ee/hooks/store/index.ts new file mode 100644 index 00000000000..a5fc99eef89 --- /dev/null +++ b/space/ee/hooks/store/index.ts @@ -0,0 +1 @@ +export * from "./use-published-view"; diff --git a/space/ee/hooks/store/use-published-view.ts b/space/ee/hooks/store/use-published-view.ts new file mode 100644 index 00000000000..170d934da20 --- /dev/null +++ b/space/ee/hooks/store/use-published-view.ts @@ -0,0 +1,5 @@ +export const useView = () => ({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + fetchViewDetails: (anchor: string) => {}, + viewData: {}, +}); diff --git a/space/helpers/string.helper.ts b/space/helpers/string.helper.ts index f6319bc7507..fe7df96dae3 100644 --- a/space/helpers/string.helper.ts +++ b/space/helpers/string.helper.ts @@ -56,3 +56,7 @@ export const isEmptyHtmlString = (htmlString: string) => { // Trim the string and check if it's empty return cleanText.trim() === ""; }; + +export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " "); + +export const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); \ No newline at end of file diff --git a/space/styles/globals.css b/space/styles/globals.css index 0b41d84811b..511f6ad1fdf 100644 --- a/space/styles/globals.css +++ b/space/styles/globals.css @@ -354,3 +354,90 @@ body { .disable-autofill-style:-webkit-autofill:active { -webkit-background-clip: text; } + + +@-moz-document url-prefix() { + * { + scrollbar-width: none; + } + .vertical-scrollbar, + .horizontal-scrollbar { + scrollbar-width: initial; + scrollbar-color: rgba(96, 100, 108, 0.1) transparent; + } + .vertical-scrollbar:hover, + .horizontal-scrollbar:hover { + scrollbar-color: rgba(96, 100, 108, 0.25) transparent; + } + .vertical-scrollbar:active, + .horizontal-scrollbar:active { + scrollbar-color: rgba(96, 100, 108, 0.7) transparent; + } +} + +.vertical-scrollbar { + overflow-y: auto; +} +.horizontal-scrollbar { + overflow-x: auto; +} +.vertical-scrollbar::-webkit-scrollbar, +.horizontal-scrollbar::-webkit-scrollbar { + display: block; +} +.vertical-scrollbar::-webkit-scrollbar-track, +.horizontal-scrollbar::-webkit-scrollbar-track { + background-color: transparent; + border-radius: 9999px; +} +.vertical-scrollbar::-webkit-scrollbar-thumb, +.horizontal-scrollbar::-webkit-scrollbar-thumb { + background-clip: padding-box; + background-color: rgba(96, 100, 108, 0.1); + border-radius: 9999px; +} +.vertical-scrollbar:hover::-webkit-scrollbar-thumb, +.horizontal-scrollbar:hover::-webkit-scrollbar-thumb { + background-color: rgba(96, 100, 108, 0.25); +} +.vertical-scrollbar::-webkit-scrollbar-thumb:hover, +.horizontal-scrollbar::-webkit-scrollbar-thumb:hover { + background-color: rgba(96, 100, 108, 0.5); +} +.vertical-scrollbar::-webkit-scrollbar-thumb:active, +.horizontal-scrollbar::-webkit-scrollbar-thumb:active { + background-color: rgba(96, 100, 108, 0.7); +} +.vertical-scrollbar::-webkit-scrollbar-corner, +.horizontal-scrollbar::-webkit-scrollbar-corner { + background-color: transparent; +} +.vertical-scrollbar-margin-top-md::-webkit-scrollbar-track { + margin-top: 44px; +} + +/* scrollbar sm size */ +.scrollbar-sm::-webkit-scrollbar { + height: 12px; + width: 12px; +} +.scrollbar-sm::-webkit-scrollbar-thumb { + border: 3px solid rgba(0, 0, 0, 0); +} +/* scrollbar md size */ +.scrollbar-md::-webkit-scrollbar { + height: 14px; + width: 14px; +} +.scrollbar-md::-webkit-scrollbar-thumb { + border: 3px solid rgba(0, 0, 0, 0); +} +/* scrollbar lg size */ + +.scrollbar-lg::-webkit-scrollbar { + height: 16px; + width: 16px; +} +.scrollbar-lg::-webkit-scrollbar-thumb { + border: 4px solid rgba(0, 0, 0, 0); +} diff --git a/web/ce/components/views/publish/index.ts b/web/ce/components/views/publish/index.ts new file mode 100644 index 00000000000..8c04a4e3d8e --- /dev/null +++ b/web/ce/components/views/publish/index.ts @@ -0,0 +1,2 @@ +export * from "./modal"; +export * from "./use-view-publish"; diff --git a/web/ce/components/views/publish/modal.tsx b/web/ce/components/views/publish/modal.tsx new file mode 100644 index 00000000000..0951de0930d --- /dev/null +++ b/web/ce/components/views/publish/modal.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { IProjectView } from "@plane/types"; + +type Props = { + isOpen: boolean; + view: IProjectView; + onClose: () => void; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const PublishViewModal = (props: Props) => <>; diff --git a/web/ce/components/views/publish/use-view-publish.tsx b/web/ce/components/views/publish/use-view-publish.tsx new file mode 100644 index 00000000000..687a79ed762 --- /dev/null +++ b/web/ce/components/views/publish/use-view-publish.tsx @@ -0,0 +1,7 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const useViewPublish = (isPublished: boolean, isAuthorized: boolean) => ({ + isPublishModalOpen: false, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + setPublishModalOpen: (value: boolean) => {}, + publishContextMenu: undefined, +}); diff --git a/web/ce/services/project/view.service.ts b/web/ce/services/project/view.service.ts index 07872394a39..6cb76222add 100644 --- a/web/ce/services/project/view.service.ts +++ b/web/ce/services/project/view.service.ts @@ -1,3 +1,4 @@ +import { TPublishViewSettings } from "@plane/types"; import { EViewAccess } from "@/constants/views"; import { API_BASE_URL } from "@/helpers/common.helper"; import { ViewService as CoreViewService } from "@/services/view.service"; @@ -21,4 +22,40 @@ export class ViewService extends CoreViewService { async unLockView(workspaceSlug: string, projectId: string, viewId: string) { return Promise.resolve(); } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getPublishDetails(workspaceSlug: string, projectId: string, viewId: string): Promise { + return Promise.resolve({}); + } + + async publishView( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + workspaceSlug: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + projectId: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + viewId: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + data: TPublishViewSettings + ): Promise { + return Promise.resolve(); + } + + async updatePublishedView( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + workspaceSlug: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + projectId: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + viewId: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + data: Partial + ): Promise { + return Promise.resolve(); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async unPublishView(workspaceSlug: string, projectId: string, viewId: string): Promise { + return Promise.resolve(); + } } diff --git a/web/core/components/views/quick-actions.tsx b/web/core/components/views/quick-actions.tsx index b2d8680b712..287fcad91d8 100644 --- a/web/core/components/views/quick-actions.tsx +++ b/web/core/components/views/quick-actions.tsx @@ -16,6 +16,7 @@ import { cn } from "@/helpers/common.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks import { useUser } from "@/hooks/store"; +import { PublishViewModal, useViewPublish } from "@/plane-web/components/views/publish"; type Props = { parentRef: React.RefObject; @@ -38,6 +39,11 @@ export const ViewQuickActions: React.FC = observer((props) => { const isOwner = view?.owned_by === data?.id; const isAdmin = !!currentProjectRole && currentProjectRole == EUserProjectRoles.ADMIN; + const { isPublishModalOpen, setPublishModalOpen, publishContextMenu } = useViewPublish( + !!view.anchor, + isAdmin || isOwner + ); + const viewLink = `${workspaceSlug}/projects/${projectId}/views/${view.id}`; const handleCopyText = () => copyUrlToClipboard(viewLink).then(() => { @@ -78,6 +84,8 @@ export const ViewQuickActions: React.FC = observer((props) => { }, ]; + if (publishContextMenu) MENU_ITEMS.splice(2, 0, publishContextMenu); + return ( <> = observer((props) => { data={view} /> setDeleteViewModal(false)} /> + setPublishModalOpen(false)} view={view} /> {MENU_ITEMS.map((item) => { diff --git a/web/core/store/project-view.store.ts b/web/core/store/project-view.store.ts index f76bb45fefa..207eb70930a 100644 --- a/web/core/store/project-view.store.ts +++ b/web/core/store/project-view.store.ts @@ -2,7 +2,7 @@ import { set } from "lodash"; import { observable, action, makeObservable, runInAction, computed } from "mobx"; import { computedFn } from "mobx-utils"; // types -import { IProjectView, TViewFilters } from "@plane/types"; +import { IProjectView, TPublishViewDetails, TPublishViewSettings, TViewFilters } from "@plane/types"; // constants import { EViewAccess } from "@/constants/views"; // helpers @@ -42,6 +42,25 @@ export interface IProjectViewStore { // favorites actions addViewToFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise; removeViewFromFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise; + // publish + publishView: ( + workspaceSlug: string, + projectId: string, + viewId: string, + data: TPublishViewSettings + ) => Promise; + fetchPublishDetails: ( + workspaceSlug: string, + projectId: string, + viewId: string + ) => Promise; + updatePublishedView: ( + workspaceSlug: string, + projectId: string, + viewId: string, + data: Partial + ) => Promise; + unPublishView: (workspaceSlug: string, projectId: string, viewId: string) => Promise; } export class ProjectViewStore implements IProjectViewStore { @@ -372,4 +391,91 @@ export class ProjectViewStore implements IProjectViewStore { }); } }; + + /** + * Publishes View to the Public + * @param workspaceSlug + * @param projectId + * @param viewId + * @returns + */ + publishView = async (workspaceSlug: string, projectId: string, viewId: string, data: TPublishViewSettings) => { + try { + const response = (await this.viewService.publishView( + workspaceSlug, + projectId, + viewId, + data + )) as TPublishViewDetails; + runInAction(() => { + set(this.viewMap, [viewId, "anchor"], response?.anchor); + }); + + return response; + } catch (error) { + console.error("Failed to publish view", error); + } + }; + + /** + * fetches Published Details + * @param workspaceSlug + * @param projectId + * @param viewId + * @returns + */ + fetchPublishDetails = async (workspaceSlug: string, projectId: string, viewId: string) => { + try { + const response = (await this.viewService.getPublishDetails( + workspaceSlug, + projectId, + viewId + )) as TPublishViewDetails; + runInAction(() => { + set(this.viewMap, [viewId, "anchor"], response?.anchor); + }); + return response; + } catch (error) { + console.error("Failed to fetch published view details", error); + } + }; + + /** + * updates already published view + * @param workspaceSlug + * @param projectId + * @param viewId + * @returns + */ + updatePublishedView = async ( + workspaceSlug: string, + projectId: string, + viewId: string, + data: Partial + ) => { + try { + return await this.viewService.updatePublishedView(workspaceSlug, projectId, viewId, data); + } catch (error) { + console.error("Failed to update published view details", error); + } + }; + + /** + * un publishes the view + * @param workspaceSlug + * @param projectId + * @param viewId + * @returns + */ + unPublishView = async (workspaceSlug: string, projectId: string, viewId: string) => { + try { + const response = await this.viewService.unPublishView(workspaceSlug, projectId, viewId); + runInAction(() => { + set(this.viewMap, [viewId, "anchor"], null); + }); + return response; + } catch (error) { + console.error("Failed to unPublish view", error); + } + }; } diff --git a/web/ee/components/views/publish/index.ts b/web/ee/components/views/publish/index.ts new file mode 100644 index 00000000000..8c04a4e3d8e --- /dev/null +++ b/web/ee/components/views/publish/index.ts @@ -0,0 +1,2 @@ +export * from "./modal"; +export * from "./use-view-publish"; diff --git a/web/ee/components/views/publish/modal.tsx b/web/ee/components/views/publish/modal.tsx new file mode 100644 index 00000000000..0951de0930d --- /dev/null +++ b/web/ee/components/views/publish/modal.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { IProjectView } from "@plane/types"; + +type Props = { + isOpen: boolean; + view: IProjectView; + onClose: () => void; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const PublishViewModal = (props: Props) => <>; diff --git a/web/ee/components/views/publish/use-view-publish.tsx b/web/ee/components/views/publish/use-view-publish.tsx new file mode 100644 index 00000000000..687a79ed762 --- /dev/null +++ b/web/ee/components/views/publish/use-view-publish.tsx @@ -0,0 +1,7 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const useViewPublish = (isPublished: boolean, isAuthorized: boolean) => ({ + isPublishModalOpen: false, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + setPublishModalOpen: (value: boolean) => {}, + publishContextMenu: undefined, +}); diff --git a/web/ee/services/project/view.service.ts b/web/ee/services/project/view.service.ts new file mode 100644 index 00000000000..6cb76222add --- /dev/null +++ b/web/ee/services/project/view.service.ts @@ -0,0 +1,61 @@ +import { TPublishViewSettings } from "@plane/types"; +import { EViewAccess } from "@/constants/views"; +import { API_BASE_URL } from "@/helpers/common.helper"; +import { ViewService as CoreViewService } from "@/services/view.service"; + +export class ViewService extends CoreViewService { + constructor() { + super(API_BASE_URL); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async updateViewAccess(workspaceSlug: string, projectId: string, viewId: string, access: EViewAccess) { + return Promise.resolve(); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async lockView(workspaceSlug: string, projectId: string, viewId: string) { + return Promise.resolve(); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async unLockView(workspaceSlug: string, projectId: string, viewId: string) { + return Promise.resolve(); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getPublishDetails(workspaceSlug: string, projectId: string, viewId: string): Promise { + return Promise.resolve({}); + } + + async publishView( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + workspaceSlug: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + projectId: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + viewId: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + data: TPublishViewSettings + ): Promise { + return Promise.resolve(); + } + + async updatePublishedView( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + workspaceSlug: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + projectId: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + viewId: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + data: Partial + ): Promise { + return Promise.resolve(); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async unPublishView(workspaceSlug: string, projectId: string, viewId: string): Promise { + return Promise.resolve(); + } +} From 08d026287c71fd877e1e46b82130bfb6be526aea Mon Sep 17 00:00:00 2001 From: rahulramesha Date: Fri, 19 Jul 2024 18:02:14 +0530 Subject: [PATCH 2/5] default views to not found page --- space/ce/components/issue-layouts/root.tsx | 3 +- space/core/components/ui/not-found.tsx | 33 ++++++++++++++++++++++ space/ee/components/issue-layouts/root.tsx | 3 +- 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 space/core/components/ui/not-found.tsx diff --git a/space/ce/components/issue-layouts/root.tsx b/space/ce/components/issue-layouts/root.tsx index 1a04c6be4e2..5fa40fe1171 100644 --- a/space/ce/components/issue-layouts/root.tsx +++ b/space/ce/components/issue-layouts/root.tsx @@ -1,3 +1,4 @@ +import { PageNotFound } from "@/components/ui/not-found"; import { PublishStore } from "@/store/publish/publish.store"; type Props = { @@ -6,4 +7,4 @@ type Props = { }; // eslint-disable-next-line @typescript-eslint/no-unused-vars -export const ViewLayoutsRoot = (props: Props) => <>; +export const ViewLayoutsRoot = (props: Props) => ; diff --git a/space/core/components/ui/not-found.tsx b/space/core/components/ui/not-found.tsx new file mode 100644 index 00000000000..4b92386d679 --- /dev/null +++ b/space/core/components/ui/not-found.tsx @@ -0,0 +1,33 @@ +"use client"; + +import React from "react"; +import { Metadata } from "next"; +import Image from "next/image"; +import Link from "next/link"; +// ui +import { Button } from "@plane/ui"; +// images +import Image404 from "@/public/404.svg"; + +export const metadata: Metadata = { + title: "404 - Page Not Found", +}; + +export const PageNotFound = () => ( +
+
+
+
+ 404- Page not found +
+
+

Oops! Something went wrong.

+

+ Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is + temporarily unavailable. +

+
+
+
+
+); diff --git a/space/ee/components/issue-layouts/root.tsx b/space/ee/components/issue-layouts/root.tsx index 1a04c6be4e2..5fa40fe1171 100644 --- a/space/ee/components/issue-layouts/root.tsx +++ b/space/ee/components/issue-layouts/root.tsx @@ -1,3 +1,4 @@ +import { PageNotFound } from "@/components/ui/not-found"; import { PublishStore } from "@/store/publish/publish.store"; type Props = { @@ -6,4 +7,4 @@ type Props = { }; // eslint-disable-next-line @typescript-eslint/no-unused-vars -export const ViewLayoutsRoot = (props: Props) => <>; +export const ViewLayoutsRoot = (props: Props) => ; From c36893828f3da21f2197d63ab99eb393ac2b8018 Mon Sep 17 00:00:00 2001 From: rahulramesha Date: Mon, 22 Jul 2024 13:01:22 +0530 Subject: [PATCH 3/5] refactor exports --- space/ee/components/issue-layouts/root.tsx | 11 +---------- space/ee/components/navbar/index.tsx | 9 +-------- space/ee/hooks/store/index.ts | 2 +- space/ee/hooks/store/use-published-view.ts | 5 ----- web/ee/components/views/publish/index.ts | 3 +-- web/ee/components/views/publish/modal.tsx | 12 ------------ web/ee/components/views/publish/use-view-publish.tsx | 7 ------- 7 files changed, 4 insertions(+), 45 deletions(-) delete mode 100644 space/ee/hooks/store/use-published-view.ts delete mode 100644 web/ee/components/views/publish/modal.tsx delete mode 100644 web/ee/components/views/publish/use-view-publish.tsx diff --git a/space/ee/components/issue-layouts/root.tsx b/space/ee/components/issue-layouts/root.tsx index 5fa40fe1171..d785c5c11c2 100644 --- a/space/ee/components/issue-layouts/root.tsx +++ b/space/ee/components/issue-layouts/root.tsx @@ -1,10 +1 @@ -import { PageNotFound } from "@/components/ui/not-found"; -import { PublishStore } from "@/store/publish/publish.store"; - -type Props = { - peekId: string | undefined; - publishSettings: PublishStore; -}; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const ViewLayoutsRoot = (props: Props) => ; +export * from "ce/components/issue-layouts/root"; diff --git a/space/ee/components/navbar/index.tsx b/space/ee/components/navbar/index.tsx index 6e6fa444149..960fa250745 100644 --- a/space/ee/components/navbar/index.tsx +++ b/space/ee/components/navbar/index.tsx @@ -1,8 +1 @@ -import { PublishStore } from "@/store/publish/publish.store"; - -type Props = { - publishSettings: PublishStore; -}; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const ViewNavbarRoot = (props: Props) => <>; +export * from "ce/components/navbar"; diff --git a/space/ee/hooks/store/index.ts b/space/ee/hooks/store/index.ts index a5fc99eef89..6ce80b4fb5a 100644 --- a/space/ee/hooks/store/index.ts +++ b/space/ee/hooks/store/index.ts @@ -1 +1 @@ -export * from "./use-published-view"; +export * from "ce/hooks/store"; diff --git a/space/ee/hooks/store/use-published-view.ts b/space/ee/hooks/store/use-published-view.ts deleted file mode 100644 index 170d934da20..00000000000 --- a/space/ee/hooks/store/use-published-view.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const useView = () => ({ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - fetchViewDetails: (anchor: string) => {}, - viewData: {}, -}); diff --git a/web/ee/components/views/publish/index.ts b/web/ee/components/views/publish/index.ts index 8c04a4e3d8e..d2680523a80 100644 --- a/web/ee/components/views/publish/index.ts +++ b/web/ee/components/views/publish/index.ts @@ -1,2 +1 @@ -export * from "./modal"; -export * from "./use-view-publish"; +export * from "ce/components/views/publish"; diff --git a/web/ee/components/views/publish/modal.tsx b/web/ee/components/views/publish/modal.tsx deleted file mode 100644 index 0951de0930d..00000000000 --- a/web/ee/components/views/publish/modal.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"use client"; - -import { IProjectView } from "@plane/types"; - -type Props = { - isOpen: boolean; - view: IProjectView; - onClose: () => void; -}; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const PublishViewModal = (props: Props) => <>; diff --git a/web/ee/components/views/publish/use-view-publish.tsx b/web/ee/components/views/publish/use-view-publish.tsx deleted file mode 100644 index 687a79ed762..00000000000 --- a/web/ee/components/views/publish/use-view-publish.tsx +++ /dev/null @@ -1,7 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const useViewPublish = (isPublished: boolean, isAuthorized: boolean) => ({ - isPublishModalOpen: false, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - setPublishModalOpen: (value: boolean) => {}, - publishContextMenu: undefined, -}); From 428270444984b914a75126d8e141a9aef55c4429 Mon Sep 17 00:00:00 2001 From: rahulramesha Date: Mon, 22 Jul 2024 14:02:21 +0530 Subject: [PATCH 4/5] remove uncessary view service --- web/ee/services/project/view.service.ts | 61 ------------------------- 1 file changed, 61 deletions(-) delete mode 100644 web/ee/services/project/view.service.ts diff --git a/web/ee/services/project/view.service.ts b/web/ee/services/project/view.service.ts deleted file mode 100644 index 6cb76222add..00000000000 --- a/web/ee/services/project/view.service.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { TPublishViewSettings } from "@plane/types"; -import { EViewAccess } from "@/constants/views"; -import { API_BASE_URL } from "@/helpers/common.helper"; -import { ViewService as CoreViewService } from "@/services/view.service"; - -export class ViewService extends CoreViewService { - constructor() { - super(API_BASE_URL); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async updateViewAccess(workspaceSlug: string, projectId: string, viewId: string, access: EViewAccess) { - return Promise.resolve(); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async lockView(workspaceSlug: string, projectId: string, viewId: string) { - return Promise.resolve(); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async unLockView(workspaceSlug: string, projectId: string, viewId: string) { - return Promise.resolve(); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async getPublishDetails(workspaceSlug: string, projectId: string, viewId: string): Promise { - return Promise.resolve({}); - } - - async publishView( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - workspaceSlug: string, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - projectId: string, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - viewId: string, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - data: TPublishViewSettings - ): Promise { - return Promise.resolve(); - } - - async updatePublishedView( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - workspaceSlug: string, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - projectId: string, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - viewId: string, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - data: Partial - ): Promise { - return Promise.resolve(); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async unPublishView(workspaceSlug: string, projectId: string, viewId: string): Promise { - return Promise.resolve(); - } -} From 40b29fa16831b2bf0b6bcd901c240650063edeee Mon Sep 17 00:00:00 2001 From: rahulramesha Date: Mon, 22 Jul 2024 15:35:58 +0530 Subject: [PATCH 5/5] fix review comments --- .../issue-layouts/kanban/base-kanban-root.tsx | 19 +++++++----- .../issue-layouts/kanban/kanban-group.tsx | 2 +- .../issue-layouts/list/base-list-root.tsx | 20 ++++++++----- .../issues/issue-layouts/list/blocks-list.tsx | 10 +++---- .../issues/issue-layouts/list/list-group.tsx | 2 +- .../issues/peek-overview/layout.tsx | 30 +++++++++++-------- space/core/components/ui/not-found.tsx | 7 ----- space/core/store/issue-detail.store.ts | 2 +- space/core/store/members.store.ts | 21 ++++++++----- space/core/store/module.store.ts | 21 ++++++++----- 10 files changed, 74 insertions(+), 60 deletions(-) diff --git a/space/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/space/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 7020797eb0e..f81f1aed8c8 100644 --- a/space/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/space/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useRef } from "react"; +import { useCallback, useMemo, useRef } from "react"; import debounce from "lodash/debounce"; import { observer } from "mobx-react"; // types @@ -20,13 +20,16 @@ export const IssueKanbanLayoutRoot: React.FC = observer((props: Props) => // store hooks const { groupedIssueIds, getIssueLoader, fetchNextPublicIssues, getGroupIssueCount, getPaginationData } = useIssue(); - const displayProperties: IIssueDisplayProperties = { - key: true, - state: true, - labels: true, - priority: true, - due_date: true, - }; + const displayProperties: IIssueDisplayProperties = useMemo( + () => ({ + key: true, + state: true, + labels: true, + priority: true, + due_date: true, + }), + [] + ); const fetchMoreIssues = useCallback( (groupId?: string, subgroupId?: string) => { diff --git a/space/core/components/issues/issue-layouts/kanban/kanban-group.tsx b/space/core/components/issues/issue-layouts/kanban/kanban-group.tsx index 0842e46acf6..fd00d20bac3 100644 --- a/space/core/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/space/core/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -87,7 +87,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => { ) : (
{" "} diff --git a/space/core/components/issues/issue-layouts/list/base-list-root.tsx b/space/core/components/issues/issue-layouts/list/base-list-root.tsx index ad0952b299a..737664b7394 100644 --- a/space/core/components/issues/issue-layouts/list/base-list-root.tsx +++ b/space/core/components/issues/issue-layouts/list/base-list-root.tsx @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { observer } from "mobx-react"; // types import { IIssueDisplayProperties, TGroupedIssues } from "@plane/types"; @@ -26,13 +26,17 @@ export const IssuesListLayoutRoot = observer((props: Props) => { const groupedIssueIds = storeGroupedIssueIds as TGroupedIssues | undefined; // auth - const displayProperties: IIssueDisplayProperties = { - key: true, - state: true, - priority: true, - due_date: true, - labels: true, - }; + const displayProperties: IIssueDisplayProperties = useMemo( + () => ({ + key: true, + state: true, + labels: true, + priority: true, + due_date: true, + }), + [] + ); + const loadMoreIssues = useCallback( (groupId?: string) => { fetchNextPublicIssues(anchor, groupId); diff --git a/space/core/components/issues/issue-layouts/list/blocks-list.tsx b/space/core/components/issues/issue-layouts/list/blocks-list.tsx index 19d690759c8..bb25e5c1675 100644 --- a/space/core/components/issues/issue-layouts/list/blocks-list.tsx +++ b/space/core/components/issues/issue-layouts/list/blocks-list.tsx @@ -1,22 +1,22 @@ import { FC, MutableRefObject } from "react"; // types -import { IIssueDisplayProperties, TGroupedIssues } from "@plane/types"; +import { IIssueDisplayProperties } from "@plane/types"; import { IssueBlock } from "./block"; interface Props { - issueIds: TGroupedIssues | any; + issueIds: string[] | undefined; groupId: string; - displayProperties: IIssueDisplayProperties | undefined; + displayProperties?: IIssueDisplayProperties; containerRef: MutableRefObject; } export const IssueBlocksList: FC = (props) => { - const { issueIds, groupId, displayProperties } = props; + const { issueIds = [], groupId, displayProperties } = props; return (
{issueIds && - issueIds.length > 0 && + issueIds?.length > 0 && issueIds.map((issueId: string) => ( ))} diff --git a/space/core/components/issues/issue-layouts/list/list-group.tsx b/space/core/components/issues/issue-layouts/list/list-group.tsx index 9dd2fdcec95..742cfeef156 100644 --- a/space/core/components/issues/issue-layouts/list/list-group.tsx +++ b/space/core/components/issues/issue-layouts/list/list-group.tsx @@ -80,7 +80,7 @@ export const ListGroup = observer((props: Props) => { ) : (
loadMoreIssues(group.id)} > diff --git a/space/core/components/issues/peek-overview/layout.tsx b/space/core/components/issues/peek-overview/layout.tsx index d91d62a32a9..e649a3279a9 100644 --- a/space/core/components/issues/peek-overview/layout.tsx +++ b/space/core/components/issues/peek-overview/layout.tsx @@ -39,19 +39,23 @@ export const IssuePeekOverview: FC = observer((props) => { } }, [anchor, issueDetailStore, peekId, issueStore.groupedIssueIds]); - const handleClose = - handlePeekClose ?? - (() => { - issueDetailStore.setPeekId(null); - let queryParams: any = { - board, - }; - if (priority && priority.length > 0) queryParams = { ...queryParams, priority: priority }; - if (state && state.length > 0) queryParams = { ...queryParams, state: state }; - if (labels && labels.length > 0) queryParams = { ...queryParams, labels: labels }; - queryParams = new URLSearchParams(queryParams).toString(); - router.push(`/issues/${anchor}?${queryParams}`); - }); + const handleClose = () => { + // if close logic is passed down, call that instead of the below logic + if (handlePeekClose) { + handlePeekClose(); + return; + } + + issueDetailStore.setPeekId(null); + let queryParams: any = { + board, + }; + if (priority && priority.length > 0) queryParams = { ...queryParams, priority: priority }; + if (state && state.length > 0) queryParams = { ...queryParams, state: state }; + if (labels && labels.length > 0) queryParams = { ...queryParams, labels: labels }; + queryParams = new URLSearchParams(queryParams).toString(); + router.push(`/issues/${anchor}?${queryParams}`); + }; useEffect(() => { if (peekId) { diff --git a/space/core/components/ui/not-found.tsx b/space/core/components/ui/not-found.tsx index 4b92386d679..a2535616d80 100644 --- a/space/core/components/ui/not-found.tsx +++ b/space/core/components/ui/not-found.tsx @@ -1,18 +1,11 @@ "use client"; import React from "react"; -import { Metadata } from "next"; import Image from "next/image"; -import Link from "next/link"; // ui -import { Button } from "@plane/ui"; // images import Image404 from "@/public/404.svg"; -export const metadata: Metadata = { - title: "404 - Page Not Found", -}; - export const PageNotFound = () => (
diff --git a/space/core/store/issue-detail.store.ts b/space/core/store/issue-detail.store.ts index 6a07a77782f..aa106b98a23 100644 --- a/space/core/store/issue-detail.store.ts +++ b/space/core/store/issue-detail.store.ts @@ -121,7 +121,7 @@ export class IssueDetailStore implements IIssueDetailStore { return issueDetails; } catch (e) { - console.error("error fetching issue details"); + console.error(`Error fetching issue details for issueId ${issueId}: `, e); } }; diff --git a/space/core/store/members.store.ts b/space/core/store/members.store.ts index 5ddb8c5c1aa..3de021e2c7e 100644 --- a/space/core/store/members.store.ts +++ b/space/core/store/members.store.ts @@ -51,13 +51,18 @@ export class MemberStore implements IIssueMemberStore { }; fetchMembers = async (anchor: string) => { - const membersResponse = await this.memberService.getAnchorMembers(anchor); - runInAction(() => { - this.memberMap = {}; - for (const member of membersResponse) { - set(this.memberMap, [member.member], member); - } - }); - return membersResponse; + try { + const membersResponse = await this.memberService.getAnchorMembers(anchor); + runInAction(() => { + this.memberMap = {}; + for (const member of membersResponse) { + set(this.memberMap, [member.member], member); + } + }); + return membersResponse; + } catch (error) { + console.error("Failed to fetch members:", error); + return []; + } }; } diff --git a/space/core/store/module.store.ts b/space/core/store/module.store.ts index 5078cc17204..6da1ab1f80e 100644 --- a/space/core/store/module.store.ts +++ b/space/core/store/module.store.ts @@ -51,13 +51,18 @@ export class ModuleStore implements IIssueModuleStore { }; fetchModules = async (anchor: string) => { - const modulesResponse = await this.moduleService.getModules(anchor); - runInAction(() => { - this.moduleMap = {}; - for (const issueModule of modulesResponse) { - set(this.moduleMap, [issueModule.id], issueModule); - } - }); - return modulesResponse; + try { + const modulesResponse = await this.moduleService.getModules(anchor); + runInAction(() => { + this.moduleMap = {}; + for (const issueModule of modulesResponse) { + set(this.moduleMap, [issueModule.id], issueModule); + } + }); + return modulesResponse; + } catch (error) { + console.error("Failed to fetch members:", error); + return []; + } }; }