[WEB-914]: fix: Exception due to cycles and modules for undefined fields (#4127)

* fix cycle types

* fix module types
This commit is contained in:
rahulramesha 2024-04-05 20:05:55 +05:30 committed by GitHub
parent 62dac421dc
commit 90609b306f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 68 additions and 63 deletions

View File

@ -6,8 +6,8 @@ export interface ICycle {
backlog_issues: number; backlog_issues: number;
cancelled_issues: number; cancelled_issues: number;
completed_issues: number; completed_issues: number;
created_at: Date; created_at?: string;
created_by: string; created_by?: string;
description: string; description: string;
distribution?: { distribution?: {
assignees: TAssigneesDistribution[]; assignees: TAssigneesDistribution[];
@ -16,23 +16,22 @@ export interface ICycle {
}; };
end_date: string | null; end_date: string | null;
id: string; id: string;
is_favorite: boolean; is_favorite?: boolean;
issue: string;
name: string; name: string;
owned_by_id: string; owned_by_id: string;
progress_snapshot: TProgressSnapshot; progress_snapshot: TProgressSnapshot;
project_id: string; project_id: string;
status: TCycleGroups; status?: TCycleGroups;
sort_order: number; sort_order: number;
start_date: string | null; start_date: string | null;
started_issues: number; started_issues: number;
sub_issues: number; sub_issues?: number;
total_issues: number; total_issues: number;
unstarted_issues: number; unstarted_issues: number;
updated_at: Date; updated_at?: string;
updated_by: string; updated_by?: string;
archived_at: string | null; archived_at: string | null;
assignee_ids: string[]; assignee_ids?: string[];
view_props: { view_props: {
filters: IIssueFilterOptions; filters: IIssueFilterOptions;
}; };

View File

@ -12,33 +12,33 @@ export interface IModule {
backlog_issues: number; backlog_issues: number;
cancelled_issues: number; cancelled_issues: number;
completed_issues: number; completed_issues: number;
created_at: Date; created_at: string;
created_by: string; created_by?: string;
description: string; description: string;
description_text: any; description_text: any;
description_html: any; description_html: any;
distribution: { distribution?: {
assignees: TAssigneesDistribution[]; assignees: TAssigneesDistribution[];
completion_chart: TCompletionChartDistribution; completion_chart: TCompletionChartDistribution;
labels: TLabelsDistribution[]; labels: TLabelsDistribution[];
}; };
id: string; id: string;
lead_id: string | null; lead_id: string | null;
link_module: ILinkDetails[]; link_module?: ILinkDetails[];
member_ids: string[]; member_ids: string[];
is_favorite: boolean; is_favorite: boolean;
name: string; name: string;
project_id: string; project_id: string;
sort_order: number; sort_order: number;
sub_issues: number; sub_issues?: number;
start_date: string | null; start_date: string | null;
started_issues: number; started_issues: number;
status: TModuleStatus; status?: TModuleStatus;
target_date: string | null; target_date: string | null;
total_issues: number; total_issues: number;
unstarted_issues: number; unstarted_issues: number;
updated_at: Date; updated_at: string;
updated_by: string; updated_by?: string;
archived_at: string | null; archived_at: string | null;
view_props: { view_props: {
filters: IIssueFilterOptions; filters: IIssueFilterOptions;

View File

@ -22,11 +22,13 @@ import emptyMembers from "public/empty-state/empty_members.svg";
// types // types
type Props = { type Props = {
distribution: { distribution:
| {
assignees: TAssigneesDistribution[]; assignees: TAssigneesDistribution[];
completion_chart: TCompletionChartDistribution; completion_chart: TCompletionChartDistribution;
labels: TLabelsDistribution[]; labels: TLabelsDistribution[];
}; }
| undefined;
groupedIssues: { groupedIssues: {
[key: string]: number; [key: string]: number;
}; };
@ -129,7 +131,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
as="div" as="div"
className="flex w-full flex-col gap-1.5 overflow-y-auto pt-3.5 vertical-scrollbar scrollbar-sm" className="flex w-full flex-col gap-1.5 overflow-y-auto pt-3.5 vertical-scrollbar scrollbar-sm"
> >
{distribution?.assignees.length > 0 ? ( {distribution && distribution?.assignees.length > 0 ? (
distribution.assignees.map((assignee, index) => { distribution.assignees.map((assignee, index) => {
if (assignee.assignee_id) if (assignee.assignee_id)
return ( return (
@ -189,7 +191,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
as="div" as="div"
className="flex w-full flex-col gap-1.5 overflow-y-auto pt-3.5 vertical-scrollbar scrollbar-sm" className="flex w-full flex-col gap-1.5 overflow-y-auto pt-3.5 vertical-scrollbar scrollbar-sm"
> >
{distribution?.labels.length > 0 ? ( {distribution && distribution?.labels.length > 0 ? (
distribution.labels.map((label, index) => ( distribution.labels.map((label, index) => (
<SingleProgressStats <SingleProgressStats
key={label.label_id ?? `no-label-${index}`} key={label.label_id ?? `no-label-${index}`}

View File

@ -23,14 +23,14 @@ export const ActiveCycleHeader: FC<ActiveCycleHeaderProps> = (props) => {
const cycleOwnerDetails = cycle && cycle.owned_by_id ? getUserDetails(cycle.owned_by_id) : undefined; const cycleOwnerDetails = cycle && cycle.owned_by_id ? getUserDetails(cycle.owned_by_id) : undefined;
const daysLeft = findHowManyDaysLeft(cycle.end_date) ?? 0; const daysLeft = findHowManyDaysLeft(cycle.end_date) ?? 0;
const currentCycleStatus = cycle.status.toLocaleLowerCase() as TCycleGroups; const currentCycleStatus = cycle.status?.toLocaleLowerCase() as TCycleGroups | undefined;
const cycleAssignee = (cycle.distribution?.assignees ?? []).filter((assignee) => assignee.display_name); const cycleAssignee = (cycle.distribution?.assignees ?? []).filter((assignee) => assignee.display_name);
return ( return (
<div className="flex items-center justify-between px-3 py-1.5 rounded border-[0.5px] border-custom-border-100 bg-custom-background-90"> <div className="flex items-center justify-between px-3 py-1.5 rounded border-[0.5px] border-custom-border-100 bg-custom-background-90">
<div className="flex items-center gap-2 cursor-default"> <div className="flex items-center gap-2 cursor-default">
<CycleGroupIcon cycleGroup={currentCycleStatus} className="h-4 w-4" /> <CycleGroupIcon cycleGroup={currentCycleStatus ?? "draft"} className="h-4 w-4" />
<Tooltip tooltipContent={cycle.name} position="top-left"> <Tooltip tooltipContent={cycle.name} position="top-left">
<h3 className="break-words text-lg font-medium">{truncateText(cycle.name, 70)}</h3> <h3 className="break-words text-lg font-medium">{truncateText(cycle.name, 70)}</h3>
</Tooltip> </Tooltip>

View File

@ -99,7 +99,7 @@ export const UpcomingCycleListItem: React.FC<Props> = observer((props) => {
{renderFormattedDate(cycle.start_date)} - {renderFormattedDate(cycle.end_date)} {renderFormattedDate(cycle.start_date)} - {renderFormattedDate(cycle.end_date)}
</div> </div>
)} )}
{cycle.assignee_ids?.length > 0 ? ( {cycle.assignee_ids && cycle.assignee_ids?.length > 0 ? (
<AvatarGroup showTooltip={false}> <AvatarGroup showTooltip={false}>
{cycle.assignee_ids?.map((assigneeId) => { {cycle.assignee_ids?.map((assigneeId) => {
const member = getUserDetails(assigneeId); const member = getUserDetails(assigneeId);

View File

@ -46,7 +46,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
if (!cycleDetails) return null; if (!cycleDetails) return null;
const cycleStatus = cycleDetails.status.toLocaleLowerCase(); const cycleStatus = cycleDetails.status?.toLocaleLowerCase();
// const isCompleted = cycleStatus === "completed"; // const isCompleted = cycleStatus === "completed";
const endDate = getDate(cycleDetails.end_date); const endDate = getDate(cycleDetails.end_date);
const startDate = getDate(cycleDetails.start_date); const startDate = getDate(cycleDetails.start_date);
@ -189,7 +189,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
<LayersIcon className="h-4 w-4 text-custom-text-300" /> <LayersIcon className="h-4 w-4 text-custom-text-300" />
<span className="text-xs text-custom-text-300">{issueCount}</span> <span className="text-xs text-custom-text-300">{issueCount}</span>
</div> </div>
{cycleDetails.assignee_ids.length > 0 && ( {cycleDetails.assignee_ids && cycleDetails.assignee_ids.length > 0 && (
<Tooltip tooltipContent={`${cycleDetails.assignee_ids.length} Members`} isMobile={isMobile}> <Tooltip tooltipContent={`${cycleDetails.assignee_ids.length} Members`} isMobile={isMobile}>
<div className="flex cursor-default items-center gap-1"> <div className="flex cursor-default items-center gap-1">
<AvatarGroup showTooltip={false}> <AvatarGroup showTooltip={false}>

View File

@ -25,7 +25,7 @@ export const CycleGanttBlock: React.FC<Props> = observer((props) => {
// derived values // derived values
const cycleDetails = getCycleById(cycleId); const cycleDetails = getCycleById(cycleId);
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
const cycleStatus = cycleDetails?.status.toLocaleLowerCase(); const cycleStatus = cycleDetails?.status?.toLocaleLowerCase();
return ( return (
<div <div
@ -74,7 +74,7 @@ export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => {
// derived values // derived values
const cycleDetails = getCycleById(cycleId); const cycleDetails = getCycleById(cycleId);
const cycleStatus = cycleDetails?.status.toLocaleLowerCase(); const cycleStatus = cycleDetails?.status?.toLocaleLowerCase();
return ( return (
<Link <Link

View File

@ -216,7 +216,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
<div className="relative flex flex-shrink-0 items-center gap-3"> <div className="relative flex flex-shrink-0 items-center gap-3">
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}> <Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
<div className="flex w-10 cursor-default items-center justify-center"> <div className="flex w-10 cursor-default items-center justify-center">
{cycleDetails.assignee_ids?.length > 0 ? ( {cycleDetails.assignee_ids && cycleDetails.assignee_ids?.length > 0 ? (
<AvatarGroup showTooltip={false}> <AvatarGroup showTooltip={false}>
{cycleDetails.assignee_ids?.map((assignee_id) => { {cycleDetails.assignee_ids?.map((assignee_id) => {
const member = getUserDetails(assignee_id); const member = getUserDetails(assignee_id);

View File

@ -37,7 +37,7 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
const { getCycleById, restoreCycle } = useCycle(); const { getCycleById, restoreCycle } = useCycle();
// derived values // derived values
const cycleDetails = getCycleById(cycleId); const cycleDetails = getCycleById(cycleId);
const isCompleted = cycleDetails?.status.toLowerCase() === "completed"; const isCompleted = cycleDetails?.status?.toLowerCase() === "completed";
// auth // auth
const isEditingAllowed = const isEditingAllowed =
!!currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId] >= EUserProjectRoles.MEMBER; !!currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId] >= EUserProjectRoles.MEMBER;

View File

@ -211,7 +211,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
// [workspaceSlug, projectId, cycleId, issueFilters, updateFilters] // [workspaceSlug, projectId, cycleId, issueFilters, updateFilters]
// ); // );
const cycleStatus = cycleDetails?.status.toLocaleLowerCase(); const cycleStatus = cycleDetails?.status?.toLocaleLowerCase();
const isCompleted = cycleStatus === "completed"; const isCompleted = cycleStatus === "completed";
const startDate = getDate(cycleDetails?.start_date); const startDate = getDate(cycleDetails?.start_date);

View File

@ -134,9 +134,11 @@ export const TransferIssuesModal: React.FC<Props> = observer((props) => {
<ContrastIcon className="h-5 w-5" /> <ContrastIcon className="h-5 w-5" />
<div className="flex w-full justify-between"> <div className="flex w-full justify-between">
<span>{cycleDetails?.name}</span> <span>{cycleDetails?.name}</span>
{cycleDetails.status && (
<span className=" flex items-center rounded-full bg-custom-background-80 px-2 capitalize"> <span className=" flex items-center rounded-full bg-custom-background-80 px-2 capitalize">
{cycleDetails.status.toLocaleLowerCase()} {cycleDetails.status.toLocaleLowerCase()}
</span> </span>
)}
</div> </div>
</button> </button>
); );

View File

@ -146,7 +146,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
const issueCount = cycleDetails const issueCount = cycleDetails
? issueFilters?.displayFilters?.sub_issue ? issueFilters?.displayFilters?.sub_issue && cycleDetails?.sub_issues
? cycleDetails.total_issues + cycleDetails?.sub_issues ? cycleDetails.total_issues + cycleDetails?.sub_issues
: cycleDetails.total_issues : cycleDetails.total_issues
: undefined; : undefined;

View File

@ -147,7 +147,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
const issueCount = moduleDetails const issueCount = moduleDetails
? issueFilters?.displayFilters?.sub_issue ? issueFilters?.displayFilters?.sub_issue && moduleDetails.sub_issues
? moduleDetails.total_issues + moduleDetails.sub_issues ? moduleDetails.total_issues + moduleDetails.sub_issues
: moduleDetails.total_issues : moduleDetails.total_issues
: undefined; : undefined;

View File

@ -62,7 +62,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {}); const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {});
const isCompletedAndEmpty = isCompletedCycleSnapshotAvailable || cycleDetails?.status.toLowerCase() === "completed"; const isCompletedAndEmpty = isCompletedCycleSnapshotAvailable || cycleDetails?.status?.toLowerCase() === "completed";
const emptyStateType = isCompletedAndEmpty const emptyStateType = isCompletedAndEmpty
? EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES ? EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES

View File

@ -51,7 +51,8 @@ export const FilterCycle: React.FC<Props> = observer((props) => {
else setItemsToRender(sortedOptions.length); else setItemsToRender(sortedOptions.length);
}; };
const cycleStatus = (status: TCycleGroups) => (status ? status.toLocaleLowerCase() : "draft") as TCycleGroups; const cycleStatus = (status: TCycleGroups | undefined) =>
(status ? status.toLocaleLowerCase() : "draft") as TCycleGroups;
return ( return (
<> <>

View File

@ -41,7 +41,7 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
const isEditingAllowed = const isEditingAllowed =
!!currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId] >= EUserProjectRoles.MEMBER; !!currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId] >= EUserProjectRoles.MEMBER;
const moduleState = moduleDetails?.status.toLocaleLowerCase(); const moduleState = moduleDetails?.status?.toLocaleLowerCase();
const isInArchivableGroup = !!moduleState && ["completed", "cancelled"].includes(moduleState); const isInArchivableGroup = !!moduleState && ["completed", "cancelled"].includes(moduleState);
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => { const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {

View File

@ -84,7 +84,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker(); const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker();
const moduleDetails = getModuleById(moduleId); const moduleDetails = getModuleById(moduleId);
const moduleState = moduleDetails?.status.toLocaleLowerCase(); const moduleState = moduleDetails?.status?.toLocaleLowerCase();
const isInArchivableGroup = !!moduleState && ["completed", "cancelled"].includes(moduleState); const isInArchivableGroup = !!moduleState && ["completed", "cancelled"].includes(moduleState);
const { reset, control } = useForm({ const { reset, control } = useForm({

View File

@ -13,6 +13,7 @@ import { ICycle, TCycleFilters } from "@plane/types";
export const orderCycles = (cycles: ICycle[]): ICycle[] => { export const orderCycles = (cycles: ICycle[]): ICycle[] => {
if (cycles.length === 0) return []; if (cycles.length === 0) return [];
const acceptedStatuses = ["current", "upcoming", "draft"];
const STATUS_ORDER: { const STATUS_ORDER: {
[key: string]: number; [key: string]: number;
} = { } = {
@ -21,10 +22,10 @@ export const orderCycles = (cycles: ICycle[]): ICycle[] => {
draft: 3, draft: 3,
}; };
let filteredCycles = cycles.filter((c) => c.status.toLowerCase() !== "completed"); let filteredCycles = cycles.filter((c) => acceptedStatuses.includes(c.status?.toLowerCase() ?? ""));
filteredCycles = sortBy(filteredCycles, [ filteredCycles = sortBy(filteredCycles, [
(c) => STATUS_ORDER[c.status.toLowerCase()], (c) => STATUS_ORDER[c.status?.toLowerCase() ?? ""],
(c) => (c.status.toLowerCase() === "upcoming" ? c.start_date : c.name.toLowerCase()), (c) => (c.status?.toLowerCase() === "upcoming" ? c.start_date : c.name.toLowerCase()),
]); ]);
return filteredCycles; return filteredCycles;
@ -41,7 +42,7 @@ export const shouldFilterCycle = (cycle: ICycle, filter: TCycleFilters): boolean
Object.keys(filter).forEach((key) => { Object.keys(filter).forEach((key) => {
const filterKey = key as keyof TCycleFilters; const filterKey = key as keyof TCycleFilters;
if (filterKey === "status" && filter.status && filter.status.length > 0) if (filterKey === "status" && filter.status && filter.status.length > 0)
fallsInFilters = fallsInFilters && filter.status.includes(cycle.status.toLowerCase()); fallsInFilters = fallsInFilters && filter.status.includes(cycle.status?.toLowerCase() ?? "");
if (filterKey === "start_date" && filter.start_date && filter.start_date.length > 0) { if (filterKey === "start_date" && filter.start_date && filter.start_date.length > 0) {
const startDate = getDate(cycle.start_date); const startDate = getDate(cycle.start_date);
filter.start_date.forEach((dateFilter) => { filter.start_date.forEach((dateFilter) => {

View File

@ -55,7 +55,7 @@ export const shouldFilterModule = (
Object.keys(filters).forEach((key) => { Object.keys(filters).forEach((key) => {
const filterKey = key as keyof TModuleFilters; const filterKey = key as keyof TModuleFilters;
if (filterKey === "status" && filters.status && filters.status.length > 0) if (filterKey === "status" && filters.status && filters.status.length > 0)
fallsInFilters = fallsInFilters && filters.status.includes(module.status.toLowerCase()); fallsInFilters = fallsInFilters && filters.status.includes(module.status?.toLowerCase() ?? "");
if (filterKey === "lead" && filters.lead && filters.lead.length > 0) if (filterKey === "lead" && filters.lead && filters.lead.length > 0)
fallsInFilters = fallsInFilters && filters.lead.includes(`${module.lead_id}`); fallsInFilters = fallsInFilters && filters.lead.includes(`${module.lead_id}`);
if (filterKey === "members" && filters.members && filters.members.length > 0) { if (filterKey === "members" && filters.members && filters.members.length > 0) {

View File

@ -256,7 +256,7 @@ export class CycleStore implements ICycleStore {
(c) => (c) =>
c.project_id === projectId && c.project_id === projectId &&
!c.archived_at && !c.archived_at &&
c.status.toLowerCase() === "completed" && c.status?.toLowerCase() === "completed" &&
c.name.toLowerCase().includes(searchQuery.toLowerCase()) && c.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
shouldFilterCycle(c, filters ?? {}) shouldFilterCycle(c, filters ?? {})
); );

View File

@ -380,7 +380,7 @@ export class ModulesStore implements IModuleStore {
) => { ) => {
const originalModuleDetails = this.getModuleById(moduleId); const originalModuleDetails = this.getModuleById(moduleId);
try { try {
const linkModules = originalModuleDetails?.link_module.map((link) => const linkModules = originalModuleDetails?.link_module?.map((link) =>
link.id === linkId ? { ...link, ...data } : link link.id === linkId ? { ...link, ...data } : link
); );
runInAction(() => { runInAction(() => {
@ -407,7 +407,7 @@ export class ModulesStore implements IModuleStore {
deleteModuleLink = async (workspaceSlug: string, projectId: string, moduleId: string, linkId: string) => deleteModuleLink = async (workspaceSlug: string, projectId: string, moduleId: string, linkId: string) =>
await this.moduleService.deleteModuleLink(workspaceSlug, projectId, moduleId, linkId).then(() => { await this.moduleService.deleteModuleLink(workspaceSlug, projectId, moduleId, linkId).then(() => {
const moduleDetails = this.getModuleById(moduleId); const moduleDetails = this.getModuleById(moduleId);
const linkModules = moduleDetails?.link_module.filter((link) => link.id !== linkId); const linkModules = moduleDetails?.link_module?.filter((link) => link.id !== linkId);
runInAction(() => { runInAction(() => {
set(this.moduleMap, [moduleId, "link_module"], linkModules); set(this.moduleMap, [moduleId, "link_module"], linkModules);
}); });