mirror of
https://github.com/makeplane/plane.git
synced 2026-01-30 18:29:30 -06:00
[WEB-5160] chore: propel banner and archived work item improvements (#7999)
This commit is contained in:
committed by
GitHub
parent
68fd2463f4
commit
33b6405695
@@ -1,9 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// ui
|
||||
import { Banner } from "@plane/propel/banner";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { ArchiveIcon } from "@plane/propel/icons";
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
@@ -16,6 +19,7 @@ import { useProject } from "@/hooks/store/use-project";
|
||||
const ArchivedIssueDetailsPage = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId, archivedIssueId } = useParams();
|
||||
const router = useRouter();
|
||||
// states
|
||||
// hooks
|
||||
const {
|
||||
@@ -62,18 +66,35 @@ const ArchivedIssueDetailsPage = observer(() => {
|
||||
</div>
|
||||
</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">
|
||||
{workspaceSlug && projectId && archivedIssueId && (
|
||||
<IssueDetailRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
issueId={archivedIssueId.toString()}
|
||||
is_archived
|
||||
/>
|
||||
)}
|
||||
<>
|
||||
<Banner
|
||||
variant="warning"
|
||||
title="This work item has been archived. Visit the Archives section to restore it."
|
||||
icon={<ArchiveIcon className="size-4" />}
|
||||
action={
|
||||
<Button
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
"import": "./dist/avatar/index.mjs",
|
||||
"require": "./dist/avatar/index.js"
|
||||
},
|
||||
"./banner": {
|
||||
"import": "./dist/banner/index.mjs",
|
||||
"require": "./dist/banner/index.js"
|
||||
},
|
||||
"./button": {
|
||||
"import": "./dist/button/index.mjs",
|
||||
"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/animated-counter/index.ts",
|
||||
"src/avatar/index.ts",
|
||||
"src/banner/index.ts",
|
||||
"src/button/index.ts",
|
||||
"src/calendar/index.ts",
|
||||
"src/card/index.ts",
|
||||
|
||||
Reference in New Issue
Block a user