[WEB-5160] chore: propel banner and archived work item improvements (#7999)
This commit is contained in:
parent
68fd2463f4
commit
33b6405695
7 changed files with 399 additions and 12 deletions
|
|
@ -1,9 +1,12 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// ui
|
// ui
|
||||||
|
import { Banner } from "@plane/propel/banner";
|
||||||
|
import { Button } from "@plane/propel/button";
|
||||||
|
import { ArchiveIcon } from "@plane/propel/icons";
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { PageHead } from "@/components/core/page-title";
|
import { PageHead } from "@/components/core/page-title";
|
||||||
|
|
@ -16,6 +19,7 @@ import { useProject } from "@/hooks/store/use-project";
|
||||||
const ArchivedIssueDetailsPage = observer(() => {
|
const ArchivedIssueDetailsPage = observer(() => {
|
||||||
// router
|
// router
|
||||||
const { workspaceSlug, projectId, archivedIssueId } = useParams();
|
const { workspaceSlug, projectId, archivedIssueId } = useParams();
|
||||||
|
const router = useRouter();
|
||||||
// states
|
// states
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
|
|
@ -62,18 +66,35 @@ const ArchivedIssueDetailsPage = observer(() => {
|
||||||
</div>
|
</div>
|
||||||
</Loader>
|
</Loader>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full overflow-hidden">
|
<>
|
||||||
<div className="h-full w-full space-y-3 divide-y-2 divide-custom-border-200 overflow-y-auto">
|
<Banner
|
||||||
{workspaceSlug && projectId && archivedIssueId && (
|
variant="warning"
|
||||||
<IssueDetailRoot
|
title="This work item has been archived. Visit the Archives section to restore it."
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
icon={<ArchiveIcon className="size-4" />}
|
||||||
projectId={projectId.toString()}
|
action={
|
||||||
issueId={archivedIssueId.toString()}
|
<Button
|
||||||
is_archived
|
variant="neutral-primary"
|
||||||
/>
|
size="sm"
|
||||||
)}
|
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/archives/issues/`)}
|
||||||
|
>
|
||||||
|
Go to archives
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
className="border-b border-custom-border-200"
|
||||||
|
/>
|
||||||
|
<div className="flex h-full overflow-hidden">
|
||||||
|
<div className="h-full w-full space-y-3 divide-y-2 divide-custom-border-200 overflow-y-auto">
|
||||||
|
{workspaceSlug && projectId && archivedIssueId && (
|
||||||
|
<IssueDetailRoot
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={projectId.toString()}
|
||||||
|
issueId={archivedIssueId.toString()}
|
||||||
|
is_archived
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@
|
||||||
"import": "./dist/avatar/index.mjs",
|
"import": "./dist/avatar/index.mjs",
|
||||||
"require": "./dist/avatar/index.js"
|
"require": "./dist/avatar/index.js"
|
||||||
},
|
},
|
||||||
|
"./banner": {
|
||||||
|
"import": "./dist/banner/index.mjs",
|
||||||
|
"require": "./dist/banner/index.js"
|
||||||
|
},
|
||||||
"./button": {
|
"./button": {
|
||||||
"import": "./dist/button/index.mjs",
|
"import": "./dist/button/index.mjs",
|
||||||
"require": "./dist/button/index.js"
|
"require": "./dist/button/index.js"
|
||||||
|
|
|
||||||
181
packages/propel/src/banner/banner.stories.tsx
Normal file
181
packages/propel/src/banner/banner.stories.tsx
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
|
import { Banner } from "./banner";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Components/Banner",
|
||||||
|
component: Banner,
|
||||||
|
parameters: {
|
||||||
|
layout: "fullscreen",
|
||||||
|
},
|
||||||
|
tags: ["autodocs"],
|
||||||
|
argTypes: {
|
||||||
|
variant: {
|
||||||
|
control: "select",
|
||||||
|
options: ["success", "error", "warning", "info"],
|
||||||
|
description: "Visual variant of the banner",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
control: "text",
|
||||||
|
description: "Banner message text",
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
control: false,
|
||||||
|
description: "Icon element to display before the title",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
control: false,
|
||||||
|
description: "Action element(s) to display on the right side",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Banner>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
// Sample icons for different variants
|
||||||
|
const SuccessIcon = () => (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="text-green-600"
|
||||||
|
>
|
||||||
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||||
|
<polyline points="22 4 12 14.01 9 11.01" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ErrorIcon = () => (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="text-red-600"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<line x1="15" y1="9" x2="9" y2="15" />
|
||||||
|
<line x1="9" y1="9" x2="15" y2="15" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const WarningIcon = () => (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="text-yellow-600"
|
||||||
|
>
|
||||||
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
||||||
|
<line x1="12" y1="9" x2="12" y2="13" />
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const InfoIcon = () => (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="text-blue-600"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<line x1="12" y1="16" x2="12" y2="12" />
|
||||||
|
<line x1="12" y1="8" x2="12.01" y2="8" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CloseButton = ({ onClick }: { onClick?: () => void }) => (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className="rounded p-1 hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
|
||||||
|
aria-label="Dismiss"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="text-text-secondary"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Interactive Stories
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const Interactive: Story = {
|
||||||
|
args: {
|
||||||
|
variant: "info",
|
||||||
|
title: "This is an interactive banner. Use the controls to customize it.",
|
||||||
|
icon: <InfoIcon />,
|
||||||
|
dismissible: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Main Variants
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const Success: Story = {
|
||||||
|
args: {
|
||||||
|
variant: "success",
|
||||||
|
title: "Operation completed successfully",
|
||||||
|
icon: <SuccessIcon />,
|
||||||
|
action: <CloseButton />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Error: Story = {
|
||||||
|
args: {
|
||||||
|
variant: "error",
|
||||||
|
title: "An error occurred while processing your request",
|
||||||
|
icon: <ErrorIcon />,
|
||||||
|
action: <CloseButton />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Warning: Story = {
|
||||||
|
args: {
|
||||||
|
variant: "warning",
|
||||||
|
title: "Your session will expire in 5 minutes",
|
||||||
|
icon: <WarningIcon />,
|
||||||
|
action: <CloseButton />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Info: Story = {
|
||||||
|
args: {
|
||||||
|
variant: "info",
|
||||||
|
title: "New features are available. Check out what's new!",
|
||||||
|
icon: <InfoIcon />,
|
||||||
|
action: <CloseButton />,
|
||||||
|
},
|
||||||
|
};
|
||||||
131
packages/propel/src/banner/banner.tsx
Normal file
131
packages/propel/src/banner/banner.tsx
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
import React from "react";
|
||||||
|
import { cn } from "../utils";
|
||||||
|
import {
|
||||||
|
TBannerVariant,
|
||||||
|
getBannerStyling,
|
||||||
|
getBannerTitleStyling,
|
||||||
|
getBannerActionStyling,
|
||||||
|
getBannerDismissStyling,
|
||||||
|
getBannerDismissIconStyling,
|
||||||
|
} from "./helper";
|
||||||
|
|
||||||
|
export interface BannerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
||||||
|
/** Visual variant of the banner */
|
||||||
|
variant?: TBannerVariant;
|
||||||
|
/** Icon to display before the title */
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
/** Banner title/message */
|
||||||
|
title?: React.ReactNode;
|
||||||
|
/** Action elements to display on the right side */
|
||||||
|
action?: React.ReactNode;
|
||||||
|
/** Whether the banner can be dismissed */
|
||||||
|
dismissible?: boolean;
|
||||||
|
/** Callback when banner is dismissed */
|
||||||
|
onDismiss?: () => void;
|
||||||
|
/** Whether to show the banner */
|
||||||
|
visible?: boolean;
|
||||||
|
/** Animation duration for show/hide */
|
||||||
|
animationDuration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Banner = React.forwardRef<HTMLDivElement, BannerProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
action,
|
||||||
|
variant = "info",
|
||||||
|
dismissible = false,
|
||||||
|
onDismiss,
|
||||||
|
visible = true,
|
||||||
|
animationDuration = 200,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
// Handle dismissal
|
||||||
|
const handleDismiss = () => {
|
||||||
|
if (onDismiss) {
|
||||||
|
onDismiss();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Don't render if not visible
|
||||||
|
if (!visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get styling using helper functions
|
||||||
|
const containerStyling = getBannerStyling(variant);
|
||||||
|
const iconStyling = "flex items-center justify-center flex-shrink-0 size-5";
|
||||||
|
const titleStyling = getBannerTitleStyling();
|
||||||
|
const actionStyling = getBannerActionStyling();
|
||||||
|
const dismissStyling = getBannerDismissStyling();
|
||||||
|
const dismissIconStyling = getBannerDismissIconStyling();
|
||||||
|
|
||||||
|
// Render custom icon component if provided
|
||||||
|
const renderIcon = () => {
|
||||||
|
if (icon) {
|
||||||
|
return <div className={cn(iconStyling)}>{icon}</div>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render dismiss button if dismissible
|
||||||
|
const renderDismissButton = () => {
|
||||||
|
if (!dismissible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={handleDismiss} className={cn(dismissStyling)} aria-label="Dismiss banner">
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className={cn(dismissIconStyling)}
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(containerStyling, className)}
|
||||||
|
style={{
|
||||||
|
transitionDuration: `${animationDuration}ms`,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{/* Left side: Icon and Title */}
|
||||||
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
|
{renderIcon()}
|
||||||
|
{title && <div className={cn(titleStyling)}>{title}</div>}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right side: Actions */}
|
||||||
|
{(action || dismissible) && (
|
||||||
|
<div className={cn(actionStyling)}>
|
||||||
|
{action}
|
||||||
|
{renderDismissButton()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Banner.displayName = "Banner";
|
||||||
|
|
||||||
|
// Export variant types for external use
|
||||||
|
export type BannerVariant = TBannerVariant;
|
||||||
46
packages/propel/src/banner/helper.tsx
Normal file
46
packages/propel/src/banner/helper.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
export type TBannerVariant = "success" | "error" | "warning" | "info";
|
||||||
|
|
||||||
|
export interface IBannerStyling {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bannerSizeStyling = {
|
||||||
|
container: "py-3 px-6 h-12",
|
||||||
|
icon: "w-5 h-5",
|
||||||
|
title: "text-sm",
|
||||||
|
action: "gap-2",
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: update this with new color once its implemented
|
||||||
|
// Banner variant styling
|
||||||
|
export const bannerStyling: IBannerStyling = {
|
||||||
|
success: "bg-green-500/10",
|
||||||
|
error: "bg-red-500/10",
|
||||||
|
warning: "bg-yellow-500/10",
|
||||||
|
info: "bg-blue-500/10",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base banner styles
|
||||||
|
export const bannerBaseStyles = "flex items-center justify-between w-full transition-all duration-200";
|
||||||
|
|
||||||
|
// Get banner container styling
|
||||||
|
export const getBannerStyling = (variant: TBannerVariant): string => {
|
||||||
|
const variantStyles = bannerStyling[variant];
|
||||||
|
const sizeStyles = bannerSizeStyling.container;
|
||||||
|
|
||||||
|
return `${bannerBaseStyles} ${variantStyles} ${sizeStyles}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get title styling
|
||||||
|
export const getBannerTitleStyling = (): string =>
|
||||||
|
`font-medium text-custom-text-200 flex-1 min-w-0 ${bannerSizeStyling.title}`;
|
||||||
|
|
||||||
|
// Get action container styling
|
||||||
|
export const getBannerActionStyling = (): string => `flex items-center flex-shrink-0 ${bannerSizeStyling.action}`;
|
||||||
|
|
||||||
|
// Get dismiss button styling
|
||||||
|
export const getBannerDismissStyling = (): string =>
|
||||||
|
"rounded p-1 hover:bg-custom-background-90 transition-colors flex-shrink-0";
|
||||||
|
|
||||||
|
// Get dismiss icon styling
|
||||||
|
export const getBannerDismissIconStyling = (): string => "text-custom-text-200";
|
||||||
3
packages/propel/src/banner/index.ts
Normal file
3
packages/propel/src/banner/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { Banner } from "./banner";
|
||||||
|
export type { BannerProps, BannerVariant } from "./banner";
|
||||||
|
export type { TBannerVariant } from "./helper";
|
||||||
|
|
@ -5,6 +5,7 @@ export default defineConfig({
|
||||||
"src/accordion/index.ts",
|
"src/accordion/index.ts",
|
||||||
"src/animated-counter/index.ts",
|
"src/animated-counter/index.ts",
|
||||||
"src/avatar/index.ts",
|
"src/avatar/index.ts",
|
||||||
|
"src/banner/index.ts",
|
||||||
"src/button/index.ts",
|
"src/button/index.ts",
|
||||||
"src/calendar/index.ts",
|
"src/calendar/index.ts",
|
||||||
"src/card/index.ts",
|
"src/card/index.ts",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue