[WEB-5338] feat: initiative scope UI enhancements (#4637)

This commit is contained in:
Jayash Tripathy
2025-11-06 19:05:36 +05:30
committed by GitHub
parent 3e15ecd524
commit d79e185234
8 changed files with 109 additions and 64 deletions
@@ -54,6 +54,8 @@ export const UpdateStatusPills = (props: TStatusPills) => {
[EUpdateStatus.AT_RISK]: analytics?.at_risk_updates ?? 0,
[EUpdateStatus.OFF_TRACK]: analytics?.off_track_updates ?? 0,
};
const getStatusText = (status: string): string => capitalizeFirstLetter(status.replaceAll("-", " ").toLowerCase());
// react-popper derived values
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "bottom",
@@ -79,9 +81,11 @@ export const UpdateStatusPills = (props: TStatusPills) => {
onClick={(e) => e.stopPropagation()}
>
<Tooltip tooltipContent={status && capitalizeFirstLetter(status.replaceAll("-", " ").toLowerCase())}>
<button className="flex items-center gap-1 border border-custom-border-300 rounded-full px-2 py-1 bg-custom-background-100 hover:shadow-sm">
<UpdateStatusIcons statusType={status as EUpdateStatus} showBackground={false} />
<span className="text-xs font-medium text-custom-text-300">{count}</span>
<button className="flex items-center gap-1 border border-custom-border-300 rounded-md px-1 py-1 bg-custom-background-100">
<UpdateStatusIcons size="xs" statusType={status as EUpdateStatus} showBackground={false} />
<span className="text-xs font-semibold text-custom-text-300">
{count} {getStatusText(status)}
</span>
</button>
</Tooltip>
</Popover.Button>
@@ -0,0 +1,55 @@
"use client";
import type { FC } from "react";
import { differenceInCalendarDays } from "date-fns";
import { observer } from "mobx-react";
// plane imports
import type { DateRange } from "@plane/propel/calendar";
import { getDate, renderFormattedPayloadDate } from "@plane/utils";
// core components
import { DateRangeDropdown } from "@/components/dropdowns/date-range";
// plane Web
import { useInitiatives } from "@/plane-web/hooks/store/use-initiatives";
import type { TInitiative } from "@/plane-web/types/initiative";
type TInitiativeDateRangeDropdownProps = {
initiative: TInitiative;
workspaceSlug: string;
};
export const InitiativeDateRangeDropdown: FC<TInitiativeDateRangeDropdownProps> = observer((props) => {
const { initiative, workspaceSlug } = props;
const {
initiative: { updateInitiative },
} = useInitiatives();
const handleDateChange = (range: DateRange | undefined) => {
updateInitiative?.(workspaceSlug, initiative.id, {
start_date: range?.from ? renderFormattedPayloadDate(range.from) : null,
end_date: range?.to ? renderFormattedPayloadDate(range.to) : null,
});
};
const endDate = getDate(initiative.end_date);
const shouldHighlightEndDate = endDate && differenceInCalendarDays(endDate, new Date()) <= 0;
return (
<DateRangeDropdown
value={{
from: getDate(initiative.start_date) || undefined,
to: endDate || undefined,
}}
onSelect={handleDateChange}
hideIcon={{
from: false,
}}
isClearable
mergeDates
buttonVariant={initiative.start_date || initiative.end_date ? "border-with-text" : "border-without-text"}
buttonClassName={shouldHighlightEndDate ? "text-red-500" : ""}
showTooltip
renderPlaceholder={false}
renderInPortal
/>
);
});
@@ -3,11 +3,8 @@ import { observer } from "mobx-react";
// plane imports
import { getRandomLabelColor } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import type { DateRange } from "@plane/propel/calendar";
import { setToast, TOAST_TYPE } from "@plane/propel/toast";
import { getDate, renderFormattedPayloadDate } from "@plane/utils";
// core components
import { DateRangeDropdown } from "@/components/dropdowns/date-range";
import { MemberDropdown } from "@/components/dropdowns/member/dropdown";
import { ProjectDropdown } from "@/components/dropdowns/project/dropdown";
// plane Web
@@ -16,6 +13,7 @@ import type { TInitiative } from "@/plane-web/types/initiative";
// ee components
import { EpicsDropdown } from "../../dropdowns/epics";
// local components
import { InitiativeDateRangeDropdown } from "./initiative-date-range-dropdown";
import { InitiativeLabelDropdown } from "./labels/initiative-label-dropdown";
import { PropertyBlockWrapper } from "./property-block-wrapper";
import { InitiativeStateDropdown } from "./states/initiative-state-dropdown";
@@ -91,13 +89,6 @@ export const InitiativesBlockProperties = observer((props: Props) => {
}
};
const handleDateChange = (range: DateRange | undefined) => {
updateInitiative?.(workspaceSlug, initiative.id, {
start_date: range?.from ? renderFormattedPayloadDate(range.from) : null,
end_date: range?.to ? renderFormattedPayloadDate(range.to) : null,
});
};
return (
<div
className={`relative flex flex-wrap ${isSidebarCollapsed ? "md:flex-grow md:flex-shrink-0" : "lg:flex-grow lg:flex-shrink-0"} items-center gap-2 whitespace-nowrap`}
@@ -107,22 +98,7 @@ export const InitiativesBlockProperties = observer((props: Props) => {
>
{/* dates */}
<PropertyBlockWrapper>
<DateRangeDropdown
value={{
from: getDate(initiative.start_date) || undefined,
to: getDate(initiative.end_date) || undefined,
}}
onSelect={handleDateChange}
hideIcon={{
from: false,
}}
isClearable
mergeDates
buttonVariant={initiative.start_date || initiative.end_date ? "border-with-text" : "border-without-text"}
showTooltip
renderPlaceholder={false}
renderInPortal
/>
<InitiativeDateRangeDropdown initiative={initiative} workspaceSlug={workspaceSlug} />
</PropertyBlockWrapper>
{/* projects */}
@@ -21,13 +21,6 @@ export type TInitiativeLabelDropdownProps = {
value: string[];
onChange?: (value: string[]) => void;
disabled?: boolean;
buttonVariant?:
| "border-with-text"
| "border-without-text"
| "background-with-text"
| "background-without-text"
| "transparent-with-text"
| "transparent-without-text";
buttonClassName?: string;
className?: string;
tabIndex?: number;
@@ -16,13 +16,6 @@ export type TInitiativeStateDropdownProps = {
value: TInitiativeStates;
onChange?: (value: TInitiativeStates) => void;
disabled?: boolean;
buttonVariant?:
| "border-with-text"
| "border-without-text"
| "background-with-text"
| "background-without-text"
| "transparent-with-text"
| "transparent-without-text";
buttonClassName?: string;
className?: string;
tabIndex?: number;
@@ -35,6 +35,7 @@ export const InitiativeMainContentRoot: FC<Props> = observer((props) => {
toggleProjectModal={toggleProjectModal}
toggleEpicModal={toggleEpicModal}
/>
<InitiativeProgressSection initiativeId={initiativeId} />
<ScopeBreakdown
workspaceSlug={workspaceSlug}
initiativeId={initiativeId}
@@ -42,7 +43,6 @@ export const InitiativeMainContentRoot: FC<Props> = observer((props) => {
toggleEpicModal={toggleEpicModal}
disabled={disabled}
/>
<InitiativeProgressSection initiativeId={initiativeId} />
<InitiativeCollapsibleSection
workspaceSlug={workspaceSlug}
initiativeId={initiativeId}
@@ -5,7 +5,7 @@ import { PlusIcon } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { ScopeIcon } from "@plane/propel/icons";
import { CircularProgressIndicator, ControlLink, Loader } from "@plane/ui";
import { Button, CircularProgressIndicator, ControlLink, Loader } from "@plane/ui";
// plane web imports
import { SectionEmptyState } from "@/plane-web/components/common/layout/main/common/empty-state";
import { SectionWrapper } from "@/plane-web/components/common/layout/main/common/section-wrapper";
@@ -43,33 +43,31 @@ const DataCard = (props: TDataCardProps) => {
return (
<ControlLink
href={`/${workspaceSlug}/initiatives/${initiativeId}/scope`}
className="group bg-custom-background-90 rounded-md p-3 w-full space-y-2 hover:cursor-pointer hover:bg-custom-background-80 transition-colors block"
className="group rounded-md py-3 px-4 w-full hover:cursor-pointer hover:bg-custom-background-80 transition-colors flex justify-between border border-custom-border-100 bg-custom-background-100"
onClick={handleControlLinkClick}
>
<div className="flex w-full justify-between text-custom-text-300 ">
<div className="flex w-full justify-between text-custom-text-300 flex-1 ">
<div className="flex gap-2 items-center">
<div className="font-semibold text-base capitalize">{type}s</div>
<span className="text-custom-text-400 font-medium text-xs">{count}</span>
</div>
</div>
{data ? (
<div className="border border-custom-border-100 bg-custom-background-100 rounded-md p-2 flex md:flex-row flex-col gap-2 justify-between w-full">
<div className="flex-1 flex flex-col gap-3">
<div className="text-custom-text-350 font-medium text-sm">Progress</div>
<div className="rounded-md flex gap-3 justify-between items-center">
<div className="flex flex-col gap-3">
<div className="flex gap-2 items-center">
<CircularProgressIndicator
percentage={progress}
strokeWidth={4}
size={24}
size={18}
strokeColor="stroke-green-500"
/>
<span className="flex items-baseline text-custom-text-200 justify-center">
<span className="font-semibold">{progress}%</span>
<span className="flex items-baseline text-custom-text-200 justify-center text-sm">
<span>{progress}%</span>
</span>
</div>
</div>
<div className="flex-1 flex flex-col gap-3">
<div className="text-custom-text-350 font-medium text-sm">Updates</div>
<div className="flex flex-col gap-3">
<div role="group" aria-label="update-status-pills">
<UpdateStatusPills
defaultTab={type}
@@ -86,9 +84,23 @@ const DataCard = (props: TDataCardProps) => {
</div>
</div>
) : (
<Loader className="w-full h-full">
<Loader.Item height="71px" width="100%" />
</Loader>
<div className="rounded-md flex gap-3 justify-between items-center">
<div className="flex flex-col gap-3">
<div className="flex gap-2 items-center">
<Loader>
<Loader.Item height="18px" width="18px" />
</Loader>
<Loader>
<Loader.Item height="14px" width="40px" />
</Loader>
</div>
</div>
<div className="flex flex-col gap-3">
<Loader>
<Loader.Item height="20px" width="120px" />
</Loader>
</div>
</div>
)}
</ControlLink>
);
@@ -132,10 +144,10 @@ export const ScopeBreakdown = observer((props: Props) => {
<div className="flex justify-between items-center">
<div className="text-custom-text-300 font-semibold text-base">{t("initiatives.scope.breakdown")}</div>
{/* button */}
<div className="flex gap-2">
<div className="flex gap-2 items-center">
<Link
href={`/${workspaceSlug}/initiatives/${initiativeId}/scope`}
className="text-custom-primary-100 font-medium text-sm"
className=" font-medium text-sm text-custom-text-200"
>
{t("initiatives.scope.view_scope")}
</Link>
@@ -143,7 +155,15 @@ export const ScopeBreakdown = observer((props: Props) => {
disabled={disabled}
workspaceSlug={workspaceSlug}
initiativeId={initiativeId}
customButton={<PlusIcon className="size-4" />}
customButton={
<Button
variant="link-neutral"
size="sm"
className="!p-1 bg-custom-background-80 hover:bg-custom-background-90"
>
<PlusIcon className="size-4" />
</Button>
}
/>
</div>
</div>
@@ -158,7 +178,7 @@ export const ScopeBreakdown = observer((props: Props) => {
}
/>
) : (
<div className="grid gap-4 w-full grid-cols-1 @sm:grid-cols-1 @md:grid-cols-2">
<div className="grid w-full grid-cols-1 @sm:grid-cols-1 bg-custom-background-90 rounded-lg p-2 gap-2">
{/* Projects */}
{shouldShowProjectsCard && (
<DataCard
@@ -18,15 +18,19 @@ export const StatusOptions = {
},
};
type TUpdateStatusIcons = {
export type TUpdateStatusIcons = {
statusType?: EUpdateStatus;
showBackground?: boolean;
size?: "sm" | "md";
size?: "xs" | "sm" | "md";
showText?: boolean;
className?: string;
};
const sizes = {
xs: {
icon: 12,
container: "w-4 h-4",
},
sm: {
icon: 16,
container: "w-6 h-6",