137 lines
4.3 KiB
TypeScript
137 lines
4.3 KiB
TypeScript
/**
|
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
* See the LICENSE file for details.
|
|
*/
|
|
|
|
import type { MutableRefObject } from "react";
|
|
import { forwardRef, useCallback, useRef, useState } from "react";
|
|
import { observer } from "mobx-react";
|
|
//types
|
|
import type {
|
|
TGroupedIssues,
|
|
IIssueDisplayProperties,
|
|
TSubGroupedIssues,
|
|
TIssueGroupByOptions,
|
|
TPaginationData,
|
|
TLoader,
|
|
} from "@plane/types";
|
|
import { cn } from "@plane/utils";
|
|
// hooks
|
|
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
|
|
// local imports
|
|
import { KanbanIssueBlocksList } from "./blocks-list";
|
|
|
|
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, subGroupId?: string) => TLoader;
|
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
|
}
|
|
|
|
// Loader components
|
|
const KanbanIssueBlockLoader = forwardRef(function KanbanIssueBlockLoader(
|
|
props: Record<string, unknown>,
|
|
ref: React.ForwardedRef<HTMLSpanElement>
|
|
) {
|
|
return (
|
|
<span ref={ref} className="m-1.5 block h-28 animate-pulse rounded-sm bg-[var(--illustration-fill-quaternary)]" />
|
|
);
|
|
});
|
|
KanbanIssueBlockLoader.displayName = "KanbanIssueBlockLoader";
|
|
|
|
export const KanbanGroup = observer(function KanbanGroup(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 cursor-pointer p-3 text-13 font-medium text-accent-primary hover:text-accent-secondary hover:underline"
|
|
onClick={loadMoreIssuesInThisGroup}
|
|
role="button"
|
|
>
|
|
{" "}
|
|
Load More ↓
|
|
</div>
|
|
);
|
|
|
|
const shouldLoadMore = nextPageResults === undefined ? issueIds?.length < groupIssueCount : !!nextPageResults;
|
|
|
|
return (
|
|
<div
|
|
id={`${groupId}__${subGroupId}`}
|
|
className={cn("relative h-full min-h-[120px] transition-all", { "vertical-scrollbar scrollbar-md": !subGroupBy })}
|
|
ref={columnRef}
|
|
>
|
|
<KanbanIssueBlocksList
|
|
subGroupId={subGroupId}
|
|
groupId={groupId}
|
|
issueIds={issueIds || []}
|
|
displayProperties={displayProperties}
|
|
scrollableContainerRef={scrollableContainerRef}
|
|
/>
|
|
|
|
{shouldLoadMore &&
|
|
(isSubGroup ? (
|
|
<>{loadMore}</>
|
|
) : (
|
|
<div className="flex flex-col gap-2">
|
|
{Array.from({ length: 2 }).map((_, index) => (
|
|
<KanbanIssueBlockLoader key={index} />
|
|
))}
|
|
<KanbanIssueBlockLoader ref={setIntersectionElement} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
});
|