Merge branch 'feat/pagination' of github.com:makeplane/plane into feat/pagination

This commit is contained in:
pablohashescobar 2024-03-27 18:06:18 +05:30
commit 1d697c9d78
21 changed files with 306 additions and 230 deletions

View File

@ -1,13 +1,18 @@
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
// hooks
import { useProjectState } from "@/hooks/store";
import { ISearchIssueResponse } from "@plane/types";
export const BulkDeleteIssuesModalItem: React.FC<any> = observer((props) => {
const { issue, delete_issue_ids, identifier } = props;
const { getStateById } = useProjectState();
interface Props {
issue: ISearchIssueResponse;
canDeleteIssueIds: boolean;
identifier: string | undefined;
}
const color = getStateById(issue.state_id)?.color;
export const BulkDeleteIssuesModalItem: React.FC<Props> = observer((props: Props) => {
const { issue, canDeleteIssueIds, identifier } = props;
const color = issue.state__color;
return (
<Combobox.Option
@ -21,7 +26,7 @@ export const BulkDeleteIssuesModalItem: React.FC<any> = observer((props) => {
}
>
<div className="flex items-center gap-2">
<input type="checkbox" checked={delete_issue_ids} readOnly />
<input type="checkbox" checked={canDeleteIssueIds} readOnly />
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{

View File

@ -127,7 +127,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
<BulkDeleteIssuesModalItem
issue={issue}
identifier={projectDetails?.identifier}
delete_issue_ids={watch("delete_issue_ids").includes(issue.id)}
canDeleteIssueIds={watch("delete_issue_ids").includes(issue.id)}
key={issue.id}
/>
))}

View File

@ -5,7 +5,7 @@ import useSWR from "swr";
import { CalendarCheck } from "lucide-react";
import { Tab } from "@headlessui/react";
// types
import { ICycle, TIssue } from "@plane/types";
import { ICycle } from "@plane/types";
// ui
import { Tooltip, Loader, PriorityIcon, Avatar } from "@plane/ui";
// components
@ -20,7 +20,7 @@ import { EIssuesStoreType } from "@/constants/issue";
import { cn } from "@/helpers/common.helper";
import { renderFormattedDate, renderFormattedDateWithoutYear } from "@/helpers/date-time.helper";
// hooks
import { useIssues, useProject } from "@/hooks/store";
import { useIssueDetail, useIssues, useProject } from "@/hooks/store";
import useLocalStorage from "@/hooks/use-local-storage";
export type ActiveCycleStatsProps = {
@ -47,17 +47,20 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
}
};
const {
issues: { fetchActiveCycleIssues },
issues: { getActiveCycleId, fetchActiveCycleIssues, fetchNextActiveCycleIssues },
} = useIssues(EIssuesStoreType.CYCLE);
const {
issue: { getIssueById },
} = useIssueDetail();
const { currentProjectDetails } = useProject();
const { data: activeCycleIssues } = useSWR(
useSWR(
workspaceSlug && projectId && cycle.id ? CYCLE_ISSUES_WITH_PARAMS(cycle.id, { priority: "urgent,high" }) : null,
workspaceSlug && projectId && cycle.id ? () => fetchActiveCycleIssues(workspaceSlug, projectId, cycle.id) : null
workspaceSlug && projectId && cycle.id ? () => fetchActiveCycleIssues(workspaceSlug, projectId, 6, cycle.id) : null
);
const cycleIssues = activeCycleIssues ?? [];
const cycleIssueDetails = getActiveCycleId(cycle.id);
return (
<div className="flex flex-col gap-4 p-4 min-h-[17rem] overflow-hidden col-span-1 lg:col-span-2 xl:col-span-1 border border-custom-border-200 rounded-lg">
@ -132,52 +135,73 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm"
>
<div className="flex flex-col gap-1 h-full w-full overflow-y-auto vertical-scrollbar scrollbar-sm">
{cycleIssues ? (
cycleIssues.length > 0 ? (
cycleIssues.map((issue: TIssue) => (
<Link
key={issue.id}
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
className="group flex cursor-pointer items-center justify-between gap-2 rounded-md hover:bg-custom-background-90 p-1"
>
<div className="flex items-center gap-1.5 flex-grow w-full min-w-24 truncate">
<PriorityIcon priority={issue.priority} withContainer size={12} />
{cycleIssueDetails && cycleIssueDetails.issueIds ? (
cycleIssueDetails.issueCount > 0 ? (
<>
{cycleIssueDetails.issueIds.map((issueId: string) => {
const issue = getIssueById(issueId);
<Tooltip
tooltipHeading="Issue ID"
tooltipContent={`${currentProjectDetails?.identifier}-${issue.sequence_id}`}
if (!issue) return null;
return (
<Link
key={issue.id}
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
className="group flex cursor-pointer items-center justify-between gap-2 rounded-md hover:bg-custom-background-90 p-1"
>
<span className="flex-shrink-0 text-xs text-custom-text-200">
{currentProjectDetails?.identifier}-{issue.sequence_id}
</span>
</Tooltip>
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<span className="text-[0.825rem] text-custom-text-100 truncate">{issue.name}</span>
</Tooltip>
</div>
<div className="flex items-center gap-1.5 flex-shrink-0">
<StateDropdown
value={issue.state_id ?? undefined}
onChange={() => {}}
projectId={projectId?.toString() ?? ""}
disabled
buttonVariant="background-with-text"
buttonContainerClassName="cursor-pointer max-w-24"
showTooltip
/>
{issue.target_date && (
<Tooltip tooltipHeading="Target Date" tooltipContent={renderFormattedDate(issue.target_date)}>
<div className="h-full flex truncate items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80 group-hover:bg-custom-background-100 cursor-pointer">
<CalendarCheck className="h-3 w-3 flex-shrink-0" />
<span className="text-xs truncate">
{renderFormattedDateWithoutYear(issue.target_date)}
<div className="flex items-center gap-1.5 flex-grow w-full min-w-24 truncate">
<PriorityIcon priority={issue.priority} withContainer size={12} />
<Tooltip
tooltipHeading="Issue ID"
tooltipContent={`${currentProjectDetails?.identifier}-${issue.sequence_id}`}
>
<span className="flex-shrink-0 text-xs text-custom-text-200">
{currentProjectDetails?.identifier}-{issue.sequence_id}
</span>
</div>
</Tooltip>
)}
</Tooltip>
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<span className="text-[0.825rem] text-custom-text-100 truncate">{issue.name}</span>
</Tooltip>
</div>
<div className="flex items-center gap-1.5 flex-shrink-0">
<StateDropdown
value={issue.state_id ?? undefined}
onChange={() => {}}
projectId={projectId?.toString() ?? ""}
disabled
buttonVariant="background-with-text"
buttonContainerClassName="cursor-pointer max-w-24"
showTooltip
/>
{issue.target_date && (
<Tooltip
tooltipHeading="Target Date"
tooltipContent={renderFormattedDate(issue.target_date)}
>
<div className="h-full flex truncate items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80 group-hover:bg-custom-background-100 cursor-pointer">
<CalendarCheck className="h-3 w-3 flex-shrink-0" />
<span className="text-xs truncate">
{renderFormattedDateWithoutYear(issue.target_date)}
</span>
</div>
</Tooltip>
)}
</div>
</Link>
);
})}
{cycleIssueDetails.nextPageResults && (
<div
className={
"h-11 relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm text-custom-primary-100 hover:underline cursor-pointer"
}
onClick={() => fetchNextActiveCycleIssues(workspaceSlug, projectId, cycle.id)}
>
Load more &darr;
</div>
</Link>
))
)}
</>
) : (
<div className="flex items-center justify-center h-full w-full">
<EmptyState

View File

@ -1,20 +1,21 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { Search } from "lucide-react";
import { Combobox, Dialog, Transition } from "@headlessui/react";
// hooks
// icons
// components
// types
import { ISearchIssueResponse } from "@plane/types";
// ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui";
import { EmptyState } from "@/components/empty-state";
// services
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { PROJECT_ISSUES_LIST } from "@/constants/fetch-keys";
import { useProject, useProjectState } from "@/hooks/store";
import { IssueService } from "@/services/issue";
// hooks
import { useProject } from "@/hooks/store";
import useDebounce from "@/hooks/use-debounce";
// services
import { ProjectService } from "@/services/project";
type Props = {
isOpen: boolean;
@ -23,7 +24,7 @@ type Props = {
onSubmit: (issueId: string) => void;
};
const issueService = new IssueService();
const projectService = new ProjectService();
export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
const { isOpen, onClose, onSubmit, value } = props;
@ -35,18 +36,27 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
const { workspaceSlug, projectId, issueId } = router.query;
// hooks
const { getProjectStates } = useProjectState();
const { getProjectById } = useProject();
const { data: issues } = useSWR(
workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
workspaceSlug && projectId
? () =>
issueService
.getIssues(workspaceSlug as string, projectId as string)
.then((res) => Object.values(res ?? {}).filter((issue) => issue.id !== issueId))
: null
);
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
const [isSearching, setIsSearching] = useState(false);
const debouncedSearchTerm: string = useDebounce(query, 500);
useEffect(() => {
if (!isOpen || !workspaceSlug || !projectId) return;
setIsSearching(true);
projectService
.projectIssuesSearch(workspaceSlug.toString(), projectId.toString(), {
search: debouncedSearchTerm,
workspace_search: false,
})
.then((res: ISearchIssueResponse[]) => setIssues(res))
.finally(() => setIsSearching(false));
}, [debouncedSearchTerm, isOpen, projectId, workspaceSlug]);
const filteredIssues = issues.filter((issue) => issue.id !== issueId);
useEffect(() => {
if (!value) {
@ -69,7 +79,52 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
handleClose();
};
const filteredIssues = (query === "" ? issues : issues?.filter((issue) => issue.name.includes(query))) ?? [];
const issueList =
filteredIssues.length > 0 ? (
<li className="p-2">
{query === "" && <h2 className="mb-2 mt-4 px-3 text-xs font-semibold text-custom-text-100">Select issue</h2>}
<ul className="text-sm text-custom-text-100">
{filteredIssues.map((issue) => {
const stateColor = issue.state__color || "";
return (
<Combobox.Option
key={issue.id}
as="div"
value={issue.id}
className={({ active, selected }) =>
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
active || selected ? "bg-custom-background-80 text-custom-text-100" : ""
} `
}
>
<div className="flex items-center gap-2">
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: stateColor,
}}
/>
<span className="flex-shrink-0 text-xs text-custom-text-200">
{getProjectById(issue?.project_id)?.identifier}-{issue.sequence_id}
</span>
<span className="text-custom-text-200">{issue.name}</span>
</div>
</Combobox.Option>
);
})}
</ul>
</li>
) : (
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
<EmptyState
type={
query === "" ? EmptyStateType.ISSUE_RELATION_EMPTY_STATE : EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE
}
layout="screen-simple"
/>
</div>
);
return (
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
@ -122,56 +177,15 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
static
className="max-h-80 scroll-py-2 divide-y divide-custom-border-200 overflow-y-auto"
>
{filteredIssues.length > 0 ? (
<li className="p-2">
{query === "" && (
<h2 className="mb-2 mt-4 px-3 text-xs font-semibold text-custom-text-100">Select issue</h2>
)}
<ul className="text-sm text-custom-text-100">
{filteredIssues.map((issue) => {
const stateColor =
getProjectStates(issue?.project_id ?? "")?.find((state) => state?.id == issue?.state_id)
?.color || "";
return (
<Combobox.Option
key={issue.id}
as="div"
value={issue.id}
className={({ active, selected }) =>
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
active || selected ? "bg-custom-background-80 text-custom-text-100" : ""
} `
}
>
<div className="flex items-center gap-2">
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: stateColor,
}}
/>
<span className="flex-shrink-0 text-xs text-custom-text-200">
{getProjectById(issue?.project_id)?.identifier}-{issue.sequence_id}
</span>
<span className="text-custom-text-200">{issue.name}</span>
</div>
</Combobox.Option>
);
})}
</ul>
</li>
{isSearching ? (
<Loader className="space-y-3 p-3">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
) : (
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
<EmptyState
type={
query === ""
? EmptyStateType.ISSUE_RELATION_EMPTY_STATE
: EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE
}
layout="screen-simple"
/>
</div>
<>{issueList}</>
)}
</Combobox.Options>
</Combobox>

View File

@ -106,7 +106,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
</div>
);
const issueIdList = groupedIssueIds ? groupedIssueIds[formattedDatePayload] : null;
const issueIdList = groupedIssueIds ? groupedIssueIds[formattedDatePayload] : [];
return (
<>
@ -182,6 +182,9 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
date={selectedDate}
issues={issues}
issueIdList={issueIdList}
loadMoreIssues={loadMoreIssues}
getPaginationData={getPaginationData}
getGroupIssueCount={getGroupIssueCount}
quickActions={quickActions}
enableQuickIssueCreate
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}

View File

@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
// types
import { TGroupedIssues, TIssue, TIssueMap, TPaginationData } from "@plane/types";
// components
import { CalendarIssueBlocks, ICalendarDate, CalendarQuickAddIssueForm } from "@/components/issues";
import { CalendarIssueBlocks, ICalendarDate } from "@/components/issues";
// helpers
import { MONTHS_LIST } from "@/constants/calendar";
import { cn } from "@/helpers/common.helper";
@ -63,11 +63,6 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
const formattedDatePayload = renderFormattedPayloadDate(date.date);
if (!formattedDatePayload) return null;
const issueIds = groupedIssueIds?.[formattedDatePayload];
const dayIssueCount = getGroupIssueCount(formattedDatePayload);
const nextPageResults = getPaginationData(formattedDatePayload)?.nextPageResults;
const shouldLoadMore =
nextPageResults === undefined && dayIssueCount !== undefined ? issueIds?.length < dayIssueCount : !!nextPageResults;
const isToday = date.date.toDateString() === new Date().toDateString();
const isSelectedDate = date.date.toDateString() == selectedDate.toDateString();
@ -117,6 +112,9 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
issues={issues}
issueIdList={issueIds ?? []}
quickActions={quickActions}
loadMoreIssues={loadMoreIssues}
getPaginationData={getPaginationData}
getGroupIssueCount={getGroupIssueCount}
isDragDisabled={readOnly}
addIssuesToView={addIssuesToView}
disableIssueCreation={disableIssueCreation}
@ -126,33 +124,6 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
readOnly={readOnly}
/>
{enableQuickIssueCreate && !disableIssueCreation && !readOnly && (
<div className="px-2 py-1">
<CalendarQuickAddIssueForm
formKey="target_date"
groupId={formattedDatePayload}
prePopulatedData={{
target_date: renderFormattedPayloadDate(date.date) ?? undefined,
}}
quickAddCallback={quickAddCallback}
addIssuesToView={addIssuesToView}
viewId={viewId}
/>
</div>
)}
{shouldLoadMore && (
<div className="flex items-center px-2.5 py-1">
<button
type="button"
className="w-min whitespace-nowrap rounded text-xs px-1.5 py-1 text-custom-text-400 font-medium hover:bg-custom-background-80 hover:text-custom-text-300"
onClick={() => loadMoreIssues(formattedDatePayload)}
>
Load More
</button>
</div>
)}
{provided.placeholder}
</div>
)}

View File

@ -1,8 +1,7 @@
import { useState } from "react";
import { Draggable } from "@hello-pangea/dnd";
import { Placement } from "@popperjs/core";
import { observer } from "mobx-react-lite";
import { TIssue, TIssueMap } from "@plane/types";
import { TIssue, TIssueMap, TPaginationData } from "@plane/types";
// components
import { CalendarQuickAddIssueForm, CalendarIssueBlockRoot } from "@/components/issues";
// helpers
@ -12,8 +11,11 @@ import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
type Props = {
date: Date;
issues: TIssueMap | undefined;
issueIdList: string[] | null;
issueIdList: string[];
quickActions: (issue: TIssue, customActionButton?: React.ReactElement, placement?: Placement) => React.ReactNode;
loadMoreIssues: (dateString: string) => void;
getPaginationData: (groupId: string | undefined) => TPaginationData | undefined;
getGroupIssueCount: (groupId: string | undefined) => number | undefined;
isDragDisabled?: boolean;
enableQuickIssueCreate?: boolean;
disableIssueCreation?: boolean;
@ -35,6 +37,9 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
issues,
issueIdList,
quickActions,
loadMoreIssues,
getPaginationData,
getGroupIssueCount,
isDragDisabled = false,
enableQuickIssueCreate,
disableIssueCreation,
@ -45,13 +50,18 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
isMobileView = false,
} = props;
// states
const [showAllIssues, setShowAllIssues] = useState(false);
const formattedDatePayload = renderFormattedPayloadDate(date);
const totalIssues = issueIdList?.length ?? 0;
if (!formattedDatePayload) return null;
const dayIssueCount = getGroupIssueCount(formattedDatePayload);
const nextPageResults = getPaginationData(formattedDatePayload)?.nextPageResults;
const shouldLoadMore =
nextPageResults === undefined && dayIssueCount !== undefined
? issueIdList?.length < dayIssueCount
: !!nextPageResults;
return (
<>
{issueIdList?.map((issueId, index) =>
@ -79,7 +89,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
)}
{enableQuickIssueCreate && !disableIssueCreation && !readOnly && (
<div className="px-1 md:px-2 py-1 border-custom-border-200 border-b md:border-none">
<div className="px-1 md:px-2 py-1 border-custom-border-200 border-b md:border-none md:hidden group-hover:block">
<CalendarQuickAddIssueForm
formKey="target_date"
groupId={formattedDatePayload}
@ -89,18 +99,18 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
quickAddCallback={quickAddCallback}
addIssuesToView={addIssuesToView}
viewId={viewId}
onOpen={() => setShowAllIssues(true)}
/>
</div>
)}
{totalIssues > 4 && (
<div className="hidden md:flex items-center px-2.5 py-1">
{shouldLoadMore && (
<div className="flex items-center px-2.5 py-1">
<button
type="button"
className="w-min whitespace-nowrap rounded text-xs px-1.5 py-1 text-custom-text-400 font-medium hover:bg-custom-background-80 hover:text-custom-text-300"
onClick={() => setShowAllIssues(!showAllIssues)}
onClick={() => loadMoreIssues(formattedDatePayload)}
>
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
Load More
</button>
</div>
)}

View File

@ -35,7 +35,6 @@ export const CycleKanBanLayout: React.FC = observer(() => {
return (
<BaseKanBanRoot
showLoader
QuickActions={CycleIssueQuickActions}
viewId={cycleId?.toString() ?? ""}
storeType={EIssuesStoreType.CYCLE}

View File

@ -7,5 +7,5 @@ import { BaseKanBanRoot } from "../base-kanban-root";
export interface IKanBanLayout {}
export const DraftKanBanLayout: React.FC = observer(() => (
<BaseKanBanRoot showLoader QuickActions={DraftIssueQuickActions} storeType={EIssuesStoreType.DRAFT} />
<BaseKanBanRoot QuickActions={DraftIssueQuickActions} storeType={EIssuesStoreType.DRAFT} />
));

View File

@ -21,7 +21,6 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
return (
<BaseKanBanRoot
showLoader
QuickActions={ModuleIssueQuickActions}
viewId={moduleId?.toString()}
storeType={EIssuesStoreType.MODULE}

View File

@ -22,7 +22,6 @@ export const ProfileIssuesKanBanLayout: React.FC = observer(() => {
return (
<BaseKanBanRoot
showLoader
QuickActions={ProjectIssueQuickActions}
storeType={EIssuesStoreType.PROFILE}
canEditPropertiesBasedOnProject={canEditPropertiesBasedOnProject}

View File

@ -8,5 +8,5 @@ import { EIssuesStoreType } from "@/constants/issue";
import { BaseKanBanRoot } from "../base-kanban-root";
export const KanBanLayout: React.FC = observer(() => (
<BaseKanBanRoot showLoader QuickActions={ProjectIssueQuickActions} storeType={EIssuesStoreType.PROJECT} />
<BaseKanBanRoot QuickActions={ProjectIssueQuickActions} storeType={EIssuesStoreType.PROJECT} />
));

View File

@ -16,7 +16,6 @@ export const ProjectViewKanBanLayout: React.FC = observer(() => {
return (
<BaseKanBanRoot
showLoader
QuickActions={ProjectIssueQuickActions}
storeType={EIssuesStoreType.PROJECT_VIEW}
viewId={viewId?.toString()}

View File

@ -21,7 +21,7 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = observer((props: Props
onChange={(data) =>
onChange(issue, { estimate_point: data }, { changed_property: "estimate_point", change_details: data })
}
projectId={issue.project_id}
projectId={issue.project_id ?? undefined}
disabled={disabled}
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"

View File

@ -464,7 +464,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
debouncedUpdatesEnabled={false}
value={
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html")
? watch("description_html") ?? ""
: value
}
initialValue={data?.description_html}

View File

@ -3,13 +3,10 @@ import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import useSWR from "swr";
// components
import { EmptyState } from "@/components/empty-state";
import { IssuePeekOverview, ProfileIssuesAppliedFiltersRoot } from "@/components/issues";
import { ProfileIssuesKanBanLayout } from "@/components/issues/issue-layouts/kanban/roots/profile-issues-root";
import { ProfileIssuesListLayout } from "@/components/issues/issue-layouts/list/roots/profile-issues-root";
import { KanbanLayoutLoader, ListLayoutLoader } from "@/components/ui";
// hooks
import { EMPTY_STATE_DETAILS } from "@/constants/empty-state";
import { EIssuesStoreType } from "@/constants/issue";
import { useIssues } from "@/hooks/store";
// constants
@ -28,7 +25,7 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
};
// store hooks
const {
issues: { loader, groupedIssueIds, fetchIssues, setViewId },
issues: { setViewId },
issuesFilter: { issueFilters, fetchFilters },
} = useIssues(EIssuesStoreType.PROFILE);
@ -41,7 +38,6 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
async () => {
if (workspaceSlug && userId) {
await fetchFilters(workspaceSlug, userId);
await fetchIssues(workspaceSlug, undefined, groupedIssueIds ? "mutation" : "init-loader", userId, type);
}
},
{ revalidateIfStale: false, revalidateOnFocus: false }
@ -49,15 +45,6 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
const emptyStateType = `profile-${type}`;
if (!groupedIssueIds || loader === "init-loader")
return <>{activeLayout === "list" ? <ListLayoutLoader /> : <KanbanLayoutLoader />}</>;
if (groupedIssueIds.length === 0) {
return <EmptyState type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS} size="sm" />;
}
return (
<>
<ProfileIssuesAppliedFiltersRoot />

View File

@ -81,7 +81,7 @@ export const ISSUE_ORDER_BY_OPTIONS: {
{ key: "-updated_at", title: "Last Updated" },
{ key: "start_date", title: "Start Date" },
{ key: "target_date", title: "Due Date" },
{ key: "-priority", title: "Priority" },
{ key: "priority", title: "Priority" },
];
export const ISSUE_FILTER_OPTIONS: {
@ -157,7 +157,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
display_properties: true,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
type: [null, "active", "backlog"],
},
extra_options: {
@ -170,7 +170,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
display_properties: true,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
type: [null, "active", "backlog"],
},
extra_options: {
@ -205,7 +205,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
"created_by",
null,
],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
type: [null, "active", "backlog"],
},
extra_options: {
@ -220,7 +220,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
display_properties: true,
display_filters: {
group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
type: [null, "active", "backlog"],
},
extra_options: {
@ -233,7 +233,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
display_properties: true,
display_filters: {
group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
type: [null, "active", "backlog"],
},
extra_options: {
@ -303,7 +303,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
display_properties: true,
display_filters: {
group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
type: [null, "active", "backlog"],
},
extra_options: {
@ -328,7 +328,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
display_filters: {
group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by"],
sub_group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority", "target_date"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority", "target_date"],
type: [null, "active", "backlog"],
},
extra_options: {
@ -362,7 +362,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
],
display_properties: true,
display_filters: {
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
type: [null, "active", "backlog"],
},
extra_options: {
@ -385,7 +385,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
],
display_properties: false,
display_filters: {
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
type: [null, "active", "backlog"],
},
extra_options: {

View File

@ -5,13 +5,26 @@ import { CycleService } from "@/services/cycle.service";
// types
import { TIssue, TLoader, ViewFlags, IssuePaginationOptions, TIssuesResponse } from "@plane/types";
import { IIssueRootStore } from "../root.store";
import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store";
import { ALL_ISSUES, BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store";
import { ICycleIssuesFilter } from "./filter.store";
import { concat, get, set, uniq, update } from "lodash";
import { computedFn } from "mobx-utils";
export const ACTIVE_CYCLE_ISSUES = "ACTIVE_CYCLE_ISSUES";
export interface ActiveCycleIssueDetails {
issueIds: string[];
issueCount: number;
nextCursor: string;
nextPageResults: boolean;
perPageCount: number;
}
export interface ICycleIssues extends IBaseIssuesStore {
viewFlags: ViewFlags;
activeCycleIds: Record<string, ActiveCycleIssueDetails>;
//action helpers
getActiveCycleId: (cycleId: string) => ActiveCycleIssueDetails | undefined;
// actions
fetchIssues: (
workspaceSlug: string,
@ -34,6 +47,18 @@ export interface ICycleIssues extends IBaseIssuesStore {
subGroupId?: string
) => Promise<TIssuesResponse | undefined>;
fetchActiveCycleIssues: (
workspaceSlug: string,
projectId: string,
perPageCount: number,
cycleId: string
) => Promise<TIssuesResponse | undefined>;
fetchNextActiveCycleIssues: (
workspaceSlug: string,
projectId: string,
cycleId: string
) => Promise<TIssuesResponse | undefined>;
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>, cycleId: string) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
@ -56,15 +81,11 @@ export interface ICycleIssues extends IBaseIssuesStore {
new_cycle_id: string;
}
) => Promise<TIssue>;
fetchActiveCycleIssues: (
workspaceSlug: string,
projectId: string,
cycleId: string
) => Promise<TIssuesResponse | undefined>;
}
export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
cycleId: string | undefined = undefined;
activeCycleIds: Record<string, ActiveCycleIssueDetails> = {};
viewFlags = {
enableQuickAdd: true,
enableIssueCreation: true,
@ -80,6 +101,7 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
makeObservable(this, {
// observable
cycleId: observable.ref,
activeCycleIds: observable,
// action
fetchIssues: action,
fetchNextIssues: action,
@ -98,6 +120,8 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
this.issueFilterStore = issueFilterStore;
}
getActiveCycleId = computedFn((cycleId: string) => this.activeCycleIds[cycleId]);
fetchIssues = async (
workspaceSlug: string,
projectId: string,
@ -240,17 +264,55 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
}
};
fetchActiveCycleIssues = async (workspaceSlug: string, projectId: string, cycleId: string) => {
fetchActiveCycleIssues = async (workspaceSlug: string, projectId: string, perPageCount: number, cycleId: string) => {
try {
const params = { priority: `urgent,high` };
set(this.activeCycleIds, [cycleId], undefined);
const params = { priority: `urgent,high`, cursor: `${perPageCount}:0:0`, per_page: perPageCount };
const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params);
// runInAction(() => {
// set(this.issues, , Object.keys(response));
// this.loader = undefined;
// });
const { issueList, groupedIssues } = this.processIssueResponse(response);
// this.rootIssueStore.issues.addIssue(Object.values(response));
this.rootIssueStore.issues.addIssue(issueList);
const activeIssueIds = groupedIssues[ALL_ISSUES] as string[];
set(this.activeCycleIds, [cycleId], {
issueIds: activeIssueIds,
issueCount: response.total_count,
nextCursor: response.next_cursor,
nextPageResults: response.next_page_results,
perPageCount: perPageCount,
});
return response;
} catch (error) {
this.loader = undefined;
throw error;
}
};
fetchNextActiveCycleIssues = async (workspaceSlug: string, projectId: string, cycleId: string) => {
try {
const activeCycle = get(this.activeCycleIds, [cycleId]);
if (!activeCycle || !activeCycle.nextPageResults) return;
const params = { priority: `urgent,high`, cursor: activeCycle.nextCursor, per_page: activeCycle.perPageCount };
const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params);
const { issueList, groupedIssues } = this.processIssueResponse(response);
this.rootIssueStore.issues.addIssue(issueList);
const activeIssueIds = groupedIssues[ALL_ISSUES] as string[];
set(this.activeCycleIds, [cycleId, "issueCount"], response.total_count);
set(this.activeCycleIds, [cycleId, "nextCursor"], response.next_cursor);
set(this.activeCycleIds, [cycleId, "nextPageResults"], response.next_page_results);
set(this.activeCycleIds, [cycleId, "issueCount"], response.total_count);
update(this.activeCycleIds, [cycleId, "issueIds"], (issueIds: string[] = []) => {
return this.issuesSortWithOrderBy(uniq(concat(issueIds, activeIssueIds)), this.orderBy);
});
return response;
} catch (error) {

View File

@ -800,11 +800,11 @@ export class BaseIssuesStore implements IBaseIssuesStore {
return this.getIssueIds(orderBy(array, "sort_order"));
case "state__name":
return this.getIssueIds(
orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"]))
orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue?.["state_id"]))
);
case "-state__name":
return this.getIssueIds(
orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"]), ["desc"])
orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue?.["state_id"]), ["desc"])
);
// dates
case "created_at":
@ -844,12 +844,12 @@ export class BaseIssuesStore implements IBaseIssuesStore {
// custom
case "priority": {
const sortArray = ISSUE_PRIORITIES.map((i) => i.key);
return this.getIssueIds(orderBy(array, (currentIssue: TIssue) => indexOf(sortArray, currentIssue.priority)));
return this.getIssueIds(orderBy(array, (currentIssue: TIssue) => indexOf(sortArray, currentIssue?.priority)));
}
case "-priority": {
const sortArray = ISSUE_PRIORITIES.map((i) => i.key);
return this.getIssueIds(
orderBy(array, (currentIssue: TIssue) => indexOf(sortArray, currentIssue.priority), ["desc"])
orderBy(array, (currentIssue: TIssue) => indexOf(sortArray, currentIssue?.priority), ["desc"])
);
}
@ -887,7 +887,7 @@ export class BaseIssuesStore implements IBaseIssuesStore {
return this.getIssueIds(
orderBy(array, [
this.getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("label_ids", issue["label_ids"], "asc"),
(issue) => this.populateIssueDataForSorting("label_ids", issue?.["label_ids"], "asc"),
])
);
case "-labels__name":
@ -896,7 +896,7 @@ export class BaseIssuesStore implements IBaseIssuesStore {
array,
[
this.getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("label_ids", issue["label_ids"], "desc"),
(issue) => this.populateIssueDataForSorting("label_ids", issue?.["label_ids"], "desc"),
],
["asc", "desc"]
)
@ -906,7 +906,7 @@ export class BaseIssuesStore implements IBaseIssuesStore {
return this.getIssueIds(
orderBy(array, [
this.getSortOrderToFilterEmptyValues.bind(null, "module_ids"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("module_ids", issue["module_ids"], "asc"),
(issue) => this.populateIssueDataForSorting("module_ids", issue?.["module_ids"], "asc"),
])
);
case "-issue_module__module__name":
@ -915,7 +915,7 @@ export class BaseIssuesStore implements IBaseIssuesStore {
array,
[
this.getSortOrderToFilterEmptyValues.bind(null, "module_ids"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("module_ids", issue["module_ids"], "desc"),
(issue) => this.populateIssueDataForSorting("module_ids", issue?.["module_ids"], "desc"),
],
["asc", "desc"]
)
@ -925,7 +925,7 @@ export class BaseIssuesStore implements IBaseIssuesStore {
return this.getIssueIds(
orderBy(array, [
this.getSortOrderToFilterEmptyValues.bind(null, "cycle_id"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("cycle_id", issue["cycle_id"], "asc"),
(issue) => this.populateIssueDataForSorting("cycle_id", issue?.["cycle_id"], "asc"),
])
);
case "-issue_cycle__cycle__name":
@ -934,7 +934,7 @@ export class BaseIssuesStore implements IBaseIssuesStore {
array,
[
this.getSortOrderToFilterEmptyValues.bind(null, "cycle_id"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("cycle_id", issue["cycle_id"], "desc"),
(issue) => this.populateIssueDataForSorting("cycle_id", issue?.["cycle_id"], "desc"),
],
["asc", "desc"]
)
@ -944,7 +944,7 @@ export class BaseIssuesStore implements IBaseIssuesStore {
return this.getIssueIds(
orderBy(array, [
this.getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("assignee_ids", issue["assignee_ids"], "asc"),
(issue) => this.populateIssueDataForSorting("assignee_ids", issue?.["assignee_ids"], "asc"),
])
);
case "-assignees__first_name":
@ -953,7 +953,7 @@ export class BaseIssuesStore implements IBaseIssuesStore {
array,
[
this.getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("assignee_ids", issue["assignee_ids"], "desc"),
(issue) => this.populateIssueDataForSorting("assignee_ids", issue?.["assignee_ids"], "desc"),
],
["asc", "desc"]
)

View File

@ -302,12 +302,12 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
groupId?: string,
subGroupId?: string
) {
const pageCursor = cursor ? cursor : groupId ? `${options.perPageCount * 2}:0:0` : `${options.perPageCount}:0:0`;
const pageCursor = cursor ? cursor : groupId ? `${options.perPageCount}:1:0` : `${options.perPageCount}:0:0`;
const paginationParams: Partial<Record<TIssueParams, string | boolean>> = {
...filterParams,
cursor: pageCursor,
per_page: (groupId ? options.perPageCount * 2 : options.perPageCount).toString(),
per_page: options.perPageCount.toString(),
};
if (options.groupedBy) {

View File

@ -122,7 +122,9 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
// fetch other issues states and members when sub-issues are from different project
if (subIssues && subIssues.length > 0) {
const otherProjectIds = uniq(subIssues.map((issue) => issue.project_id).filter((id) => id !== projectId));
const otherProjectIds = uniq(
subIssues.map((issue) => issue.project_id).filter((id) => !!id && id !== projectId)
) as string[];
this.fetchOtherProjectProperties(workspaceSlug, otherProjectIds);
}
@ -152,7 +154,9 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
// fetch other issues states and members when sub-issues are from different project
if (subIssues && subIssues.length > 0) {
const otherProjectIds = uniq(subIssues.map((issue) => issue.project_id).filter((id) => id !== projectId));
const otherProjectIds = uniq(
subIssues.map((issue) => issue.project_id).filter((id) => !!id && id !== projectId)
) as string[];
this.fetchOtherProjectProperties(workspaceSlug, otherProjectIds);
}