[WEB-1255] chore: Required Spaces refactor (#5177)
* Changes required to enable Publish Views * default views to not found page * refactor exports * remove uncessary view service * fix review comments
This commit is contained in:
parent
2ee6cd20d8
commit
8577a56068
77 changed files with 2772 additions and 484 deletions
2
packages/types/src/view-props.d.ts
vendored
2
packages/types/src/view-props.d.ts
vendored
|
|
@ -202,4 +202,6 @@ export interface IssuePaginationOptions {
|
||||||
before?: string;
|
before?: string;
|
||||||
after?: string;
|
after?: string;
|
||||||
groupedBy?: TIssueGroupByOptions;
|
groupedBy?: TIssueGroupByOptions;
|
||||||
|
subGroupedBy?: TIssueGroupByOptions;
|
||||||
|
orderBy?: TIssueOrderByOptions;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
packages/types/src/views.d.ts
vendored
12
packages/types/src/views.d.ts
vendored
|
|
@ -25,9 +25,21 @@ export interface IProjectView {
|
||||||
workspace: string;
|
workspace: string;
|
||||||
logo_props: TLogoProps | undefined;
|
logo_props: TLogoProps | undefined;
|
||||||
is_locked: boolean;
|
is_locked: boolean;
|
||||||
|
anchor?: string;
|
||||||
owned_by: 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 TViewFiltersSortKey = "name" | "created_at" | "updated_at";
|
||||||
|
|
||||||
export type TViewFiltersSortBy = "asc" | "desc";
|
export type TViewFiltersSortBy = "asc" | "desc";
|
||||||
|
|
|
||||||
75
space/app/views/[anchor]/layout.tsx
Normal file
75
space/app/views/[anchor]/layout.tsx
Normal file
|
|
@ -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 <LogoSpinner />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden">
|
||||||
|
<div className="relative flex h-[60px] flex-shrink-0 select-none items-center border-b border-custom-border-300 bg-custom-sidebar-background-100">
|
||||||
|
<ViewNavbarRoot publishSettings={publishSettings} />
|
||||||
|
</div>
|
||||||
|
<div className="relative h-full w-full overflow-hidden bg-custom-background-90">{children}</div>
|
||||||
|
<a
|
||||||
|
href="https://plane.so"
|
||||||
|
className="fixed bottom-2.5 right-5 !z-[999999] flex items-center gap-1 rounded border border-custom-border-200 bg-custom-background-100 px-2 py-1 shadow-custom-shadow-2xs"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
<div className="relative grid h-6 w-6 place-items-center">
|
||||||
|
<Image src={planeLogo} alt="Plane logo" className="h-6 w-6" height="24" width="24" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs">
|
||||||
|
Powered by <span className="font-semibold">Plane Publish</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default IssuesLayout;
|
||||||
30
space/app/views/[anchor]/page.tsx
Normal file
30
space/app/views/[anchor]/page.tsx
Normal file
|
|
@ -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 <ViewLayoutsRoot peekId={peekId} publishSettings={publishSettings} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default IssuesPage;
|
||||||
10
space/ce/components/issue-layouts/root.tsx
Normal file
10
space/ce/components/issue-layouts/root.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
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) => <PageNotFound />;
|
||||||
8
space/ce/components/navbar/index.tsx
Normal file
8
space/ce/components/navbar/index.tsx
Normal file
|
|
@ -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) => <></>;
|
||||||
1
space/ce/hooks/store/index.ts
Normal file
1
space/ce/hooks/store/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./use-published-view";
|
||||||
5
space/ce/hooks/store/use-published-view.ts
Normal file
5
space/ce/hooks/store/use-published-view.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export const useView = () => ({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
fetchViewDetails: (anchor: string) => {},
|
||||||
|
viewData: {},
|
||||||
|
});
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export * from "./kanban";
|
export * from "./kanban/base-kanban-root";
|
||||||
export * from "./list";
|
export * from "./list/base-list-root";
|
||||||
export * from "./properties";
|
export * from "./properties";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<div className="relative flex h-screen w-full items-center justify-center">
|
||||||
|
<LogoSpinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getGroupIssueCount(undefined, undefined, false) === 0) {
|
||||||
|
return <div className="flex w-full h-full items-center justify-center">No Issues Found</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{props.children}</>;
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useMemo, 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<Props> = observer((props: Props) => {
|
||||||
|
const { anchor } = props;
|
||||||
|
// store hooks
|
||||||
|
const { groupedIssueIds, getIssueLoader, fetchNextPublicIssues, getGroupIssueCount, getPaginationData } = useIssue();
|
||||||
|
|
||||||
|
const displayProperties: IIssueDisplayProperties = useMemo(
|
||||||
|
() => ({
|
||||||
|
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<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IssueLayoutHOC getGroupIssueCount={getGroupIssueCount} getIssueLoader={getIssueLoader}>
|
||||||
|
<div
|
||||||
|
className={`horizontal-scrollbar scrollbar-lg relative flex h-full w-full bg-custom-background-90 overflow-x-auto overflow-y-hidden`}
|
||||||
|
ref={scrollableContainerRef}
|
||||||
|
>
|
||||||
|
<div className="relative h-full w-max min-w-full bg-custom-background-90">
|
||||||
|
<div className="h-full w-max">
|
||||||
|
<KanBan
|
||||||
|
groupedIssueIds={groupedIssueIds ?? {}}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
subGroupBy={null}
|
||||||
|
groupBy="state"
|
||||||
|
showEmptyGroup
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
loadMoreIssues={debouncedFetchMoreIssues}
|
||||||
|
getGroupIssueCount={getGroupIssueCount}
|
||||||
|
getPaginationData={getPaginationData}
|
||||||
|
getIssueLoader={getIssueLoader}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</IssueLayoutHOC>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -1,81 +1,101 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC } from "react";
|
import { MutableRefObject } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
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
|
// components
|
||||||
import { IssueBlockDueDate, IssueBlockPriority, IssueBlockState } from "@/components/issues";
|
import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/with-display-properties-HOC";
|
||||||
// helpers
|
// helpers
|
||||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssue, useIssueDetails, usePublish } from "@/hooks/store";
|
import { useIssueDetails, usePublish } from "@/hooks/store";
|
||||||
// interfaces
|
|
||||||
|
|
||||||
type Props = {
|
import { IIssue } from "@/types/issue";
|
||||||
anchor: string;
|
import { IssueProperties } from "../properties/all-properties";
|
||||||
|
import { getIssueBlockId } from "../utils";
|
||||||
|
|
||||||
|
interface IssueBlockProps {
|
||||||
issueId: string;
|
issueId: string;
|
||||||
};
|
groupId: string;
|
||||||
|
subGroupId: string;
|
||||||
|
displayProperties: IIssueDisplayProperties | undefined;
|
||||||
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
}
|
||||||
|
|
||||||
export const IssueKanBanBlock: FC<Props> = observer((props) => {
|
interface IssueDetailsBlockProps {
|
||||||
const { anchor, issueId } = props;
|
issue: IIssue;
|
||||||
const { getIssueById } = useIssue();
|
displayProperties: IIssueDisplayProperties | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((props) => {
|
||||||
|
const { issue, displayProperties } = props;
|
||||||
|
const { anchor } = useParams();
|
||||||
|
// hooks
|
||||||
|
const { project_details } = usePublish(anchor.toString());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2 px-3 py-2">
|
||||||
|
<WithDisplayPropertiesHOC displayProperties={displayProperties || {}} displayPropertyKey="key">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="line-clamp-1 text-xs text-custom-text-300">
|
||||||
|
{project_details?.identifier}-{issue.sequence_id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
|
||||||
|
<div className="w-full line-clamp-1 text-sm text-custom-text-100 mb-1.5">
|
||||||
|
<Tooltip tooltipContent={issue.name}>
|
||||||
|
<span>{issue.name}</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IssueProperties
|
||||||
|
className="flex flex-wrap items-center gap-2 whitespace-nowrap text-custom-text-300 pt-1.5"
|
||||||
|
issue={issue}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
||||||
|
const { issueId, groupId, subGroupId, displayProperties } = props;
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
// query params
|
// query params
|
||||||
const board = searchParams.get("board");
|
const board = searchParams.get("board");
|
||||||
const state = searchParams.get("state");
|
// hooks
|
||||||
const priority = searchParams.get("priority");
|
const { setPeekId, getIsIssuePeeked, getIssueById } = useIssueDetails();
|
||||||
const labels = searchParams.get("labels");
|
|
||||||
// store hooks
|
|
||||||
const { project_details } = usePublish(anchor);
|
|
||||||
const { setPeekId } = useIssueDetails();
|
|
||||||
|
|
||||||
const { queryParam } = queryParamGenerator({ board, peekId: issueId, priority, state, labels });
|
const handleIssuePeekOverview = () => {
|
||||||
|
|
||||||
const handleBlockClick = () => {
|
|
||||||
setPeekId(issueId);
|
setPeekId(issueId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { queryParam } = queryParamGenerator(board ? { board, peekId: issueId } : { peekId: issueId });
|
||||||
|
|
||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
|
|
||||||
if (!issue) return <></>;
|
if (!issue) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className={cn("group/kanban-block relative p-1.5")}>
|
||||||
<Link
|
<Link
|
||||||
href={`/issues/${anchor}?${queryParam}`}
|
id={getIssueBlockId(issueId, groupId, subGroupId)}
|
||||||
onClick={handleBlockClick}
|
className={cn(
|
||||||
className="flex flex-col gap-1.5 space-y-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs select-none"
|
"block rounded border-[1px] outline-[0.5px] outline-transparent w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
|
||||||
|
{ "border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issue.id) }
|
||||||
|
)}
|
||||||
|
href={`?${queryParam}`}
|
||||||
|
onClick={handleIssuePeekOverview}
|
||||||
>
|
>
|
||||||
{/* id */}
|
<KanbanIssueDetailsBlock issue={issue} displayProperties={displayProperties} />
|
||||||
<div className="break-words text-xs text-custom-text-300">
|
|
||||||
{project_details?.identifier}-{issue?.sequence_id}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* name */}
|
|
||||||
<h6 role="button" className="line-clamp-2 cursor-pointer break-words text-sm">
|
|
||||||
{issue.name}
|
|
||||||
</h6>
|
|
||||||
|
|
||||||
<div className="hide-horizontal-scrollbar relative flex w-full flex-grow items-end gap-2 overflow-x-scroll">
|
|
||||||
{/* priority */}
|
|
||||||
{issue?.priority && (
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<IssueBlockPriority priority={issue?.priority} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* state */}
|
|
||||||
{issue?.state_id && (
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<IssueBlockState stateId={issue?.state_id} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* due date */}
|
|
||||||
{issue?.target_date && (
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<IssueBlockDueDate due_date={issue?.target_date} stateId={issue?.state_id ?? undefined} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
KanbanIssueBlock.displayName = "KanbanIssueBlock";
|
||||||
|
|
|
||||||
|
|
@ -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<HTMLDivElement | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = 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 (
|
||||||
|
<KanbanIssueBlock
|
||||||
|
key={draggableId}
|
||||||
|
issueId={issueId}
|
||||||
|
groupId={groupId}
|
||||||
|
subGroupId={subGroupId}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
) : 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<HTMLDivElement | null>(null);
|
|
||||||
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(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 (
|
|
||||||
<div key={stateId} className="relative flex h-full w-[340px] flex-shrink-0 flex-col">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<IssueKanBanHeader stateId={stateId} />
|
|
||||||
</div>
|
|
||||||
<div className="hide-vertical-scrollbar h-full w-full overflow-hidden overflow-y-auto" ref={containerRef}>
|
|
||||||
{issueIds && issueIds.length > 0 ? (
|
|
||||||
<div className="space-y-3 px-2 pb-2">
|
|
||||||
{issueIds.map((issueId) => (
|
|
||||||
<IssueKanBanBlock key={issueId} anchor={anchor} issueId={issueId} />
|
|
||||||
))}
|
|
||||||
{shouldLoadMore && (
|
|
||||||
<div className="w-full h-[100px] bg-custom-background-80 animate-pulse" ref={setIntersectionElement} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center gap-2 pt-10 text-center text-sm font-medium text-custom-text-200">
|
|
||||||
<Icon iconName="stack" />
|
|
||||||
No issues in this state
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
130
space/core/components/issues/issue-layouts/kanban/default.tsx
Normal file
130
space/core/components/issues/issue-layouts/kanban/default.tsx
Normal file
|
|
@ -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<HTMLDivElement | null>;
|
||||||
|
showEmptyGroup?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KanBan: React.FC<IKanBan> = 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 (
|
||||||
|
<div className={`relative w-full flex gap-2 px-2 ${subGroupBy ? "h-full" : "h-full"}`}>
|
||||||
|
{groupList &&
|
||||||
|
groupList.length > 0 &&
|
||||||
|
groupList.map((subList: IGroupByColumn) => {
|
||||||
|
const groupByVisibilityToggle = visibilityGroupBy(subList);
|
||||||
|
|
||||||
|
if (groupByVisibilityToggle.showGroup === false) return <></>;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={subList.id}
|
||||||
|
className={`group relative flex flex-shrink-0 flex-col ${
|
||||||
|
groupByVisibilityToggle.showIssues ? `w-[350px]` : ``
|
||||||
|
} `}
|
||||||
|
>
|
||||||
|
{isNil(subGroupBy) && (
|
||||||
|
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
|
||||||
|
<HeaderGroupByCard
|
||||||
|
groupBy={groupBy}
|
||||||
|
icon={subList.icon}
|
||||||
|
title={subList.name}
|
||||||
|
count={getGroupIssueCount(subList.id, undefined, false) ?? 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{groupByVisibilityToggle.showIssues && (
|
||||||
|
<KanbanGroup
|
||||||
|
groupId={subList.id}
|
||||||
|
groupedIssueIds={groupedIssueIds}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
subGroupBy={subGroupBy}
|
||||||
|
subGroupId={subGroupId}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
loadMoreIssues={loadMoreIssues}
|
||||||
|
getGroupIssueCount={getGroupIssueCount}
|
||||||
|
getPaginationData={getPaginationData}
|
||||||
|
getIssueLoader={getIssueLoader}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -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<Props> = observer((props) => {
|
|
||||||
const { stateId } = props;
|
|
||||||
|
|
||||||
const { getStateById } = useStates();
|
|
||||||
const { getGroupIssueCount } = useIssue();
|
|
||||||
|
|
||||||
const state = getStateById(stateId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2 px-2 pb-2">
|
|
||||||
<div className="flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
|
|
||||||
<StateGroupIcon stateGroup={state?.group ?? "backlog"} color={state?.color} height="14" width="14" />
|
|
||||||
</div>
|
|
||||||
<div className="mr-1 truncate font-medium capitalize text-custom-text-200">{state?.name ?? "State"}</div>
|
|
||||||
<span className="flex-shrink-0 rounded-full text-custom-text-300">
|
|
||||||
{getGroupIssueCount(stateId, undefined, false) ?? 0}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -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<IHeaderGroupByCard> = observer((props) => {
|
||||||
|
const { icon, title, count } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={`relative flex flex-shrink-0 gap-2 p-1.5 w-full flex-row items-center`}>
|
||||||
|
<div className="flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center overflow-hidden rounded-sm">
|
||||||
|
{icon ? icon : <Circle width={14} strokeWidth={2} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`relative flex items-center gap-1 w-full flex-row overflow-hidden`}>
|
||||||
|
<div className={`line-clamp-1 inline-block overflow-hidden truncate font-medium text-custom-text-100`}>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className={`flex-shrink-0 text-sm font-medium text-custom-text-300 pl-2`}>{count || 0}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -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<IHeaderSubGroupByCard> = observer((props) => {
|
||||||
|
const { icon, title, count, isExpanded, toggleExpanded } = props;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative flex w-full flex-shrink-0 flex-row items-center gap-2 rounded-sm p-1.5 cursor-pointer`}
|
||||||
|
onClick={() => toggleExpanded()}
|
||||||
|
>
|
||||||
|
<div className="flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80">
|
||||||
|
{isExpanded ? <ChevronUp width={14} strokeWidth={2} /> : <ChevronDown width={14} strokeWidth={2} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center overflow-hidden rounded-sm">
|
||||||
|
{icon ? icon : <Circle width={14} strokeWidth={2} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-shrink-0 items-center gap-1 text-sm">
|
||||||
|
<div className="line-clamp-1 text-custom-text-100">{title}</div>
|
||||||
|
<div className="pl-2 text-sm font-medium text-custom-text-300">{count || 0}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -1,3 +1,2 @@
|
||||||
export * from "./block";
|
export * from "./block";
|
||||||
export * from "./header";
|
export * from "./blocks-list";
|
||||||
export * from "./root";
|
|
||||||
|
|
|
||||||
|
|
@ -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<HTMLDivElement | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loader components
|
||||||
|
const KanbanIssueBlockLoader = forwardRef<HTMLSpanElement>((props, ref) => (
|
||||||
|
<span ref={ref} className="block h-28 m-1.5 animate-pulse bg-custom-background-80 rounded" />
|
||||||
|
));
|
||||||
|
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<HTMLSpanElement | null>(null);
|
||||||
|
const columnRef = useRef<HTMLDivElement | null>(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 ? (
|
||||||
|
<KanbanIssueBlockLoader />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="w-full p-3 text-sm font-medium text-custom-primary-100 hover:text-custom-primary-200 hover:underline cursor-pointer"
|
||||||
|
onClick={loadMoreIssuesInThisGroup}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
Load More ↓
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldLoadMore = nextPageResults === undefined ? issueIds?.length < groupIssueCount : !!nextPageResults;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={`${groupId}__${subGroupId}`}
|
||||||
|
className={cn("relative h-full transition-all min-h-[120px]", { "vertical-scrollbar scrollbar-md": !subGroupBy })}
|
||||||
|
ref={columnRef}
|
||||||
|
>
|
||||||
|
<KanbanIssueBlocksList
|
||||||
|
subGroupId={subGroupId}
|
||||||
|
groupId={groupId}
|
||||||
|
issueIds={issueIds || []}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{shouldLoadMore && (isSubGroup ? <>{loadMore}</> : <KanbanIssueBlockLoader ref={setIntersectionElement} />)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -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<Props> = 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 (
|
|
||||||
<div className="relative flex h-full w-full gap-3 overflow-hidden overflow-x-auto">
|
|
||||||
{issueGroupIds?.map((stateId) => {
|
|
||||||
const issueIds = groupedIssues[stateId];
|
|
||||||
return <Column key={stateId} anchor={anchor} stateId={stateId} issueIds={issueIds} />;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
294
space/core/components/issues/issue-layouts/kanban/swimlanes.tsx
Normal file
294
space/core/components/issues/issue-layouts/kanban/swimlanes.tsx
Normal file
|
|
@ -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<HTMLDivElement | null>;
|
||||||
|
orderBy: TIssueOrderByOptions | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = 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 (
|
||||||
|
<div className="relative">
|
||||||
|
<div className="sticky top-0 z-[4] h-[50px] bg-custom-background-90 px-2">
|
||||||
|
<SubGroupSwimlaneHeader
|
||||||
|
groupBy={groupBy}
|
||||||
|
subGroupBy={subGroupBy}
|
||||||
|
groupList={groupByList}
|
||||||
|
showEmptyGroup={showEmptyGroup}
|
||||||
|
getGroupIssueCount={getGroupIssueCount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{subGroupBy && (
|
||||||
|
<SubGroupSwimlane
|
||||||
|
groupList={subGroupByList}
|
||||||
|
groupedIssueIds={groupedIssueIds}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
groupBy={groupBy}
|
||||||
|
subGroupBy={subGroupBy}
|
||||||
|
orderBy={orderBy}
|
||||||
|
loadMoreIssues={loadMoreIssues}
|
||||||
|
getGroupIssueCount={getGroupIssueCount}
|
||||||
|
getPaginationData={getPaginationData}
|
||||||
|
getIssueLoader={getIssueLoader}
|
||||||
|
showEmptyGroup={showEmptyGroup}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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<ISubGroupSwimlaneHeader> = observer(
|
||||||
|
({ subGroupBy, groupBy, groupList, showEmptyGroup, getGroupIssueCount }) => (
|
||||||
|
<div className="relative flex h-max min-h-full w-full items-center gap-2">
|
||||||
|
{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 (
|
||||||
|
<div key={`${subGroupBy}_${group.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
|
||||||
|
<HeaderGroupByCard groupBy={groupBy} icon={group.icon} title={group.name} count={groupCount} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
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<HTMLDivElement | null>;
|
||||||
|
loadMoreIssues: (groupId?: string, subGroupId?: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||||
|
const {
|
||||||
|
groupedIssueIds,
|
||||||
|
subGroupBy,
|
||||||
|
groupBy,
|
||||||
|
groupList,
|
||||||
|
displayProperties,
|
||||||
|
loadMoreIssues,
|
||||||
|
getGroupIssueCount,
|
||||||
|
getPaginationData,
|
||||||
|
getIssueLoader,
|
||||||
|
showEmptyGroup,
|
||||||
|
scrollableContainerRef,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-max min-h-full w-full">
|
||||||
|
{groupList &&
|
||||||
|
groupList.length > 0 &&
|
||||||
|
groupList.map((group: IGroupByColumn) => (
|
||||||
|
<SubGroup
|
||||||
|
key={group.id}
|
||||||
|
groupedIssueIds={groupedIssueIds}
|
||||||
|
subGroupBy={subGroupBy}
|
||||||
|
groupBy={groupBy}
|
||||||
|
group={group}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
loadMoreIssues={loadMoreIssues}
|
||||||
|
getGroupIssueCount={getGroupIssueCount}
|
||||||
|
getPaginationData={getPaginationData}
|
||||||
|
getIssueLoader={getIssueLoader}
|
||||||
|
showEmptyGroup={showEmptyGroup}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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<HTMLDivElement | null>;
|
||||||
|
loadMoreIssues: (groupId?: string, subGroupId?: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SubGroup: React.FC<ISubGroup> = 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 (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-shrink-0 flex-col">
|
||||||
|
<div className="sticky top-[50px] z-[3] py-1 flex w-full items-center bg-custom-background-100 border-y-[0.5px] border-custom-border-200">
|
||||||
|
<div className="sticky left-0 flex-shrink-0">
|
||||||
|
<HeaderSubGroupByCard
|
||||||
|
icon={group.icon}
|
||||||
|
title={group.name || ""}
|
||||||
|
count={issueCount}
|
||||||
|
isExpanded={isExpanded}
|
||||||
|
toggleExpanded={toggleExpanded}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{subGroupByVisibilityToggle.showIssues && isExpanded && (
|
||||||
|
<div className="relative">
|
||||||
|
<KanBan
|
||||||
|
groupedIssueIds={groupedIssueIds}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
subGroupBy={subGroupBy}
|
||||||
|
groupBy={groupBy}
|
||||||
|
subGroupId={group.id}
|
||||||
|
showEmptyGroup={showEmptyGroup}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
loadMoreIssues={loadMoreIssues}
|
||||||
|
getGroupIssueCount={getGroupIssueCount}
|
||||||
|
getPaginationData={getPaginationData}
|
||||||
|
getIssueLoader={getIssueLoader}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { useCallback, useMemo } 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 = useMemo(
|
||||||
|
() => ({
|
||||||
|
key: true,
|
||||||
|
state: true,
|
||||||
|
labels: true,
|
||||||
|
priority: true,
|
||||||
|
due_date: true,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadMoreIssues = useCallback(
|
||||||
|
(groupId?: string) => {
|
||||||
|
fetchNextPublicIssues(anchor, groupId);
|
||||||
|
},
|
||||||
|
[fetchNextPublicIssues]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IssueLayoutHOC getGroupIssueCount={getGroupIssueCount} getIssueLoader={getIssueLoader}>
|
||||||
|
<div className={`relative size-full bg-custom-background-90`}>
|
||||||
|
<List
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
groupBy={"state"}
|
||||||
|
groupedIssueIds={groupedIssueIds ?? {}}
|
||||||
|
loadMoreIssues={loadMoreIssues}
|
||||||
|
getGroupIssueCount={getGroupIssueCount}
|
||||||
|
getPaginationData={getPaginationData}
|
||||||
|
getIssueLoader={getIssueLoader}
|
||||||
|
showEmptyGroup
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</IssueLayoutHOC>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -1,88 +1,90 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { FC } from "react";
|
|
||||||
|
import { useRef } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useParams, useSearchParams } from "next/navigation";
|
||||||
// components
|
// types
|
||||||
import { IssueBlockDueDate, IssueBlockLabels, IssueBlockPriority, IssueBlockState } from "@/components/issues";
|
import { cn } from "@plane/editor";
|
||||||
|
import { IIssueDisplayProperties } from "@plane/types";
|
||||||
|
import { Tooltip } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||||
// hook
|
// hooks
|
||||||
import { useIssue, useIssueDetails, usePublish } from "@/hooks/store";
|
import { useIssueDetails, usePublish } from "@/hooks/store";
|
||||||
|
//
|
||||||
|
import { IssueProperties } from "../properties/all-properties";
|
||||||
|
|
||||||
type IssueListBlockProps = {
|
interface IssueBlockProps {
|
||||||
anchor: string;
|
|
||||||
issueId: string;
|
issueId: string;
|
||||||
};
|
groupId: string;
|
||||||
|
displayProperties: IIssueDisplayProperties | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export const IssueListLayoutBlock: FC<IssueListBlockProps> = observer((props) => {
|
export const IssueBlock = observer((props: IssueBlockProps) => {
|
||||||
const { anchor, issueId } = props;
|
const { anchor } = useParams();
|
||||||
const { getIssueById } = useIssue();
|
const { issueId, displayProperties } = props;
|
||||||
// query params
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const board = searchParams.get("board") || undefined;
|
// query params
|
||||||
const state = searchParams.get("state") || undefined;
|
const board = searchParams.get("board");
|
||||||
const priority = searchParams.get("priority") || undefined;
|
// ref
|
||||||
const labels = searchParams.get("labels") || undefined;
|
const issueRef = useRef<HTMLDivElement | null>(null);
|
||||||
// store hooks
|
// hooks
|
||||||
const { setPeekId } = useIssueDetails();
|
const { project_details } = usePublish(anchor.toString());
|
||||||
const { project_details } = usePublish(anchor);
|
const { getIsIssuePeeked, setPeekId, getIssueById } = useIssueDetails();
|
||||||
|
|
||||||
const { queryParam } = queryParamGenerator({ board, peekId: issueId, priority, state, labels });
|
const handleIssuePeekOverview = () => {
|
||||||
const handleBlockClick = () => {
|
|
||||||
setPeekId(issueId);
|
setPeekId(issueId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { queryParam } = queryParamGenerator(board ? { board, peekId: issueId } : { peekId: issueId });
|
||||||
|
|
||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
|
|
||||||
if (!issue) return <></>;
|
if (!issue) return null;
|
||||||
|
|
||||||
|
const projectIdentifier = project_details?.identifier;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<div
|
||||||
href={`/issues/${anchor}?${queryParam}`}
|
ref={issueRef}
|
||||||
onClick={handleBlockClick}
|
className={cn(
|
||||||
className="relative flex items-center gap-10 bg-custom-background-100 p-3"
|
"group/list-block min-h-11 relative flex flex-col md:flex-row md:items-center gap-3 bg-custom-background-100 hover:bg-custom-background-90 p-3 pl-1.5 text-sm transition-colors border-b border-b-custom-border-200",
|
||||||
|
{
|
||||||
|
"border-custom-primary-70": getIsIssuePeeked(issue.id),
|
||||||
|
"last:border-b-transparent": !getIsIssuePeeked(issue.id),
|
||||||
|
}
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative flex w-full flex-grow items-center gap-3 overflow-hidden">
|
<div className="flex w-full truncate">
|
||||||
{/* id */}
|
<div className="flex flex-grow items-center gap-0.5 truncate">
|
||||||
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
|
<div className="flex items-center gap-1">
|
||||||
{project_details?.identifier}-{issue?.sequence_id}
|
{displayProperties && displayProperties?.key && (
|
||||||
</div>
|
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300 px-4">
|
||||||
{/* name */}
|
{projectIdentifier}-{issue.sequence_id}
|
||||||
<div onClick={handleBlockClick} className="flex-grow cursor-pointer truncate text-sm">
|
|
||||||
{issue.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="inline-flex flex-shrink-0 items-center gap-2 text-xs">
|
|
||||||
{/* priority */}
|
|
||||||
{issue?.priority && (
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<IssueBlockPriority priority={issue?.priority} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* state */}
|
|
||||||
{issue?.state_id && (
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<IssueBlockState stateId={issue?.state_id} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* labels */}
|
|
||||||
{issue?.label_ids && issue?.label_ids.length > 0 && (
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<IssueBlockLabels labelIds={issue?.label_ids} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* due date */}
|
|
||||||
{issue?.target_date && (
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<IssueBlockDueDate due_date={issue?.target_date} stateId={issue?.state_id ?? undefined} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
id={`issue-${issue.id}`}
|
||||||
|
href={`?${queryParam}`}
|
||||||
|
onClick={handleIssuePeekOverview}
|
||||||
|
className="w-full truncate cursor-pointer text-sm text-custom-text-100"
|
||||||
|
>
|
||||||
|
<Tooltip tooltipContent={issue.name} position="top-left">
|
||||||
|
<p className="truncate">{issue.name}</p>
|
||||||
|
</Tooltip>
|
||||||
</Link>
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-shrink-0 items-center gap-2">
|
||||||
|
<IssueProperties
|
||||||
|
className="relative flex flex-wrap md:flex-grow md:flex-shrink-0 items-center gap-2 whitespace-nowrap"
|
||||||
|
issue={issue}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { FC, MutableRefObject } from "react";
|
||||||
|
// types
|
||||||
|
import { IIssueDisplayProperties } from "@plane/types";
|
||||||
|
import { IssueBlock } from "./block";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
issueIds: string[] | undefined;
|
||||||
|
groupId: string;
|
||||||
|
displayProperties?: IIssueDisplayProperties;
|
||||||
|
containerRef: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IssueBlocksList: FC<Props> = (props) => {
|
||||||
|
const { issueIds = [], groupId, displayProperties } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-full w-full">
|
||||||
|
{issueIds &&
|
||||||
|
issueIds?.length > 0 &&
|
||||||
|
issueIds.map((issueId: string) => (
|
||||||
|
<IssueBlock key={issueId} issueId={issueId} displayProperties={displayProperties} groupId={groupId} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
86
space/core/components/issues/issue-layouts/list/default.tsx
Normal file
86
space/core/components/issues/issue-layouts/list/default.tsx
Normal file
|
|
@ -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<IList> = observer((props) => {
|
||||||
|
const {
|
||||||
|
groupedIssueIds,
|
||||||
|
groupBy,
|
||||||
|
displayProperties,
|
||||||
|
showEmptyGroup,
|
||||||
|
loadMoreIssues,
|
||||||
|
getGroupIssueCount,
|
||||||
|
getPaginationData,
|
||||||
|
getIssueLoader,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(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 (
|
||||||
|
<div className="relative size-full flex flex-col">
|
||||||
|
{groupList && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="size-full vertical-scrollbar scrollbar-lg relative overflow-auto vertical-scrollbar-margin-top-md"
|
||||||
|
>
|
||||||
|
{groupList.map((group: IGroupByColumn) => (
|
||||||
|
<ListGroup
|
||||||
|
key={group.id}
|
||||||
|
groupIssueIds={groupedIssueIds?.[group.id]}
|
||||||
|
groupBy={groupBy}
|
||||||
|
group={group}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
showEmptyGroup={showEmptyGroup}
|
||||||
|
loadMoreIssues={loadMoreIssues}
|
||||||
|
getGroupIssueCount={getGroupIssueCount}
|
||||||
|
getPaginationData={getPaginationData}
|
||||||
|
getIssueLoader={getIssueLoader}
|
||||||
|
containerRef={containerRef}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -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 (
|
|
||||||
<div key={stateId} className="relative w-full">
|
|
||||||
<IssueListLayoutHeader stateId={stateId} />
|
|
||||||
{issueIds && issueIds.length > 0 ? (
|
|
||||||
<div className="divide-y divide-custom-border-200">
|
|
||||||
{issueIds.map((issueId) => (
|
|
||||||
<IssueListLayoutBlock key={issueId} anchor={anchor} issueId={issueId} />
|
|
||||||
))}
|
|
||||||
{isPaginating ? (
|
|
||||||
<div className="w-full h-[46px] bg-custom-background-80 animate-pulse" />
|
|
||||||
) : (
|
|
||||||
shouldLoadMore && (
|
|
||||||
<div
|
|
||||||
className="w-full min-h-[45px] bg-custom-background-100 p-3 text-sm border-b-[1px] cursor-pointer text-custom-text-350 hover:text-custom-text-300"
|
|
||||||
onClick={loadMoreIssuesInThisGroup}
|
|
||||||
>
|
|
||||||
Load More ↓
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="bg-custom-background-100 p-3 text-sm text-custom-text-400">No issues.</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -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<Props> = observer((props) => {
|
|
||||||
const { stateId } = props;
|
|
||||||
|
|
||||||
const { getStateById } = useStates();
|
|
||||||
const { getGroupIssueCount } = useIssue();
|
|
||||||
|
|
||||||
const state = getStateById(stateId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex sticky top-0 items-center gap-2 p-3 bg-custom-background-90 z-[1] border-b-[1px] border-custom-border-200">
|
|
||||||
<div className="flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<StateGroupIcon stateGroup={state?.group ?? "backlog"} color={state?.color} height="14" width="14" />
|
|
||||||
</div>
|
|
||||||
<div className="mr-1 font-medium capitalize">{state?.name}</div>
|
|
||||||
<div className="text-sm font-medium text-custom-text-200">
|
|
||||||
{getGroupIssueCount(stateId, undefined, false) ?? 0}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="group/list-header relative w-full flex-shrink-0 flex items-center gap-2 py-1.5"
|
||||||
|
onClick={() => toggleListGroup(groupID)}
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0 grid place-items-center overflow-hidden">
|
||||||
|
{icon ?? <CircleDashed className="size-3.5" strokeWidth={2} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative flex w-full flex-row items-center gap-1 overflow-hidden cursor-pointer">
|
||||||
|
<div className="inline-block line-clamp-1 truncate font-medium text-custom-text-100">{title}</div>
|
||||||
|
<div className="pl-2 text-sm font-medium text-custom-text-300">{count || 0}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -1,3 +1,2 @@
|
||||||
export * from "./block";
|
export * from "./block";
|
||||||
export * from "./header";
|
export * from "./blocks-list";
|
||||||
export * from "./root";
|
|
||||||
|
|
|
||||||
129
space/core/components/issues/issue-layouts/list/list-group.tsx
Normal file
129
space/core/components/issues/issue-layouts/list/list-group.tsx
Normal file
|
|
@ -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<HTMLDivElement | null>;
|
||||||
|
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<HTMLDivElement>((props, ref) => (
|
||||||
|
<div ref={ref} className="flex items-center justify-between h-11 p-3 border-b border-custom-border-200">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="h-5 w-10 bg-custom-background-80 rounded animate-pulse" />
|
||||||
|
<span className={`h-5 w-52 bg-custom-background-80 rounded animate-pulse`} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{[...Array(6)].map((_, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
<span key={index} className="h-5 w-5 bg-custom-background-80 rounded animate-pulse" />
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
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<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(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 ? (
|
||||||
|
<ListLoaderItemRow />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"h-11 relative flex items-center gap-3 bg-custom-background-100 border border-transparent border-t-custom-border-200 pl-6 p-3 text-sm font-medium text-custom-primary-100 hover:text-custom-primary-200 hover:underline cursor-pointer"
|
||||||
|
}
|
||||||
|
onClick={() => loadMoreIssues(group.id)}
|
||||||
|
>
|
||||||
|
Load More ↓
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
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) ? (
|
||||||
|
<div ref={groupRef} className={cn(`relative flex flex-shrink-0 flex-col border-[1px] border-transparent`)}>
|
||||||
|
<div className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 pl-2 pr-3 py-1">
|
||||||
|
<HeaderGroupByCard
|
||||||
|
groupID={group.id}
|
||||||
|
icon={group.icon}
|
||||||
|
title={group.name || ""}
|
||||||
|
count={groupIssueCount}
|
||||||
|
toggleListGroup={toggleListGroup}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{shouldExpand && (
|
||||||
|
<div className="relative">
|
||||||
|
{groupIssueIds && (
|
||||||
|
<IssueBlocksList
|
||||||
|
issueIds={groupIssueIds}
|
||||||
|
groupId={group.id}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
containerRef={containerRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{shouldLoadMore && (groupBy ? <>{loadMore}</> : <ListLoaderItemRow ref={setIntersectionElement} />)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : 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<Props> = 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 <Group key={stateId} anchor={anchor} stateId={stateId} issueIds={issueIds} />;
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -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<IIssueProperties> = 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 (
|
||||||
|
<div className={className}>
|
||||||
|
{/* basic properties */}
|
||||||
|
{/* state */}
|
||||||
|
{issue.state_id && (
|
||||||
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="state">
|
||||||
|
<div className="h-5">
|
||||||
|
<IssueBlockState stateId={issue.state_id} />
|
||||||
|
</div>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* priority */}
|
||||||
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="priority">
|
||||||
|
<div className="h-5">
|
||||||
|
<IssueBlockPriority priority={issue.priority} />
|
||||||
|
</div>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
|
||||||
|
{/* label */}
|
||||||
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="labels">
|
||||||
|
<div className="h-5">
|
||||||
|
<IssueBlockLabels labelIds={issue.label_ids} />
|
||||||
|
</div>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
|
||||||
|
{/* start date */}
|
||||||
|
{issue?.start_date && (
|
||||||
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="start_date">
|
||||||
|
<div className="h-5">
|
||||||
|
<IssueBlockDate
|
||||||
|
due_date={issue?.start_date}
|
||||||
|
stateId={issue?.state_id ?? undefined}
|
||||||
|
shouldHighLight={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* target/due date */}
|
||||||
|
{issue?.target_date && (
|
||||||
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="due_date">
|
||||||
|
<div className="h-5">
|
||||||
|
<IssueBlockDate due_date={issue?.target_date} stateId={issue?.state_id ?? undefined} />
|
||||||
|
</div>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* assignee */}
|
||||||
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="assignee">
|
||||||
|
<div className="h-5">
|
||||||
|
<IssueBlockMembers memberIds={issue.assignee_ids} />
|
||||||
|
</div>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
|
||||||
|
{/* modules */}
|
||||||
|
{issue.module_ids && issue.module_ids.length > 0 && (
|
||||||
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="modules">
|
||||||
|
<div className="h-5">
|
||||||
|
<IssueBlockModules moduleIds={issue.module_ids} />
|
||||||
|
</div>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* cycles */}
|
||||||
|
{issue.cycle_id && (
|
||||||
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="cycle">
|
||||||
|
<div className="h-5">
|
||||||
|
<IssueBlockCycle cycleId={issue.cycle_id} />
|
||||||
|
</div>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* estimates */}
|
||||||
|
{/* {projectId && areEstimateEnabledByProjectId(projectId?.toString()) && (
|
||||||
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="estimate">
|
||||||
|
<div className="h-5">
|
||||||
|
<EstimateDropdown
|
||||||
|
value={issue.estimate_point ?? undefined}
|
||||||
|
onChange={handleEstimate}
|
||||||
|
projectId={issue.project_id}
|
||||||
|
disabled={isReadOnly}
|
||||||
|
buttonVariant="border-with-text"
|
||||||
|
showTooltip
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
)} */}
|
||||||
|
|
||||||
|
{/* extra render properties */}
|
||||||
|
{/* sub-issues */}
|
||||||
|
<WithDisplayPropertiesHOC
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
displayPropertyKey="sub_issue_count"
|
||||||
|
shouldRenderProperty={(properties) => !!properties.sub_issue_count && !!issue.sub_issues_count}
|
||||||
|
>
|
||||||
|
<Tooltip tooltipHeading="Sub-issues" tooltipContent={`${issue.sub_issues_count}`}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1",
|
||||||
|
{
|
||||||
|
"hover:bg-custom-background-80 cursor-pointer": issue.sub_issues_count,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Layers className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
|
||||||
|
<div className="text-xs">{issue.sub_issues_count}</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
|
||||||
|
{/* attachments */}
|
||||||
|
<WithDisplayPropertiesHOC
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
displayPropertyKey="attachment_count"
|
||||||
|
shouldRenderProperty={(properties) => !!properties.attachment_count && !!issue.attachment_count}
|
||||||
|
>
|
||||||
|
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
|
||||||
|
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1">
|
||||||
|
<Paperclip className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
|
||||||
|
<div className="text-xs">{issue.attachment_count}</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
|
||||||
|
{/* link */}
|
||||||
|
<WithDisplayPropertiesHOC
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
displayPropertyKey="link"
|
||||||
|
shouldRenderProperty={(properties) => !!properties.link && !!issue.link_count}
|
||||||
|
>
|
||||||
|
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
|
||||||
|
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1">
|
||||||
|
<Link className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
|
||||||
|
<div className="text-xs">{issue.link_count}</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -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 (
|
||||||
|
<Tooltip tooltipHeading="Cycle" tooltipContent={cycle?.name ?? "No Cycle"}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-full items-center justify-between gap-1 rounded px-2.5 py-1 text-xs duration-300 focus:outline-none",
|
||||||
|
{ "border-[0.5px] border-custom-border-300": shouldShowBorder }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center text-xs gap-1.5">
|
||||||
|
<ContrastIcon className="h-3 w-3 flex-shrink-0" />
|
||||||
|
<div className="max-w-40 flex-grow truncate ">{cycle?.name ?? "No Cycle"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { CalendarCheck2 } from "lucide-react";
|
import { CalendarCheck2 } from "lucide-react";
|
||||||
|
import { Tooltip } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||||
|
|
@ -10,27 +11,31 @@ import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||||
import { useStates } from "@/hooks/store";
|
import { useStates } from "@/hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
due_date: string;
|
due_date: string | undefined;
|
||||||
stateId: string | undefined;
|
stateId: string | undefined;
|
||||||
|
shouldHighLight?: boolean;
|
||||||
|
shouldShowBorder?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueBlockDueDate = observer((props: Props) => {
|
export const IssueBlockDate = observer((props: Props) => {
|
||||||
const { due_date, stateId } = props;
|
const { due_date, stateId, shouldHighLight = true, shouldShowBorder = true } = props;
|
||||||
const { getStateById } = useStates();
|
const { getStateById } = useStates();
|
||||||
|
|
||||||
const state = getStateById(stateId);
|
const state = getStateById(stateId);
|
||||||
|
|
||||||
|
const formattedDate = renderFormattedDate(due_date);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Tooltip tooltipHeading="Due Date" tooltipContent={formattedDate}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("flex h-full items-center gap-1 rounded px-2.5 py-1 text-xs text-custom-text-100", {
|
||||||
"flex items-center gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs text-custom-text-100",
|
"text-red-500": shouldHighLight && due_date && shouldHighlightIssueDueDate(due_date, state?.group),
|
||||||
{
|
"border-[0.5px] border-custom-border-300": shouldShowBorder,
|
||||||
"text-red-500": shouldHighlightIssueDueDate(due_date, state?.group),
|
})}
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<CalendarCheck2 className="size-3 flex-shrink-0" />
|
<CalendarCheck2 className="size-3 flex-shrink-0" />
|
||||||
{renderFormattedDate(due_date)}
|
{formattedDate ? formattedDate : "No Date"}
|
||||||
</div>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,7 @@ export * from "./due-date";
|
||||||
export * from "./labels";
|
export * from "./labels";
|
||||||
export * from "./priority";
|
export * from "./priority";
|
||||||
export * from "./state";
|
export * from "./state";
|
||||||
|
export * from "./cycle";
|
||||||
|
export * from "./member";
|
||||||
|
export * from "./modules";
|
||||||
|
export * from "./all-properties";
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,69 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { Tags } from "lucide-react";
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
import { useLabel } from "@/hooks/store";
|
import { useLabel } from "@/hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
labelIds: string[];
|
labelIds: string[];
|
||||||
|
shouldShowLabel?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueBlockLabels = observer(({ labelIds }: Props) => {
|
export const IssueBlockLabels = observer(({ labelIds, shouldShowLabel = false }: Props) => {
|
||||||
const { getLabelsByIds } = useLabel();
|
const { getLabelsByIds } = useLabel();
|
||||||
|
|
||||||
const labels = getLabelsByIds(labelIds);
|
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";
|
||||||
|
|
||||||
|
if (labels.length <= 0)
|
||||||
|
return (
|
||||||
|
<Tooltip position="top" tooltipHeading="Labels" tooltipContent="None">
|
||||||
|
<div
|
||||||
|
className={`flex h-full items-center justify-center gap-2 rounded px-2.5 py-1 text-xs border-[0.5px] border-custom-border-300`}
|
||||||
|
>
|
||||||
|
<Tags className="h-3.5 w-3.5" strokeWidth={2} />
|
||||||
|
{shouldShowLabel && <span>No Labels</span>}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-wrap items-center gap-1">
|
<div className="flex h-5 w-full flex-wrap items-center gap-2 overflow-hidden">
|
||||||
{labels.length === 1 ? (
|
{labels.length <= 2 ? (
|
||||||
|
<>
|
||||||
|
{labels.map((label) => (
|
||||||
|
<Tooltip key={label.id} position="top" tooltipHeading="Labels" tooltipContent={label?.name ?? ""}>
|
||||||
<div
|
<div
|
||||||
key={labels[0].id}
|
key={label?.id}
|
||||||
className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
|
className={`flex overflow-hidden h-full max-w-full flex-shrink-0 items-center rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
<div className="flex max-w-full items-center gap-1.5 overflow-hidden text-custom-text-200">
|
||||||
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: `${labels[0].color}` }} />
|
<span
|
||||||
<div className="text-xs">{labels[0].name}</div>
|
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||||
</div>
|
style={{
|
||||||
</div>
|
backgroundColor: label?.color ?? "#000000",
|
||||||
) : (
|
}}
|
||||||
<Tooltip tooltipContent={labelsString}>
|
/>
|
||||||
<div className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm">
|
<div className="line-clamp-1 inline-block w-auto max-w-[100px] truncate">{label?.name}</div>
|
||||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
|
||||||
<div className="text-xs">{labels.length} Labels</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={`flex h-full flex-shrink-0 items-center rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs cursor-not-allowed"
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Tooltip position="top" tooltipHeading="Labels" tooltipContent={labelsString}>
|
||||||
|
<div className="flex h-full items-center gap-1.5 text-custom-text-200">
|
||||||
|
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-custom-primary" />
|
||||||
|
{`${labels.length} Labels`}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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<AvatarProps> = observer((props: AvatarProps) => {
|
||||||
|
const { showTooltip, members, icon: Icon } = props;
|
||||||
|
|
||||||
|
if (Array.isArray(members)) {
|
||||||
|
if (members.length > 1) {
|
||||||
|
return (
|
||||||
|
<AvatarGroup size="md" showTooltip={!showTooltip}>
|
||||||
|
{members.map((member) => {
|
||||||
|
if (!member) return;
|
||||||
|
return <Avatar key={member.id} src={member.member__avatar} name={member.member__display_name} />;
|
||||||
|
})}
|
||||||
|
</AvatarGroup>
|
||||||
|
);
|
||||||
|
} else if (members.length === 1) {
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
src={members[0].member__avatar}
|
||||||
|
name={members[0].member__display_name}
|
||||||
|
size="md"
|
||||||
|
showTooltip={!showTooltip}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Icon ? <Icon className="h-3 w-3 flex-shrink-0" /> : <Users className="h-3 w-3 mx-[4px] flex-shrink-0" />;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const IssueBlockMembers = observer(({ memberIds, shouldShowBorder = true }: Props) => {
|
||||||
|
const { getMembersByIds } = useMember();
|
||||||
|
|
||||||
|
const members = getMembersByIds(memberIds);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-full flex flex-wrap items-center gap-1">
|
||||||
|
<div
|
||||||
|
className={cn("flex flex-shrink-0 cursor-default items-center rounded-md text-xs", {
|
||||||
|
"border-[0.5px] border-custom-border-300 px-2.5 py-1": shouldShowBorder && !members?.length,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||||
|
<ButtonAvatars members={members} showTooltip={false} />
|
||||||
|
{!shouldShowBorder && members.length <= 1 && (
|
||||||
|
<span>{members?.[0]?.member__display_name ?? "No Assignees"}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -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 (
|
||||||
|
<div className="relative flex h-full flex-wrap items-center gap-1">
|
||||||
|
<Tooltip tooltipHeading="Modules" tooltipContent={modulesString}>
|
||||||
|
{modules.length <= 1 ? (
|
||||||
|
<div
|
||||||
|
key={modules?.[0]?.id}
|
||||||
|
className={cn("flex h-full flex-shrink-0 cursor-default items-center rounded-md px-2.5 py-1 text-xs", {
|
||||||
|
"border-[0.5px] border-custom-border-300": shouldShowBorder,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||||
|
<DiceIcon className="h-3 w-3 flex-shrink-0" />
|
||||||
|
<div className="text-xs">{modules?.[0]?.name ?? "No Modules"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs">
|
||||||
|
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||||
|
<div className="text-xs">{modules.length} Modules</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -2,17 +2,29 @@
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import { TIssuePriorities } from "@plane/types";
|
import { TIssuePriorities } from "@plane/types";
|
||||||
|
import { Tooltip } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
import { issuePriorityFilter } from "@/constants/issue";
|
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;
|
const priority_detail = priority != null ? issuePriorityFilter(priority) : null;
|
||||||
|
|
||||||
if (priority_detail === null) return <></>;
|
if (priority_detail === null) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`grid h-6 w-6 place-items-center rounded border-[0.5px] ${priority_detail?.className}`}>
|
<Tooltip tooltipHeading="Priority" tooltipContent={priority_detail?.title}>
|
||||||
|
<div className="flex items-center relative w-full h-full">
|
||||||
|
<div className={`grid h-5 w-5 place-items-center rounded border-[0.5px] gap-2 ${priority_detail?.className}`}>
|
||||||
<span className="material-symbols-rounded text-sm">{priority_detail?.icon}</span>
|
<span className="material-symbols-rounded text-sm">{priority_detail?.icon}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{shouldShowName && <span className="pl-2 text-sm">{priority_detail?.title}</span>}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,32 @@
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// ui
|
// ui
|
||||||
import { StateGroupIcon } from "@plane/ui";
|
import { cn } from "@plane/editor";
|
||||||
|
import { StateGroupIcon, Tooltip } from "@plane/ui";
|
||||||
//hooks
|
//hooks
|
||||||
import { useStates } from "@/hooks/store";
|
import { useStates } from "@/hooks/store";
|
||||||
|
|
||||||
type Props = {
|
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 { getStateById } = useStates();
|
||||||
|
|
||||||
const state = getStateById(stateId);
|
const state = getStateById(stateId);
|
||||||
|
|
||||||
if (!state) return <></>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs shadow-sm duration-300 focus:outline-none">
|
<Tooltip tooltipHeading="State" tooltipContent={state?.name ?? "State"}>
|
||||||
|
<div
|
||||||
|
className={cn("flex h-full w-full items-center justify-between gap-1 rounded px-2.5 py-1 text-xs", {
|
||||||
|
"border-[0.5px] border-custom-border-300": shouldShowBorder,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div className="flex w-full items-center gap-1.5">
|
<div className="flex w-full items-center gap-1.5">
|
||||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
<StateGroupIcon stateGroup={state?.group ?? "backlog"} color={state?.color} />
|
||||||
<div className="text-xs">{state?.name}</div>
|
<div className="text-xs">{state?.name ?? "State"}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
240
space/core/components/issues/issue-layouts/utils.tsx
Normal file
240
space/core/components/issues/issue-layouts/utils.tsx
Normal file
|
|
@ -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: <CycleGroupIcon cycleGroup={cycleStatus as TCycleGroups} className="h-3.5 w-3.5" />,
|
||||||
|
payload: { cycle_id: cycle.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cycleGroups.push({
|
||||||
|
id: "None",
|
||||||
|
name: "None",
|
||||||
|
icon: <ContrastIcon className="h-3.5 w-3.5" />,
|
||||||
|
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: <DiceIcon className="h-3.5 w-3.5" />,
|
||||||
|
payload: { module_ids: [moduleInfo.id] },
|
||||||
|
});
|
||||||
|
}) as any;
|
||||||
|
moduleGroups.push({
|
||||||
|
id: "None",
|
||||||
|
name: "None",
|
||||||
|
icon: <DiceIcon className="h-3.5 w-3.5" />,
|
||||||
|
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: (
|
||||||
|
<div className="h-3.5 w-3.5 rounded-full">
|
||||||
|
<StateGroupIcon stateGroup={state.group} color={state.color} width="14" height="14" />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
payload: { state_id: state.id },
|
||||||
|
})) as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPriorityColumns = () => {
|
||||||
|
const priorities = ISSUE_PRIORITIES;
|
||||||
|
|
||||||
|
return priorities.map((priority) => ({
|
||||||
|
id: priority.key,
|
||||||
|
name: priority.title,
|
||||||
|
icon: <PriorityIcon priority={priority?.key} />,
|
||||||
|
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: (
|
||||||
|
<div className="h-[12px] w-[12px] rounded-full" style={{ backgroundColor: label.color ? label.color : "#666" }} />
|
||||||
|
),
|
||||||
|
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: <Avatar name={member?.member__display_name} src={undefined} size="md" />,
|
||||||
|
payload: { assignee_ids: [member.id] },
|
||||||
|
}));
|
||||||
|
|
||||||
|
assigneeColumns.push({ id: "None", name: "None", icon: <Avatar size="md" />, 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: <Avatar name={member?.member__display_name} src={undefined} size="md" />,
|
||||||
|
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 = <T,>(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;
|
||||||
|
};
|
||||||
|
|
@ -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}</>;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
@ -6,16 +6,17 @@ import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// components
|
// components
|
||||||
import { FullScreenPeekView, SidePeekView } from "@/components/issues/peek-overview";
|
import { FullScreenPeekView, SidePeekView } from "@/components/issues/peek-overview";
|
||||||
// store
|
// hooks
|
||||||
import { useIssue, useIssueDetails } from "@/hooks/store";
|
import { useIssue, useIssueDetails } from "@/hooks/store";
|
||||||
|
|
||||||
type TIssuePeekOverview = {
|
type TIssuePeekOverview = {
|
||||||
anchor: string;
|
anchor: string;
|
||||||
peekId: string;
|
peekId: string;
|
||||||
|
handlePeekClose?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssuePeekOverview: FC<TIssuePeekOverview> = observer((props) => {
|
export const IssuePeekOverview: FC<TIssuePeekOverview> = observer((props) => {
|
||||||
const { anchor, peekId } = props;
|
const { anchor, peekId, handlePeekClose } = props;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
// query params
|
// query params
|
||||||
|
|
@ -34,13 +35,17 @@ export const IssuePeekOverview: FC<TIssuePeekOverview> = observer((props) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (anchor && peekId && issueStore.groupedIssueIds) {
|
if (anchor && peekId && issueStore.groupedIssueIds) {
|
||||||
if (!issueDetails) {
|
|
||||||
issueDetailStore.fetchIssueDetails(anchor, peekId.toString());
|
issueDetailStore.fetchIssueDetails(anchor, peekId.toString());
|
||||||
}
|
}
|
||||||
}
|
}, [anchor, issueDetailStore, peekId, issueStore.groupedIssueIds]);
|
||||||
}, [anchor, issueDetailStore, issueDetails, peekId, issueStore.groupedIssueIds]);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
// if close logic is passed down, call that instead of the below logic
|
||||||
|
if (handlePeekClose) {
|
||||||
|
handlePeekClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
issueDetailStore.setPeekId(null);
|
issueDetailStore.setPeekId(null);
|
||||||
let queryParams: any = {
|
let queryParams: any = {
|
||||||
board,
|
board,
|
||||||
|
|
|
||||||
26
space/core/components/ui/not-found.tsx
Normal file
26
space/core/components/ui/not-found.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
// ui
|
||||||
|
// images
|
||||||
|
import Image404 from "@/public/404.svg";
|
||||||
|
|
||||||
|
export const PageNotFound = () => (
|
||||||
|
<div className={`h-screen w-full overflow-hidden bg-custom-background-100`}>
|
||||||
|
<div className="grid h-full place-items-center p-4">
|
||||||
|
<div className="space-y-8 text-center">
|
||||||
|
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">
|
||||||
|
<Image src={Image404} layout="fill" alt="404- Page not found" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-lg font-semibold">Oops! Something went wrong.</h3>
|
||||||
|
<p className="text-sm text-custom-text-200">
|
||||||
|
Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
|
||||||
|
temporarily unavailable.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
@ -76,3 +76,14 @@ export const issuePriorityFilter = (priorityKey: TIssuePriorities): TIssueFilter
|
||||||
if (currentIssuePriority) return currentIssuePriority;
|
if (currentIssuePriority) return currentIssuePriority;
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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" },
|
||||||
|
];
|
||||||
|
|
@ -7,3 +7,6 @@ export * from "./use-issue-details";
|
||||||
export * from "./use-issue-filter";
|
export * from "./use-issue-filter";
|
||||||
export * from "./use-state";
|
export * from "./use-state";
|
||||||
export * from "./use-label";
|
export * from "./use-label";
|
||||||
|
export * from "./use-cycle";
|
||||||
|
export * from "./use-module";
|
||||||
|
export * from "./use-member";
|
||||||
|
|
|
||||||
11
space/core/hooks/store/use-cycle.ts
Normal file
11
space/core/hooks/store/use-cycle.ts
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
11
space/core/hooks/store/use-member.ts
Normal file
11
space/core/hooks/store/use-member.ts
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
11
space/core/hooks/store/use-module.ts
Normal file
11
space/core/hooks/store/use-module.ts
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
17
space/core/services/cycle.service.ts
Normal file
17
space/core/services/cycle.service.ts
Normal file
|
|
@ -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<TPublicCycle[]> {
|
||||||
|
return this.get(`api/public/anchor/${anchor}/cycles/`)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
17
space/core/services/member.service.ts
Normal file
17
space/core/services/member.service.ts
Normal file
|
|
@ -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<TPublicMember[]> {
|
||||||
|
return this.get(`api/public/anchor/${anchor}/members/`)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
17
space/core/services/module.service.ts
Normal file
17
space/core/services/module.service.ts
Normal file
|
|
@ -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<TPublicModule[]> {
|
||||||
|
return this.get(`api/public/anchor/${anchor}/modules/`)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
40
space/core/store/cycle.store.ts
Normal file
40
space/core/store/cycle.store.ts
Normal file
|
|
@ -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<TPublicCycle[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import concat from "lodash/concat";
|
import concat from "lodash/concat";
|
||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
import isEmpty from "lodash/isEmpty";
|
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
import uniq from "lodash/uniq";
|
import uniq from "lodash/uniq";
|
||||||
import update from "lodash/update";
|
import update from "lodash/update";
|
||||||
|
|
@ -23,7 +22,6 @@ import {
|
||||||
// services
|
// services
|
||||||
import IssueService from "@/services/issue.service";
|
import IssueService from "@/services/issue.service";
|
||||||
import { IIssue, TIssuesResponse } from "@/types/issue";
|
import { IIssue, TIssuesResponse } from "@/types/issue";
|
||||||
import { IIssueFilterStore } from "../issue-filters.store";
|
|
||||||
import { CoreRootStore } from "../root.store";
|
import { CoreRootStore } from "../root.store";
|
||||||
// constants
|
// constants
|
||||||
// helpers
|
// helpers
|
||||||
|
|
@ -39,14 +37,9 @@ export enum EIssueGroupedAction {
|
||||||
export interface IBaseIssuesStore {
|
export interface IBaseIssuesStore {
|
||||||
// observable
|
// observable
|
||||||
loader: Record<string, TLoader>;
|
loader: Record<string, TLoader>;
|
||||||
issuesMap: Record<string, IIssue>; // Record defines issue_id as key and IIssue as value
|
|
||||||
// actions
|
// actions
|
||||||
addIssue(issues: IIssue[], shouldReplace?: boolean): void;
|
addIssue(issues: IIssue[], shouldReplace?: boolean): void;
|
||||||
// helper methods
|
// helper methods
|
||||||
getIssueById(issueId: string): undefined | IIssue;
|
|
||||||
|
|
||||||
fetchIssueById(anchorId: string, issueId: string): Promise<IIssue | undefined>;
|
|
||||||
|
|
||||||
groupedIssueIds: TGroupedIssues | TSubGroupedIssues | undefined; // object to store Issue Ids based on group or subgroup
|
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
|
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
|
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<string, TLoader> = {};
|
loader: Record<string, TLoader> = {};
|
||||||
groupedIssueIds: TIssues | undefined = undefined;
|
groupedIssueIds: TIssues | undefined = undefined;
|
||||||
issuePaginationData: TIssuePaginationData = {};
|
issuePaginationData: TIssuePaginationData = {};
|
||||||
issuesMap: Record<string, IIssue> = {}; // Record defines issue_id as key and TIssue as value
|
|
||||||
groupedIssueCount: TGroupedIssueCount = {};
|
groupedIssueCount: TGroupedIssueCount = {};
|
||||||
//
|
//
|
||||||
paginationOptions: IssuePaginationOptions | undefined = undefined;
|
paginationOptions: IssuePaginationOptions | undefined = undefined;
|
||||||
|
|
@ -87,9 +79,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
issueService;
|
issueService;
|
||||||
// root store
|
// root store
|
||||||
rootIssueStore;
|
rootIssueStore;
|
||||||
issueFilterStore;
|
|
||||||
|
|
||||||
constructor(_rootStore: CoreRootStore, issueFilterStore: IIssueFilterStore) {
|
constructor(_rootStore: CoreRootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
// observable
|
// observable
|
||||||
loader: observable,
|
loader: observable,
|
||||||
|
|
@ -107,7 +98,6 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
setLoader: action.bound,
|
setLoader: action.bound,
|
||||||
});
|
});
|
||||||
this.rootIssueStore = _rootStore;
|
this.rootIssueStore = _rootStore;
|
||||||
this.issueFilterStore = issueFilterStore;
|
|
||||||
this.issueService = new IssueService();
|
this.issueService = new IssueService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,35 +131,12 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||||
if (issues && issues.length <= 0) return;
|
if (issues && issues.length <= 0) return;
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
issues.forEach((issue) => {
|
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
|
* Store the pagination data required for next subsequent issue pagination calls
|
||||||
* @param prevCursor cursor value of previous page
|
* @param prevCursor cursor value of previous page
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,16 @@ export const getPaginationParams = (
|
||||||
paginationParams.group_by = EIssueGroupByToServerOptions[options.groupedBy];
|
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 before and after dates are sent from option to filter by then, add them to filter the options
|
||||||
if (options.after && options.before) {
|
if (options.after && options.before) {
|
||||||
paginationParams["target_date"] = `${options.after};after,${options.before};before`;
|
paginationParams["target_date"] = `${options.after};after,${options.before};before`;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
import isEmpty from "lodash/isEmpty";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
import { makeObservable, observable, action, runInAction } from "mobx";
|
import { makeObservable, observable, action, runInAction } from "mobx";
|
||||||
|
import { computedFn } from "mobx-utils";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
// services
|
// services
|
||||||
import IssueService from "@/services/issue.service";
|
import IssueService from "@/services/issue.service";
|
||||||
|
|
@ -17,7 +19,10 @@ export interface IIssueDetailStore {
|
||||||
details: {
|
details: {
|
||||||
[key: string]: IIssue;
|
[key: string]: IIssue;
|
||||||
};
|
};
|
||||||
|
// computed actions
|
||||||
|
getIsIssuePeeked: (issueID: string) => boolean;
|
||||||
// actions
|
// actions
|
||||||
|
getIssueById: (issueId: string) => IIssue | undefined;
|
||||||
setPeekId: (issueID: string | null) => void;
|
setPeekId: (issueID: string | null) => void;
|
||||||
setPeekMode: (mode: IPeekMode) => void;
|
setPeekMode: (mode: IPeekMode) => void;
|
||||||
// issue actions
|
// issue actions
|
||||||
|
|
@ -88,6 +93,38 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||||
this.peekMode = mode;
|
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 for issueId ${issueId}: `, e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description fetc
|
* @description fetc
|
||||||
* @param {string} anchor
|
* @param {string} anchor
|
||||||
|
|
@ -98,7 +135,7 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
this.error = null;
|
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);
|
const commentsResponse = await this.issueService.getIssueComments(anchor, issueID);
|
||||||
|
|
||||||
if (issueDetails) {
|
if (issueDetails) {
|
||||||
|
|
@ -120,11 +157,11 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||||
|
|
||||||
addIssueComment = async (anchor: string, issueID: string, data: any) => {
|
addIssueComment = async (anchor: string, issueID: string, data: any) => {
|
||||||
try {
|
try {
|
||||||
const issueDetails = this.rootStore.issue.getIssueById(issueID);
|
const issueDetails = this.getIssueById(issueID);
|
||||||
const issueCommentResponse = await this.issueService.createIssueComment(anchor, issueID, data);
|
const issueCommentResponse = await this.issueService.createIssueComment(anchor, issueID, data);
|
||||||
if (issueDetails) {
|
if (issueDetails) {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(this.details, [issueID, "comments"], [...this.details[issueID].comments, issueCommentResponse]);
|
set(this.details, [issueID, "comments"], [...(issueDetails?.comments ?? []), issueCommentResponse]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return issueCommentResponse;
|
return issueCommentResponse;
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export class IssueStore extends BaseIssuesStore implements IIssueStore {
|
||||||
issueService: IssueService;
|
issueService: IssueService;
|
||||||
|
|
||||||
constructor(_rootStore: CoreRootStore) {
|
constructor(_rootStore: CoreRootStore) {
|
||||||
super(_rootStore, _rootStore.issueFilter);
|
super(_rootStore);
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
// actions
|
// actions
|
||||||
fetchPublicIssues: action,
|
fetchPublicIssues: action,
|
||||||
|
|
|
||||||
68
space/core/store/members.store.ts
Normal file
68
space/core/store/members.store.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
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<TPublicMember[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MemberStore implements IIssueMemberStore {
|
||||||
|
memberMap: Record<string, TPublicMember> = {};
|
||||||
|
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) => {
|
||||||
|
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 [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
68
space/core/store/module.store.ts
Normal file
68
space/core/store/module.store.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
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<TPublicModule[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ModuleStore implements IIssueModuleStore {
|
||||||
|
moduleMap: Record<string, TPublicModule> = {};
|
||||||
|
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) => {
|
||||||
|
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 [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -4,9 +4,12 @@ import { IInstanceStore, InstanceStore } from "@/store/instance.store";
|
||||||
import { IssueDetailStore, IIssueDetailStore } from "@/store/issue-detail.store";
|
import { IssueDetailStore, IIssueDetailStore } from "@/store/issue-detail.store";
|
||||||
import { IssueStore, IIssueStore } from "@/store/issue.store";
|
import { IssueStore, IIssueStore } from "@/store/issue.store";
|
||||||
import { IUserStore, UserStore } from "@/store/user.store";
|
import { IUserStore, UserStore } from "@/store/user.store";
|
||||||
|
import { CycleStore, ICycleStore } from "./cycle.store";
|
||||||
import { IssueFilterStore, IIssueFilterStore } from "./issue-filters.store";
|
import { IssueFilterStore, IIssueFilterStore } from "./issue-filters.store";
|
||||||
import { IIssueLabelStore, LabelStore } from "./label.store";
|
import { IIssueLabelStore, LabelStore } from "./label.store";
|
||||||
|
import { IIssueMemberStore, MemberStore } from "./members.store";
|
||||||
import { IMentionsStore, MentionsStore } from "./mentions.store";
|
import { IMentionsStore, MentionsStore } from "./mentions.store";
|
||||||
|
import { IIssueModuleStore, ModuleStore } from "./module.store";
|
||||||
import { IPublishListStore, PublishListStore } from "./publish/publish_list.store";
|
import { IPublishListStore, PublishListStore } from "./publish/publish_list.store";
|
||||||
import { IStateStore, StateStore } from "./state.store";
|
import { IStateStore, StateStore } from "./state.store";
|
||||||
|
|
||||||
|
|
@ -20,6 +23,9 @@ export class CoreRootStore {
|
||||||
mentionStore: IMentionsStore;
|
mentionStore: IMentionsStore;
|
||||||
state: IStateStore;
|
state: IStateStore;
|
||||||
label: IIssueLabelStore;
|
label: IIssueLabelStore;
|
||||||
|
module: IIssueModuleStore;
|
||||||
|
member: IIssueMemberStore;
|
||||||
|
cycle: ICycleStore;
|
||||||
issueFilter: IIssueFilterStore;
|
issueFilter: IIssueFilterStore;
|
||||||
publishList: IPublishListStore;
|
publishList: IPublishListStore;
|
||||||
|
|
||||||
|
|
@ -31,6 +37,9 @@ export class CoreRootStore {
|
||||||
this.mentionStore = new MentionsStore(this);
|
this.mentionStore = new MentionsStore(this);
|
||||||
this.state = new StateStore(this);
|
this.state = new StateStore(this);
|
||||||
this.label = new LabelStore(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.issueFilter = new IssueFilterStore(this);
|
||||||
this.publishList = new PublishListStore(this);
|
this.publishList = new PublishListStore(this);
|
||||||
}
|
}
|
||||||
|
|
@ -51,6 +60,9 @@ export class CoreRootStore {
|
||||||
this.mentionStore = new MentionsStore(this);
|
this.mentionStore = new MentionsStore(this);
|
||||||
this.state = new StateStore(this);
|
this.state = new StateStore(this);
|
||||||
this.label = new LabelStore(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.issueFilter = new IssueFilterStore(this);
|
||||||
this.publishList = new PublishListStore(this);
|
this.publishList = new PublishListStore(this);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
space/core/types/cycle.d.ts
vendored
Normal file
5
space/core/types/cycle.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export type TPublicCycle = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
6
space/core/types/issue.d.ts
vendored
6
space/core/types/issue.d.ts
vendored
|
|
@ -37,6 +37,8 @@ export interface IIssue
|
||||||
extends Pick<
|
extends Pick<
|
||||||
TIssue,
|
TIssue,
|
||||||
| "description_html"
|
| "description_html"
|
||||||
|
| "created_at"
|
||||||
|
| "updated_at"
|
||||||
| "created_by"
|
| "created_by"
|
||||||
| "id"
|
| "id"
|
||||||
| "name"
|
| "name"
|
||||||
|
|
@ -51,6 +53,10 @@ export interface IIssue
|
||||||
| "module_ids"
|
| "module_ids"
|
||||||
| "label_ids"
|
| "label_ids"
|
||||||
| "assignee_ids"
|
| "assignee_ids"
|
||||||
|
| "attachment_count"
|
||||||
|
| "sub_issues_count"
|
||||||
|
| "link_count"
|
||||||
|
| "estimate_point"
|
||||||
> {
|
> {
|
||||||
comments: Comment[];
|
comments: Comment[];
|
||||||
reaction_items: IIssueReaction[];
|
reaction_items: IIssueReaction[];
|
||||||
|
|
|
||||||
10
space/core/types/member.d.ts
vendored
Normal file
10
space/core/types/member.d.ts
vendored
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
4
space/core/types/modules.d.ts
vendored
Normal file
4
space/core/types/modules.d.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export type TPublicModule = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
1
space/ee/components/issue-layouts/root.tsx
Normal file
1
space/ee/components/issue-layouts/root.tsx
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "ce/components/issue-layouts/root";
|
||||||
1
space/ee/components/navbar/index.tsx
Normal file
1
space/ee/components/navbar/index.tsx
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "ce/components/navbar";
|
||||||
1
space/ee/hooks/store/index.ts
Normal file
1
space/ee/hooks/store/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "ce/hooks/store";
|
||||||
|
|
@ -56,3 +56,7 @@ export const isEmptyHtmlString = (htmlString: string) => {
|
||||||
// Trim the string and check if it's empty
|
// Trim the string and check if it's empty
|
||||||
return cleanText.trim() === "";
|
return cleanText.trim() === "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
|
||||||
|
|
||||||
|
export const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
|
@ -354,3 +354,90 @@ body {
|
||||||
.disable-autofill-style:-webkit-autofill:active {
|
.disable-autofill-style:-webkit-autofill:active {
|
||||||
-webkit-background-clip: text;
|
-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);
|
||||||
|
}
|
||||||
|
|
|
||||||
2
web/ce/components/views/publish/index.ts
Normal file
2
web/ce/components/views/publish/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./modal";
|
||||||
|
export * from "./use-view-publish";
|
||||||
12
web/ce/components/views/publish/modal.tsx
Normal file
12
web/ce/components/views/publish/modal.tsx
Normal file
|
|
@ -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) => <></>;
|
||||||
7
web/ce/components/views/publish/use-view-publish.tsx
Normal file
7
web/ce/components/views/publish/use-view-publish.tsx
Normal file
|
|
@ -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,
|
||||||
|
});
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { TPublishViewSettings } from "@plane/types";
|
||||||
import { EViewAccess } from "@/constants/views";
|
import { EViewAccess } from "@/constants/views";
|
||||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||||
import { ViewService as CoreViewService } from "@/services/view.service";
|
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) {
|
async unLockView(workspaceSlug: string, projectId: string, viewId: string) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async getPublishDetails(workspaceSlug: string, projectId: string, viewId: string): Promise<any> {
|
||||||
|
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<any> {
|
||||||
|
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<TPublishViewSettings>
|
||||||
|
): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async unPublishView(workspaceSlug: string, projectId: string, viewId: string): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { cn } from "@/helpers/common.helper";
|
||||||
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "@/hooks/store";
|
import { useUser } from "@/hooks/store";
|
||||||
|
import { PublishViewModal, useViewPublish } from "@/plane-web/components/views/publish";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
parentRef: React.RefObject<HTMLElement>;
|
parentRef: React.RefObject<HTMLElement>;
|
||||||
|
|
@ -38,6 +39,11 @@ export const ViewQuickActions: React.FC<Props> = observer((props) => {
|
||||||
const isOwner = view?.owned_by === data?.id;
|
const isOwner = view?.owned_by === data?.id;
|
||||||
const isAdmin = !!currentProjectRole && currentProjectRole == EUserProjectRoles.ADMIN;
|
const isAdmin = !!currentProjectRole && currentProjectRole == EUserProjectRoles.ADMIN;
|
||||||
|
|
||||||
|
const { isPublishModalOpen, setPublishModalOpen, publishContextMenu } = useViewPublish(
|
||||||
|
!!view.anchor,
|
||||||
|
isAdmin || isOwner
|
||||||
|
);
|
||||||
|
|
||||||
const viewLink = `${workspaceSlug}/projects/${projectId}/views/${view.id}`;
|
const viewLink = `${workspaceSlug}/projects/${projectId}/views/${view.id}`;
|
||||||
const handleCopyText = () =>
|
const handleCopyText = () =>
|
||||||
copyUrlToClipboard(viewLink).then(() => {
|
copyUrlToClipboard(viewLink).then(() => {
|
||||||
|
|
@ -78,6 +84,8 @@ export const ViewQuickActions: React.FC<Props> = observer((props) => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (publishContextMenu) MENU_ITEMS.splice(2, 0, publishContextMenu);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateUpdateProjectViewModal
|
<CreateUpdateProjectViewModal
|
||||||
|
|
@ -88,6 +96,7 @@ export const ViewQuickActions: React.FC<Props> = observer((props) => {
|
||||||
data={view}
|
data={view}
|
||||||
/>
|
/>
|
||||||
<DeleteProjectViewModal data={view} isOpen={deleteViewModal} onClose={() => setDeleteViewModal(false)} />
|
<DeleteProjectViewModal data={view} isOpen={deleteViewModal} onClose={() => setDeleteViewModal(false)} />
|
||||||
|
<PublishViewModal isOpen={isPublishModalOpen} onClose={() => setPublishModalOpen(false)} view={view} />
|
||||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||||
<CustomMenu ellipsis placement="bottom-end" closeOnSelect>
|
<CustomMenu ellipsis placement="bottom-end" closeOnSelect>
|
||||||
{MENU_ITEMS.map((item) => {
|
{MENU_ITEMS.map((item) => {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { set } from "lodash";
|
||||||
import { observable, action, makeObservable, runInAction, computed } from "mobx";
|
import { observable, action, makeObservable, runInAction, computed } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
// types
|
// types
|
||||||
import { IProjectView, TViewFilters } from "@plane/types";
|
import { IProjectView, TPublishViewDetails, TPublishViewSettings, TViewFilters } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EViewAccess } from "@/constants/views";
|
import { EViewAccess } from "@/constants/views";
|
||||||
// helpers
|
// helpers
|
||||||
|
|
@ -42,6 +42,25 @@ export interface IProjectViewStore {
|
||||||
// favorites actions
|
// favorites actions
|
||||||
addViewToFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise<any>;
|
addViewToFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise<any>;
|
||||||
removeViewFromFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise<any>;
|
removeViewFromFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise<any>;
|
||||||
|
// publish
|
||||||
|
publishView: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
viewId: string,
|
||||||
|
data: TPublishViewSettings
|
||||||
|
) => Promise<TPublishViewDetails | undefined>;
|
||||||
|
fetchPublishDetails: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
viewId: string
|
||||||
|
) => Promise<TPublishViewDetails | undefined>;
|
||||||
|
updatePublishedView: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
viewId: string,
|
||||||
|
data: Partial<TPublishViewSettings>
|
||||||
|
) => Promise<void>;
|
||||||
|
unPublishView: (workspaceSlug: string, projectId: string, viewId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProjectViewStore implements IProjectViewStore {
|
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<TPublishViewSettings>
|
||||||
|
) => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
web/ee/components/views/publish/index.ts
Normal file
1
web/ee/components/views/publish/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "ce/components/views/publish";
|
||||||
Loading…
Add table
Add a link
Reference in a new issue