[WEB-5338] feat: initiative scope UI enhancements (#4637)
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user