Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 66 additions & 33 deletions web/core/components/modules/module-card-item.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
"use client";

import React, { useRef } from "react";
import React, { SyntheticEvent, useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname, useSearchParams } from "next/navigation";
import { CalendarCheck2, CalendarClock, Info, MoveRight, SquareUser } from "lucide-react";
import { Info, SquareUser } from "lucide-react";
// ui
import { IModule } from "@plane/types";
import { Card, FavoriteStar, LayersIcon, LinearProgressIndicator, Tooltip, setPromiseToast } from "@plane/ui";
import { Card, FavoriteStar, LayersIcon, LinearProgressIndicator, TOAST_TYPE, Tooltip, setPromiseToast, setToast } from "@plane/ui";
// components
import { DateRangeDropdown } from "@/components/dropdowns";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
import { ModuleQuickActions } from "@/components/modules";
import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown";
// constants
import { PROGRESS_STATE_GROUPS_DETAILS } from "@/constants/common";
import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "@/constants/event-tracker";
import { MODULE_STATUS } from "@/constants/module";
import { EUserProjectRoles } from "@/constants/project";
// helpers
import { getDate, renderFormattedDate } from "@/helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { generateQueryParams } from "@/helpers/router.helper";
// hooks
import { useEventTracker, useMember, useModule, useProjectEstimates, useUser } from "@/hooks/store";
Expand All @@ -43,14 +45,18 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
const {
membership: { currentProjectRole },
} = useUser();
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites, updateModuleDetails } = useModule();
const { getUserDetails } = useMember();
const { captureEvent } = useEventTracker();
const { currentActiveEstimateId, areEstimateEnabledByProjectId, estimateById } = useProjectEstimates();

// derived values
const moduleDetails = getModuleById(moduleId);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const isDisabled = !isEditingAllowed || !!moduleDetails?.archived_at;
const renderIcon = Boolean(moduleDetails?.start_date) || Boolean(moduleDetails?.target_date);


const { isMobile } = usePlatformOS();
const handleAddToFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
Expand Down Expand Up @@ -110,6 +116,31 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
});
};

const handleEventPropagation = (e: SyntheticEvent<HTMLDivElement>) => {
e.stopPropagation();
e.preventDefault();
};

const handleModuleDetailsChange = async (payload: Partial<IModule>) => {
if (!workspaceSlug || !projectId) return;

await updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId, payload)
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Module updated successfully.",
});
})
.catch((err) => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.detail ?? "Module could not be updated. Please try again.",
});
});
};

const openModuleOverview = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
Expand Down Expand Up @@ -147,10 +178,6 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
? moduleDetails?.completed_estimate_points || 0
: moduleDetails.completed_issues;

const endDate = getDate(moduleDetails.target_date);
const startDate = getDate(moduleDetails.start_date);

const isDateValid = moduleDetails.target_date || moduleDetails.start_date;

// const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();

Expand All @@ -174,25 +201,21 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
}));

