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

View File

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

View File

@ -5,7 +5,7 @@ import useSWR from "swr";
import { CalendarCheck } from "lucide-react"; import { CalendarCheck } from "lucide-react";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
// types // types
import { ICycle, TIssue } from "@plane/types"; import { ICycle } from "@plane/types";
// ui // ui
import { Tooltip, Loader, PriorityIcon, Avatar } from "@plane/ui"; import { Tooltip, Loader, PriorityIcon, Avatar } from "@plane/ui";
// components // components
@ -20,7 +20,7 @@ import { EIssuesStoreType } from "@/constants/issue";
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { renderFormattedDate, renderFormattedDateWithoutYear } from "@/helpers/date-time.helper"; import { renderFormattedDate, renderFormattedDateWithoutYear } from "@/helpers/date-time.helper";
// hooks // hooks
import { useIssues, useProject } from "@/hooks/store"; import { useIssueDetail, useIssues, useProject } from "@/hooks/store";
import useLocalStorage from "@/hooks/use-local-storage"; import useLocalStorage from "@/hooks/use-local-storage";
export type ActiveCycleStatsProps = { export type ActiveCycleStatsProps = {
@ -47,17 +47,20 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
} }
}; };
const { const {
issues: { fetchActiveCycleIssues }, issues: { getActiveCycleId, fetchActiveCycleIssues, fetchNextActiveCycleIssues },
} = useIssues(EIssuesStoreType.CYCLE); } = useIssues(EIssuesStoreType.CYCLE);
const {
issue: { getIssueById },
} = useIssueDetail();
const { currentProjectDetails } = useProject(); 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 ? 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 ( 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"> <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" 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"> <div className="flex flex-col gap-1 h-full w-full overflow-y-auto vertical-scrollbar scrollbar-sm">
{cycleIssues ? ( {cycleIssueDetails && cycleIssueDetails.issueIds ? (
cycleIssues.length > 0 ? ( cycleIssueDetails.issueCount > 0 ? (
cycleIssues.map((issue: TIssue) => ( <>
<Link {cycleIssueDetails.issueIds.map((issueId: string) => {
key={issue.id} const issue = getIssueById(issueId);
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} />
<Tooltip if (!issue) return null;
tooltipHeading="Issue ID"
tooltipContent={`${currentProjectDetails?.identifier}-${issue.sequence_id}`} 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"> <div className="flex items-center gap-1.5 flex-grow w-full min-w-24 truncate">
{currentProjectDetails?.identifier}-{issue.sequence_id} <PriorityIcon priority={issue.priority} withContainer size={12} />
</span>
</Tooltip> <Tooltip
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}> tooltipHeading="Issue ID"
<span className="text-[0.825rem] text-custom-text-100 truncate">{issue.name}</span> tooltipContent={`${currentProjectDetails?.identifier}-${issue.sequence_id}`}
</Tooltip> >
</div> <span className="flex-shrink-0 text-xs text-custom-text-200">
<div className="flex items-center gap-1.5 flex-shrink-0"> {currentProjectDetails?.identifier}-{issue.sequence_id}
<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> </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> </div>
</Link> )}
)) </>
) : ( ) : (
<div className="flex items-center justify-center h-full w-full"> <div className="flex items-center justify-center h-full w-full">
<EmptyState <EmptyState

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
// types // types
import { TGroupedIssues, TIssue, TIssueMap, TPaginationData } from "@plane/types"; import { TGroupedIssues, TIssue, TIssueMap, TPaginationData } from "@plane/types";
// components // components
import { CalendarIssueBlocks, ICalendarDate, CalendarQuickAddIssueForm } from "@/components/issues"; import { CalendarIssueBlocks, ICalendarDate } from "@/components/issues";
// helpers // helpers
import { MONTHS_LIST } from "@/constants/calendar"; import { MONTHS_LIST } from "@/constants/calendar";
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
@ -63,11 +63,6 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
const formattedDatePayload = renderFormattedPayloadDate(date.date); const formattedDatePayload = renderFormattedPayloadDate(date.date);
if (!formattedDatePayload) return null; if (!formattedDatePayload) return null;
const issueIds = groupedIssueIds?.[formattedDatePayload]; 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 isToday = date.date.toDateString() === new Date().toDateString();
const isSelectedDate = date.date.toDateString() == selectedDate.toDateString(); const isSelectedDate = date.date.toDateString() == selectedDate.toDateString();
@ -117,6 +112,9 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
issues={issues} issues={issues}
issueIdList={issueIds ?? []} issueIdList={issueIds ?? []}
quickActions={quickActions} quickActions={quickActions}
loadMoreIssues={loadMoreIssues}
getPaginationData={getPaginationData}
getGroupIssueCount={getGroupIssueCount}
isDragDisabled={readOnly} isDragDisabled={readOnly}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
disableIssueCreation={disableIssueCreation} disableIssueCreation={disableIssueCreation}
@ -126,33 +124,6 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
readOnly={readOnly} 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} {provided.placeholder}
</div> </div>
)} )}

View File

@ -1,8 +1,7 @@
import { useState } from "react";
import { Draggable } from "@hello-pangea/dnd"; import { Draggable } from "@hello-pangea/dnd";
import { Placement } from "@popperjs/core"; import { Placement } from "@popperjs/core";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { TIssue, TIssueMap } from "@plane/types"; import { TIssue, TIssueMap, TPaginationData } from "@plane/types";
// components // components
import { CalendarQuickAddIssueForm, CalendarIssueBlockRoot } from "@/components/issues"; import { CalendarQuickAddIssueForm, CalendarIssueBlockRoot } from "@/components/issues";
// helpers // helpers
@ -12,8 +11,11 @@ import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
type Props = { type Props = {
date: Date; date: Date;
issues: TIssueMap | undefined; issues: TIssueMap | undefined;
issueIdList: string[] | null; issueIdList: string[];
quickActions: (issue: TIssue, customActionButton?: React.ReactElement, placement?: Placement) => React.ReactNode; 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; isDragDisabled?: boolean;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
@ -35,6 +37,9 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
issues, issues,
issueIdList, issueIdList,
quickActions, quickActions,
loadMoreIssues,
getPaginationData,
getGroupIssueCount,
isDragDisabled = false, isDragDisabled = false,
enableQuickIssueCreate, enableQuickIssueCreate,
disableIssueCreation, disableIssueCreation,
@ -45,13 +50,18 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
isMobileView = false, isMobileView = false,
} = props; } = props;
// states // states
const [showAllIssues, setShowAllIssues] = useState(false);
const formattedDatePayload = renderFormattedPayloadDate(date); const formattedDatePayload = renderFormattedPayloadDate(date);
const totalIssues = issueIdList?.length ?? 0;
if (!formattedDatePayload) return null; if (!formattedDatePayload) return null;
const dayIssueCount = getGroupIssueCount(formattedDatePayload);
const nextPageResults = getPaginationData(formattedDatePayload)?.nextPageResults;
const shouldLoadMore =
nextPageResults === undefined && dayIssueCount !== undefined
? issueIdList?.length < dayIssueCount
: !!nextPageResults;
return ( return (
<> <>
{issueIdList?.map((issueId, index) => {issueIdList?.map((issueId, index) =>
@ -79,7 +89,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
)} )}
{enableQuickIssueCreate && !disableIssueCreation && !readOnly && ( {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 <CalendarQuickAddIssueForm
formKey="target_date" formKey="target_date"
groupId={formattedDatePayload} groupId={formattedDatePayload}
@ -89,18 +99,18 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
viewId={viewId} viewId={viewId}
onOpen={() => setShowAllIssues(true)}
/> />
</div> </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 <button
type="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" 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> </button>
</div> </div>
)} )}

View File

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

View File

@ -7,5 +7,5 @@ import { BaseKanBanRoot } from "../base-kanban-root";
export interface IKanBanLayout {} export interface IKanBanLayout {}
export const DraftKanBanLayout: React.FC = observer(() => ( 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 ( return (
<BaseKanBanRoot <BaseKanBanRoot
showLoader
QuickActions={ModuleIssueQuickActions} QuickActions={ModuleIssueQuickActions}
viewId={moduleId?.toString()} viewId={moduleId?.toString()}
storeType={EIssuesStoreType.MODULE} storeType={EIssuesStoreType.MODULE}

View File

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

View File

@ -8,5 +8,5 @@ import { EIssuesStoreType } from "@/constants/issue";
import { BaseKanBanRoot } from "../base-kanban-root"; import { BaseKanBanRoot } from "../base-kanban-root";
export const KanBanLayout: React.FC = observer(() => ( 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 ( return (
<BaseKanBanRoot <BaseKanBanRoot
showLoader
QuickActions={ProjectIssueQuickActions} QuickActions={ProjectIssueQuickActions}
storeType={EIssuesStoreType.PROJECT_VIEW} storeType={EIssuesStoreType.PROJECT_VIEW}
viewId={viewId?.toString()} viewId={viewId?.toString()}

View File

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

View File

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

View File

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

View File

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

View File

@ -5,13 +5,26 @@ import { CycleService } from "@/services/cycle.service";
// types // types
import { TIssue, TLoader, ViewFlags, IssuePaginationOptions, TIssuesResponse } from "@plane/types"; import { TIssue, TLoader, ViewFlags, IssuePaginationOptions, TIssuesResponse } from "@plane/types";
import { IIssueRootStore } from "../root.store"; 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 { 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 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 { export interface ICycleIssues extends IBaseIssuesStore {
viewFlags: ViewFlags; viewFlags: ViewFlags;
activeCycleIds: Record<string, ActiveCycleIssueDetails>;
//action helpers
getActiveCycleId: (cycleId: string) => ActiveCycleIssueDetails | undefined;
// actions // actions
fetchIssues: ( fetchIssues: (
workspaceSlug: string, workspaceSlug: string,
@ -34,6 +47,18 @@ export interface ICycleIssues extends IBaseIssuesStore {
subGroupId?: string subGroupId?: string
) => Promise<TIssuesResponse | undefined>; ) => 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>; createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>, cycleId: string) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
@ -56,15 +81,11 @@ export interface ICycleIssues extends IBaseIssuesStore {
new_cycle_id: string; new_cycle_id: string;
} }
) => Promise<TIssue>; ) => Promise<TIssue>;
fetchActiveCycleIssues: (
workspaceSlug: string,
projectId: string,
cycleId: string
) => Promise<TIssuesResponse | undefined>;
} }
export class CycleIssues extends BaseIssuesStore implements ICycleIssues { export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
cycleId: string | undefined = undefined; cycleId: string | undefined = undefined;
activeCycleIds: Record<string, ActiveCycleIssueDetails> = {};
viewFlags = { viewFlags = {
enableQuickAdd: true, enableQuickAdd: true,
enableIssueCreation: true, enableIssueCreation: true,
@ -80,6 +101,7 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
makeObservable(this, { makeObservable(this, {
// observable // observable
cycleId: observable.ref, cycleId: observable.ref,
activeCycleIds: observable,
// action // action
fetchIssues: action, fetchIssues: action,
fetchNextIssues: action, fetchNextIssues: action,
@ -98,6 +120,8 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
this.issueFilterStore = issueFilterStore; this.issueFilterStore = issueFilterStore;
} }
getActiveCycleId = computedFn((cycleId: string) => this.activeCycleIds[cycleId]);
fetchIssues = async ( fetchIssues = async (
workspaceSlug: string, workspaceSlug: string,
projectId: 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 { 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); const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params);
// runInAction(() => { const { issueList, groupedIssues } = this.processIssueResponse(response);
// set(this.issues, , Object.keys(response));
// this.loader = undefined;
// });
// 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; return response;
} catch (error) { } catch (error) {

View File

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

View File

@ -302,12 +302,12 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
groupId?: string, groupId?: string,
subGroupId?: 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>> = { const paginationParams: Partial<Record<TIssueParams, string | boolean>> = {
...filterParams, ...filterParams,
cursor: pageCursor, cursor: pageCursor,
per_page: (groupId ? options.perPageCount * 2 : options.perPageCount).toString(), per_page: options.perPageCount.toString(),
}; };
if (options.groupedBy) { 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 // fetch other issues states and members when sub-issues are from different project
if (subIssues && subIssues.length > 0) { 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); 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 // fetch other issues states and members when sub-issues are from different project
if (subIssues && subIssues.length > 0) { 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); this.fetchOtherProjectProperties(workspaceSlug, otherProjectIds);
} }