return (
<div className="relative">
<div className="relative" data-prevent-nprogress>
<Link ref={parentRef} href={`/${workspaceSlug}/projects/${moduleDetails.project_id}/modules/${moduleDetails.id}`}>
<Card>
<div>
<div className="flex items-center justify-between gap-2">
<Tooltip tooltipContent={moduleDetails.name} position="top" isMobile={isMobile}>
<span className="truncate text-base font-medium">{moduleDetails.name}</span>
</Tooltip>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2" onClick={handleEventPropagation}>
{moduleStatus && (
<span
className="flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs"
style={{
color: moduleStatus.color,
backgroundColor: `${moduleStatus.color}20`,
}}
>
{moduleStatus.label}
</span>
<ModuleStatusDropdown
isDisabled={isDisabled}
moduleDetails={moduleDetails}
handleModuleDetailsChange={handleModuleDetailsChange}
/>
)}
<button onClick={openModuleOverview}>
<Info className="h-4 w-4 text-custom-text-400" />
Expand All @@ -217,18 +240,28 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
)}
</div>
<LinearProgressIndicator size="lg" data={progressIndicatorData} />
<div className="flex items-center justify-between py-0.5">
{isDateValid ? (
<div className="h-6 flex items-center gap-1.5 text-custom-text-300 border-[0.5px] border-custom-border-300 rounded text-xs px-2 cursor-default">
<CalendarClock className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">{renderFormattedDate(startDate)}</span>
<MoveRight className="h-3 w-3 flex-shrink-0" />
<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">{renderFormattedDate(endDate)}</span>
</div>
) : (
<span className="text-xs text-custom-text-400">No due date</span>
)}
<div className="flex items-center justify-between py-0.5" onClick={handleEventPropagation}>
<DateRangeDropdown
buttonContainerClassName={`h-6 w-full flex ${isDisabled ? "cursor-not-allowed" : "cursor-pointer"} items-center gap-1.5 text-custom-text-300 border-[0.5px] border-custom-border-300 rounded text-xs`}
buttonVariant="transparent-with-text"
className="h-7"
value={{
from: getDate(moduleDetails.start_date),
to: getDate(moduleDetails.target_date),
}}
onSelect={(val) => {
handleModuleDetailsChange({
start_date: (val?.from ? renderFormattedPayloadDate(val.from) : null),
target_date: (val?.to ? renderFormattedPayloadDate(val.to) : null)
})
}}
placeholder={{
from: "Start date",
to: "End date",
}}
disabled={isDisabled}
hideIcon={{ from: renderIcon ?? true, to: renderIcon }}
/>
</div>
</div>
</Card>
Expand All @@ -254,4 +287,4 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
</div>
</div>
);
});
});
84 changes: 55 additions & 29 deletions web/core/components/modules/module-list-item-action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@ import React, { FC } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// icons
import { CalendarCheck2, CalendarClock, MoveRight, SquareUser } from "lucide-react";
import { SquareUser } from "lucide-react";
// types
import { IModule } from "@plane/types";
// ui
import { FavoriteStar, Tooltip, setPromiseToast } from "@plane/ui";
import { FavoriteStar, TOAST_TYPE, Tooltip, setPromiseToast, setToast } from "@plane/ui";
// components
import { DateRangeDropdown } from "@/components/dropdowns";
import { ModuleQuickActions } from "@/components/modules";
import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown";
// constants
import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "@/constants/event-tracker";
import { MODULE_STATUS } from "@/constants/module";
import { EUserProjectRoles } from "@/constants/project";
// helpers
import { getDate, renderFormattedDate } from "@/helpers/date-time.helper";
// hooks
import { renderFormattedPayloadDate, getDate } from "@/helpers/date-time.helper";
import { useEventTracker, useMember, useModule, useUser } from "@/hooks/store";
import { ButtonAvatars } from "../dropdowns/member/avatar";

Expand All @@ -35,19 +36,16 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
const {
membership: { currentProjectRole },
} = useUser();
const { addModuleToFavorites, removeModuleFromFavorites } = useModule();
const { addModuleToFavorites, removeModuleFromFavorites, updateModuleDetails } = useModule();
const { getUserDetails } = useMember();
const { captureEvent } = useEventTracker();

// derived values
const endDate = getDate(moduleDetails.target_date);
const startDate = getDate(moduleDetails.start_date);

const renderDate = moduleDetails.start_date || moduleDetails.target_date;

const moduleStatus = MODULE_STATUS.find((status) => status.value === moduleDetails.status);

const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const isDisabled = !isEditingAllowed || !!moduleDetails?.archived_at;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed issue resolution: Simplified boolean expression.

The boolean expression for isDisabled has been simplified, enhancing readability and maintainability. This change correctly implements previous suggestions.

const renderIcon = Boolean(moduleDetails.start_date) || Boolean(moduleDetails.target_date);

// handlers
const handleAddToFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
Expand Down Expand Up @@ -108,30 +106,58 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
});
};

const handleModuleDetailsChange = async (payload: Partial<IModule>) => {
if (!workspaceSlug || !projectId) return;

await updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId, payload)
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Module updated successfully.",
});
})
.catch((err) => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.detail ?? "Module could not be updated. Please try again.",
});
});
};

const moduleLeadDetails = moduleDetails.lead_id ? getUserDetails(moduleDetails.lead_id) : undefined;

return (
<>
{renderDate && (
<div className="h-6 flex items-center gap-1.5 text-custom-text-300 border-[0.5px] border-custom-border-300 rounded text-xs px-2 cursor-default">
<CalendarClock className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">{renderFormattedDate(startDate)}</span>
<MoveRight className="h-3 w-3 flex-shrink-0" />
<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">{renderFormattedDate(endDate)}</span>
</div>
)}
<DateRangeDropdown
buttonContainerClassName={`h-6 w-full flex ${isDisabled ? "cursor-not-allowed" : "cursor-pointer"} items-center gap-1.5 text-custom-text-300 border-[0.5px] border-custom-border-300 rounded text-xs`}
buttonVariant="transparent-with-text"
className="h-7"
value={{
from: getDate(moduleDetails.start_date),
to: getDate(moduleDetails.target_date),
}}
onSelect={(val) => {
handleModuleDetailsChange({
start_date: (val?.from ? renderFormattedPayloadDate(val.from) : null),
target_date: (val?.to ? renderFormattedPayloadDate(val.to) : null)
})
}}
placeholder={{
from: "Start date",
to: "End date",
}}
disabled={isDisabled}
hideIcon={{ from: renderIcon ?? true, to: renderIcon }}
/>

{moduleStatus && (
<span
className="flex h-6 w-20 flex-shrink-0 items-center justify-center rounded-sm text-center text-xs cursor-default"
style={{
color: moduleStatus.color,
backgroundColor: `${moduleStatus.color}20`,
}}
>
{moduleStatus.label}
</span>
<ModuleStatusDropdown
isDisabled={isDisabled}
moduleDetails={moduleDetails}
handleModuleDetailsChange={handleModuleDetailsChange}
/>
)}

{moduleLeadDetails ? (
Expand Down Expand Up @@ -165,4 +191,4 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
)}
</>
);
});
});
50 changes: 50 additions & 0 deletions web/core/components/modules/module-status-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { FC } from 'react'
import { observer } from 'mobx-react';
import { IModule } from '@plane/types';
import { CustomSelect, TModuleStatus, ModuleStatusIcon } from '@plane/ui'
import { MODULE_STATUS } from '@/constants/module'

type Props = {
isDisabled: boolean;
moduleDetails: IModule;
handleModuleDetailsChange: (payload: Partial<IModule>) => Promise<void>;
};

export const ModuleStatusDropdown : FC<Props> = observer((props : Props) => {
const {isDisabled, moduleDetails, handleModuleDetailsChange} = props;
const moduleStatus = MODULE_STATUS.find((status) => status.value === moduleDetails.status);

if(!moduleStatus) return <></>

return (
<CustomSelect
customButton={
<span
className={`flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs ${
isDisabled ? "cursor-not-allowed" : "cursor-pointer"
}`}
style={{
color: moduleStatus ? moduleStatus.color : "#a3a3a2",
backgroundColor: moduleStatus ? `${moduleStatus.color}20` : "#a3a3a220",
}}
>
{moduleStatus?.label ?? "Backlog"}
</span>
}
value={moduleStatus?.value}
onChange={(val: TModuleStatus)=>{
handleModuleDetailsChange({status: val})
}}
disabled={isDisabled}
>
{MODULE_STATUS.map((status) => (
<CustomSelect.Option key={status.value} value={status.value}>
<div className="flex items-center gap-2">
<ModuleStatusIcon status={status.value} />
{status.label}
</div>
</CustomSelect.Option>
))}
</CustomSelect>
)
})