Add Activepieces integration for workflow automation
- Add Activepieces fork with SmoothSchedule custom piece - Create integrations app with Activepieces service layer - Add embed token endpoint for iframe integration - Create Automations page with embedded workflow builder - Add sidebar visibility fix for embed mode - Add list inactive customers endpoint to Public API - Include SmoothSchedule triggers: event created/updated/cancelled - Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export const InvalidStepIcon = ({ className }: { className: string }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={cn(
|
||||
'stroke-1 dark:stroke-amber-900 stroke-amber-500 ',
|
||||
className,
|
||||
)}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle
|
||||
cx="8.08325"
|
||||
cy="8.26868"
|
||||
r="7.5"
|
||||
className="dark:fill-amber-950 fill-amber-50 "
|
||||
></circle>
|
||||
<path
|
||||
d="M8.08325 3.69368C7.86445 3.69368 7.65461 3.7806 7.49989 3.93531C7.34517 4.09003 7.25825 4.29987 7.25825 4.51868V8.51868C7.25825 8.73748 7.34517 8.94732 7.49989 9.10204C7.65461 9.25676 7.86445 9.34368 8.08325 9.34368C8.30206 9.34368 8.5119 9.25676 8.66662 9.10204C8.82133 8.94732 8.90825 8.73748 8.90825 8.51868V4.51868C8.90825 4.29987 8.82133 4.09003 8.66662 3.93531C8.5119 3.7806 8.30206 3.69368 8.08325 3.69368ZM8.90825 12.0187C8.90825 11.563 8.53889 11.1937 8.08325 11.1937C7.62762 11.1937 7.25825 11.563 7.25825 12.0187C7.25825 12.4743 7.62762 12.8437 8.08325 12.8437C8.53889 12.8437 8.90825 12.4743 8.90825 12.0187Z"
|
||||
className="dark:fill-amber-600 fill-amber-700"
|
||||
strokeWidth="0.4"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { AvatarImage } from '@radix-ui/react-avatar';
|
||||
import { Workflow } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
|
||||
import { UserAvatar } from '../ui/user-avatar';
|
||||
|
||||
interface ApAvatarProps {
|
||||
type: 'agent' | 'user' | 'flow';
|
||||
fullName: string;
|
||||
userEmail?: string;
|
||||
pictureUrl?: string;
|
||||
profileUrl?: string;
|
||||
size: 'small' | 'medium';
|
||||
includeName?: boolean;
|
||||
}
|
||||
|
||||
export const ApAvatar = ({
|
||||
type,
|
||||
fullName,
|
||||
userEmail,
|
||||
pictureUrl,
|
||||
profileUrl,
|
||||
includeName = false,
|
||||
size = 'medium',
|
||||
}: ApAvatarProps) => {
|
||||
const renderAvatar = () => {
|
||||
if (type === 'agent') {
|
||||
return (
|
||||
<Avatar className={size === 'small' ? 'w-6 h-6' : 'w-8 h-8'}>
|
||||
<AvatarImage
|
||||
src={pictureUrl}
|
||||
alt={fullName}
|
||||
className={`${size} rounded-full`}
|
||||
/>
|
||||
</Avatar>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'user') {
|
||||
return (
|
||||
<UserAvatar
|
||||
name={fullName}
|
||||
email={userEmail!}
|
||||
size={size === 'small' ? 24 : 32}
|
||||
disableTooltip={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Avatar className={size === 'small' ? 'w-6 h-6' : 'w-8 h-8'}>
|
||||
<AvatarFallback
|
||||
className={`text-xs font-bold border ${
|
||||
size === 'small' ? 'w-6 h-6' : 'w-8 h-8'
|
||||
}`}
|
||||
>
|
||||
<Workflow className="p-1" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
};
|
||||
|
||||
const content = (
|
||||
<div className="flex items-center gap-2">
|
||||
{renderAvatar()}
|
||||
{includeName && <span className="text-sm">{fullName}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (type === 'agent' && profileUrl) {
|
||||
return <Link to={profileUrl}>{content}</Link>;
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
type ApErrorDialogParams = {
|
||||
title: string;
|
||||
description: React.ReactNode;
|
||||
error: unknown;
|
||||
};
|
||||
interface ApErrorDialogStore {
|
||||
params: ApErrorDialogParams | null;
|
||||
openDialog: (params: ApErrorDialogParams) => void;
|
||||
closeDialog: () => void;
|
||||
}
|
||||
|
||||
export const useApErrorDialogStore = create<ApErrorDialogStore>((set) => ({
|
||||
params: null,
|
||||
openDialog: (params) => set({ params }),
|
||||
closeDialog: () => set({ params: null }),
|
||||
}));
|
||||
@@ -0,0 +1,66 @@
|
||||
import { t } from 'i18next';
|
||||
import { AlertCircleIcon } from 'lucide-react';
|
||||
|
||||
import { CollapsibleJson } from '@/components/custom/collapsible-json';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { isNil } from '@activepieces/shared';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from '../../ui/dialog';
|
||||
|
||||
import { useApErrorDialogStore } from './ap-error-dialog-store';
|
||||
|
||||
const ApErrorDialog = () => {
|
||||
const { params, closeDialog } = useApErrorDialogStore();
|
||||
|
||||
if (isNil(params)) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={!!params} onOpenChange={closeDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<div className="flex flex-col items-center">
|
||||
<span
|
||||
className="rounded-full bg-red-100 flex items-center justify-center mb-2 mt-1"
|
||||
style={{ width: 48, height: 48 }}
|
||||
>
|
||||
<AlertCircleIcon className="h-8 w-8 text-red-500" />
|
||||
</span>
|
||||
<div className="flex flex-col items-center text-center w-full gap-2">
|
||||
<DialogTitle className="text-lg font-semibold">
|
||||
{params?.title}
|
||||
</DialogTitle>
|
||||
{params?.description && (
|
||||
<DialogDescription className="mt-0.5 text-sm text-muted-foreground">
|
||||
{params.description}
|
||||
</DialogDescription>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="w-full flex flex-col items-stretch mt-2">
|
||||
<CollapsibleJson
|
||||
json={params?.error}
|
||||
label={t('Technical Details')}
|
||||
defaultOpen={true}
|
||||
className="w-full text-left"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="mt-2">
|
||||
<Button variant="outline" onClick={closeDialog}>
|
||||
{t('Close')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
ApErrorDialog.displayName = 'ApErrorDialog';
|
||||
export { ApErrorDialog };
|
||||
@@ -0,0 +1,26 @@
|
||||
import { t } from 'i18next';
|
||||
import { PanelRightClose, PanelRightOpen } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useSidebar } from '@/components/ui/sidebar-shadcn';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
export const ApSidebarToggle = () => {
|
||||
const { open, setOpen } = useSidebar();
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button variant="ghost" size="icon" onClick={() => setOpen(!open)}>
|
||||
{open ? <PanelRightOpen /> : <PanelRightClose />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{open ? t('Close Sidebar') : t('Open Sidebar')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,202 @@
|
||||
import { DragHandleDots2Icon } from '@radix-ui/react-icons';
|
||||
import { t } from 'i18next';
|
||||
import { Plus, TrashIcon } from 'lucide-react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import React, { useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { FormControl, FormField, FormItem } from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Sortable,
|
||||
SortableDragHandle,
|
||||
SortableItem,
|
||||
} from '@/components/ui/sortable';
|
||||
import { TextWithIcon } from '@/components/ui/text-with-icon';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type ArrayInputProps = {
|
||||
inputName: string;
|
||||
disabled: boolean;
|
||||
required?: boolean;
|
||||
customInputNode?: (
|
||||
onChange: (value: string) => void,
|
||||
value: string,
|
||||
disabled: boolean,
|
||||
) => React.ReactNode;
|
||||
thinInputs?: boolean;
|
||||
};
|
||||
|
||||
type ArrayField = {
|
||||
id: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const ArrayInput = React.memo(
|
||||
({
|
||||
inputName,
|
||||
disabled,
|
||||
required,
|
||||
customInputNode,
|
||||
thinInputs,
|
||||
}: ArrayInputProps) => {
|
||||
const form = useFormContext();
|
||||
const [fields, setFields] = useState<ArrayField[]>(() => {
|
||||
const formValues = form.getValues(inputName);
|
||||
if (formValues) {
|
||||
return formValues.map((value: string) => ({
|
||||
id: nanoid(),
|
||||
value,
|
||||
}));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const updateFormValue = (newFields: ArrayField[]) => {
|
||||
form.setValue(
|
||||
inputName,
|
||||
newFields.map((f) => f.value),
|
||||
{ shouldValidate: true },
|
||||
);
|
||||
};
|
||||
|
||||
const append = () => {
|
||||
const formValues = form.getValues(inputName) || [];
|
||||
const newFields = [
|
||||
...formValues.map((value: string) => ({
|
||||
id: nanoid(),
|
||||
value,
|
||||
})),
|
||||
{ id: nanoid(), value: '' },
|
||||
];
|
||||
|
||||
setFields(newFields);
|
||||
updateFormValue(newFields);
|
||||
};
|
||||
|
||||
const remove = (index: number) => {
|
||||
const currentFields: ArrayField[] = form
|
||||
.getValues(inputName)
|
||||
.map((value: string) => ({
|
||||
id: nanoid(),
|
||||
value,
|
||||
}));
|
||||
const newFields = currentFields.filter((_, i) => i !== index);
|
||||
setFields(newFields);
|
||||
updateFormValue(newFields);
|
||||
};
|
||||
|
||||
const move = (from: number, to: number) => {
|
||||
const newFields = [...fields];
|
||||
const [removed] = newFields.splice(from, 1);
|
||||
newFields.splice(to, 0, removed);
|
||||
setFields(newFields);
|
||||
updateFormValue(newFields);
|
||||
};
|
||||
|
||||
const updateFieldValue = (index: number, newValue: string) => {
|
||||
const newFields = fields.map((field, i) =>
|
||||
i === index ? { ...field, value: newValue } : field,
|
||||
);
|
||||
setFields(newFields);
|
||||
updateFormValue(newFields);
|
||||
};
|
||||
const showRemoveButton = !required || fields.length > 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full flex-col gap-4 ">
|
||||
<Sortable
|
||||
value={fields}
|
||||
onMove={({ activeIndex, overIndex }) => {
|
||||
move(activeIndex, overIndex);
|
||||
}}
|
||||
>
|
||||
{fields.map((field, index) => (
|
||||
<SortableItem key={field.id} value={field.id} asChild>
|
||||
<div className="flex items-center gap-3">
|
||||
<SortableDragHandle
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={disabled}
|
||||
className={cn('shrink-0 size-8', thinInputs && 'size-7')}
|
||||
>
|
||||
<DragHandleDots2Icon
|
||||
className="size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</SortableDragHandle>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`${inputName}.${index}`}
|
||||
render={() => (
|
||||
<FormItem className="grow">
|
||||
<FormControl>
|
||||
{customInputNode ? (
|
||||
customInputNode(
|
||||
(value) => updateFieldValue(index, value),
|
||||
field.value as string,
|
||||
disabled,
|
||||
)
|
||||
) : (
|
||||
<Input
|
||||
thin={thinInputs}
|
||||
value={field.value as string}
|
||||
onChange={(e) =>
|
||||
updateFieldValue(index, e.target.value)
|
||||
}
|
||||
disabled={disabled}
|
||||
className="grow"
|
||||
/>
|
||||
)}
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{showRemoveButton && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={disabled}
|
||||
className={cn('shrink-0 size-8', thinInputs && 'size-7')}
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
}}
|
||||
>
|
||||
<TrashIcon
|
||||
className="size-4 text-destructive"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="sr-only">{t('Remove')}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</SortableItem>
|
||||
))}
|
||||
</Sortable>
|
||||
</div>
|
||||
{!disabled && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-3"
|
||||
onClick={() => {
|
||||
append();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<TextWithIcon icon={<Plus size={18} />} text={t('Add Item')} />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ArrayInput.displayName = 'ArrayInput';
|
||||
export { ArrayInput };
|
||||
@@ -0,0 +1,55 @@
|
||||
import { PermissionNeededTooltip } from '@/components/custom/permission-needed-tooltip';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
type ButtonWithTooltipProps = {
|
||||
tooltip: string;
|
||||
onClick: (e?: React.MouseEvent) => void;
|
||||
variant?:
|
||||
| 'ghost'
|
||||
| 'outline'
|
||||
| 'default'
|
||||
| 'destructive'
|
||||
| 'secondary'
|
||||
| 'link';
|
||||
icon: React.ReactNode;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
hasPermission?: boolean;
|
||||
};
|
||||
|
||||
export const ButtonWithTooltip = ({
|
||||
tooltip,
|
||||
onClick,
|
||||
variant = 'ghost',
|
||||
icon,
|
||||
className = 'h-7 w-7',
|
||||
disabled = false,
|
||||
hasPermission = true,
|
||||
}: ButtonWithTooltipProps) => (
|
||||
<PermissionNeededTooltip hasPermission={hasPermission}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={variant}
|
||||
size="icon"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
disabled={disabled || !hasPermission}
|
||||
>
|
||||
{icon}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</PermissionNeededTooltip>
|
||||
);
|
||||
@@ -0,0 +1,113 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { PackageOpen } from 'lucide-react';
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { Skeleton } from '../ui/skeleton';
|
||||
|
||||
const CardList = forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & { listClassName?: string }
|
||||
>(({ children, className, listClassName, ...props }, ref) => (
|
||||
<ScrollArea
|
||||
className={`h-full overflow-auto ${className}`}
|
||||
viewPortClassName="[&>div]:h-full"
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col h-full w-full', listClassName)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
));
|
||||
CardList.displayName = 'CardList';
|
||||
export { CardList };
|
||||
|
||||
const cardItemListVariants = cva('flex items-center gap-4 w-full py-4 px-2 ', {
|
||||
variants: {
|
||||
interactive: {
|
||||
true: 'cursor-pointer transition-all hover:bg-accent hover:text-accent-foreground',
|
||||
false: 'cursor-default text-accent-foreground/50 font-semibold',
|
||||
},
|
||||
selected: {
|
||||
true: 'bg-accent text-accent-foreground',
|
||||
false: '',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
interactive: true,
|
||||
selected: false,
|
||||
},
|
||||
});
|
||||
|
||||
type CardListItemProps = React.HTMLAttributes<HTMLDivElement> &
|
||||
VariantProps<typeof cardItemListVariants> & {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const CardListItem = React.forwardRef<HTMLDivElement, CardListItemProps>(
|
||||
({ children, onClick, className, interactive, selected, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
cardItemListVariants({ interactive, selected }),
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CardListItem.displayName = 'CardListItem';
|
||||
export { CardListItem };
|
||||
|
||||
type CardListItemSkeletonProps = {
|
||||
numberOfCards?: number;
|
||||
withCircle?: boolean;
|
||||
};
|
||||
|
||||
const CardListItemSkeleton: React.FC<CardListItemSkeletonProps> = React.memo(
|
||||
({ numberOfCards = 3, withCircle = true }) => {
|
||||
return (
|
||||
<>
|
||||
{[...Array(numberOfCards)].map((_, index) => (
|
||||
<div key={index} className="flex items-center gap-4 w-full py-4 px-5">
|
||||
{withCircle && <Skeleton className="h-8 w-8 rounded-full" />}
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-[250px]" />
|
||||
<Skeleton className="h-4 w-[200px]" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CardListItemSkeleton.displayName = 'CardListItemSkeleton';
|
||||
export { CardListItemSkeleton };
|
||||
|
||||
type CardListEmptyProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
message: string;
|
||||
};
|
||||
const CardListEmpty = React.memo(({ message }: CardListEmptyProps) => {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center gap-3 flex-col text-muted-foreground">
|
||||
<PackageOpen className="w-10 h-10" />
|
||||
<div className="text-center tracking-tight">{message}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
CardListEmpty.displayName = 'CardListEmpty';
|
||||
export { CardListEmpty };
|
||||
@@ -0,0 +1,65 @@
|
||||
interface Props {
|
||||
value: number;
|
||||
size?: number;
|
||||
strokeWidth?: number;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const CircularIcon: React.FC<Props> = ({
|
||||
value,
|
||||
size = 50,
|
||||
strokeWidth = 3.5,
|
||||
label,
|
||||
}) => {
|
||||
const radius = (size - strokeWidth) / 2;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const offset = circumference - (value / 100) * circumference;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Progress Circle */}
|
||||
<svg width={size} height={size} className="inline-block">
|
||||
{/* Background Circle */}
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
className="text-gray-200 dark:text-gray-700"
|
||||
fill="transparent"
|
||||
/>
|
||||
{/* Progress Circle */}
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
className="text-primary"
|
||||
fill="transparent"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={offset}
|
||||
transform={`rotate(-90 ${size / 2} ${size / 2})`}
|
||||
/>
|
||||
{/* Percentage Label */}
|
||||
<text
|
||||
x="50%"
|
||||
y="50%"
|
||||
dominantBaseline="middle"
|
||||
textAnchor="middle"
|
||||
fontSize={size * 0.225}
|
||||
fontWeight="bold"
|
||||
className="fill-current text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
{value.toFixed(1)}%
|
||||
</text>
|
||||
</svg>
|
||||
|
||||
{/* Label */}
|
||||
{label && (
|
||||
<div className="text-sm text-gray-700 dark:text-gray-400">{label}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { PolarGrid, RadialBar, RadialBarChart } from 'recharts';
|
||||
|
||||
import { ChartContainer } from '../ui/chart';
|
||||
|
||||
const ProgressCircularComponent: React.FC<{
|
||||
data: {
|
||||
plan: number;
|
||||
usage: number;
|
||||
};
|
||||
size?: 'big' | 'small';
|
||||
}> = ({ data, size = 'big' }) => {
|
||||
const sizeClass = size === 'big' ? 'size-[40px]' : 'size-[25px]';
|
||||
return (
|
||||
<div className={`overflow-hidden ${sizeClass}`}>
|
||||
<ChartContainer
|
||||
config={{}}
|
||||
className={`mx-auto aspect-square max-h-[250px] min-h-[180px] ${sizeClass}`}
|
||||
>
|
||||
<RadialBarChart
|
||||
data={[
|
||||
{
|
||||
name: 'plan',
|
||||
progress: data.usage,
|
||||
fill: 'hsl(var(--primary))',
|
||||
},
|
||||
]}
|
||||
startAngle={0}
|
||||
endAngle={(data.usage / data.plan) * 360}
|
||||
innerRadius={80}
|
||||
outerRadius={110}
|
||||
style={{
|
||||
height: size === 'big' ? 40 : 25,
|
||||
}}
|
||||
>
|
||||
<PolarGrid
|
||||
gridType="circle"
|
||||
radialLines={false}
|
||||
stroke="none"
|
||||
className="first:fill-muted last:fill-background"
|
||||
polarRadius={[86, 74]}
|
||||
/>
|
||||
<RadialBar dataKey="progress" background cornerRadius={10} />
|
||||
</RadialBarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ProgressCircularComponent };
|
||||
@@ -0,0 +1,82 @@
|
||||
import { TooltipContentProps } from '@radix-ui/react-tooltip';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { t } from 'i18next';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { Button, ButtonProps } from '@/components/ui/button';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
interface CopyButtonProps extends ButtonProps {
|
||||
textToCopy: string;
|
||||
tooltipSide?: TooltipContentProps['side'];
|
||||
withoutTooltip?: boolean;
|
||||
}
|
||||
|
||||
export const CopyButton = ({
|
||||
textToCopy,
|
||||
className,
|
||||
tooltipSide,
|
||||
withoutTooltip = false,
|
||||
...props
|
||||
}: CopyButtonProps) => {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const { mutate: copyToClipboard } = useMutation({
|
||||
mutationFn: async () => {
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 1500);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t('Failed to copy to clipboard'), {
|
||||
duration: 3000,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (withoutTooltip) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
type="button"
|
||||
className={className}
|
||||
onClick={() => copyToClipboard()}
|
||||
{...props}
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
type="button"
|
||||
className={className}
|
||||
onClick={() => copyToClipboard()}
|
||||
{...props}
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={tooltipSide}>{t('Copy')}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip';
|
||||
|
||||
import { CopyButton } from './copy-button';
|
||||
|
||||
const CopyTextTooltip = ({
|
||||
text,
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
text: string;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="flex text-xs gap-2 items-center">
|
||||
{title}: {text || '-'}{' '}
|
||||
<CopyButton
|
||||
withoutTooltip={true}
|
||||
variant="ghost"
|
||||
textToCopy={text || ''}
|
||||
></CopyButton>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
CopyTextTooltip.displayName = 'CopyTextTooltip';
|
||||
export { CopyTextTooltip };
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { DownloadButton } from '../download-button';
|
||||
|
||||
import { CopyButton } from './copy-button';
|
||||
|
||||
type CopyToClipboardInputProps = {
|
||||
textToCopy: string;
|
||||
useInput: boolean;
|
||||
fileName?: string;
|
||||
};
|
||||
|
||||
const noBorderInputClass = `border-none w-full rfocus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0`;
|
||||
|
||||
const CopyToClipboardInput = ({
|
||||
textToCopy,
|
||||
fileName,
|
||||
useInput,
|
||||
}: CopyToClipboardInputProps) => {
|
||||
return (
|
||||
<div className="flex gap-2 items-center bg-background border border-solid text-sm rounded block w-full select-none pr-3">
|
||||
{useInput ? (
|
||||
<Input value={textToCopy} className={noBorderInputClass} readOnly />
|
||||
) : (
|
||||
<Textarea
|
||||
value={textToCopy}
|
||||
rows={6}
|
||||
className={noBorderInputClass}
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={cn('flex gap-1', {
|
||||
'flex-col': !useInput,
|
||||
})}
|
||||
>
|
||||
<CopyButton textToCopy={textToCopy} variant="ghost" />
|
||||
{fileName && (
|
||||
<DownloadButton
|
||||
textToDownload={textToCopy}
|
||||
fileName={fileName}
|
||||
variant="ghost"
|
||||
tooltipSide="bottom"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CopyToClipboardInput.displayName = 'CopyToClipboardInput';
|
||||
export { CopyToClipboardInput };
|
||||
@@ -0,0 +1,58 @@
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { CopyButton } from '@/components/custom/clipboard/copy-button';
|
||||
|
||||
export function CollapsibleJson({
|
||||
json,
|
||||
label,
|
||||
description,
|
||||
defaultOpen = false,
|
||||
className = '',
|
||||
}: CollapsibleJsonProps) {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const toggleVisibility = () => setIsOpen(!isOpen);
|
||||
|
||||
const jsonString =
|
||||
typeof json === 'string' ? json : JSON.stringify(json, null, 2);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-2 ${className}`}>
|
||||
<button
|
||||
onClick={toggleVisibility}
|
||||
className="flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{isOpen ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
{label}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="flex flex-col gap-2 min-w-0">
|
||||
<div className="relative min-w-0">
|
||||
<pre className="bg-muted/50 whitespace-pre-wrap break-all rounded-md px-4 py-4 text-xs overflow-x-auto max-w-full">
|
||||
<code>{jsonString}</code>
|
||||
</pre>
|
||||
<div className="absolute top-2 right-2">
|
||||
<CopyButton textToCopy={jsonString} />
|
||||
</div>
|
||||
</div>
|
||||
{description && (
|
||||
<p className="text-xs text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type CollapsibleJsonProps = {
|
||||
json: unknown;
|
||||
label: string;
|
||||
description?: string;
|
||||
defaultOpen?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { TooltipContentProps } from '@radix-ui/react-tooltip';
|
||||
import { t } from 'i18next';
|
||||
import { Download } from 'lucide-react';
|
||||
|
||||
import { Button, ButtonProps } from '@/components/ui/button';
|
||||
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '../ui/tooltip';
|
||||
|
||||
interface DownloadButtonProps extends ButtonProps {
|
||||
fileName: string;
|
||||
textToDownload: string;
|
||||
tooltipSide?: TooltipContentProps['side'];
|
||||
}
|
||||
|
||||
export const DownloadButton = ({
|
||||
fileName,
|
||||
className,
|
||||
textToDownload,
|
||||
tooltipSide,
|
||||
...props
|
||||
}: DownloadButtonProps) => {
|
||||
const downloadFile = () => {
|
||||
const blob = new Blob([textToDownload], {
|
||||
type: 'text/plain',
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${fileName}.txt`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={className}
|
||||
onClick={() => downloadFile()}
|
||||
{...props}
|
||||
>
|
||||
<Download className="h-4 w-4"></Download>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={tooltipSide}>{t('Download')}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const inputClass =
|
||||
'grow flex h-10 w-full rounded-sm border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-within:outline-hidden focus-within:ring-1 focus-within:ring-ring focus-within:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 box-border';
|
||||
|
||||
const InputWithIcon = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className={cn(inputClass, className, 'items-center gap-2')}>
|
||||
{props.icon}
|
||||
<input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-full w-full rounded-md bg-transparent text-sm outline-hidden placeholder:text-muted-foreground',
|
||||
{ 'cursor-not-allowed opacity-50': props.disabled },
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
InputWithIcon.displayName = 'InputWithIcon';
|
||||
|
||||
export { InputWithIcon };
|
||||
@@ -0,0 +1,90 @@
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { githubDark, githubLight } from '@uiw/codemirror-theme-github';
|
||||
import CodeMirror, {
|
||||
EditorState,
|
||||
EditorView,
|
||||
ReactCodeMirrorRef,
|
||||
} from '@uiw/react-codemirror';
|
||||
import React, { RefObject, useRef, useState } from 'react';
|
||||
import { ControllerRenderProps } from 'react-hook-form';
|
||||
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const styleTheme = EditorView.baseTheme({
|
||||
'&.cm-editor.cm-focused': {
|
||||
outline: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
const convertToString = (value: unknown): string => {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
return JSON.stringify(value, null, 2);
|
||||
};
|
||||
|
||||
const tryParseJson = (value: unknown): unknown => {
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
type JsonEditorProps = {
|
||||
field: ControllerRenderProps<Record<string, any>, string>;
|
||||
readonly: boolean;
|
||||
onFocus?: (ref: RefObject<ReactCodeMirrorRef>) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const JsonEditor = React.memo(
|
||||
({ field, readonly, onFocus, className }: JsonEditorProps) => {
|
||||
const [value, setValue] = useState(convertToString(field.value));
|
||||
const { theme } = useTheme();
|
||||
const editorTheme = theme === 'dark' ? githubDark : githubLight;
|
||||
const extensions = [
|
||||
styleTheme,
|
||||
EditorState.readOnly.of(readonly),
|
||||
EditorView.editable.of(!readonly),
|
||||
EditorView.lineWrapping,
|
||||
json(),
|
||||
];
|
||||
const ref = useRef<ReactCodeMirrorRef>(null);
|
||||
return (
|
||||
<div className="flex flex-col gap-2 border rounded py-2 px-2">
|
||||
<CodeMirror
|
||||
ref={ref}
|
||||
value={value}
|
||||
className={cn('border-none', className)}
|
||||
height="250px"
|
||||
width="100%"
|
||||
maxWidth="100%"
|
||||
basicSetup={{
|
||||
foldGutter: false,
|
||||
lineNumbers: true,
|
||||
searchKeymap: false,
|
||||
lintKeymap: true,
|
||||
autocompletion: true,
|
||||
}}
|
||||
lang="json"
|
||||
onChange={(value) => {
|
||||
setValue(value);
|
||||
field.onChange(tryParseJson(value));
|
||||
}}
|
||||
theme={editorTheme}
|
||||
readOnly={readonly}
|
||||
onFocus={() => onFocus?.(ref)}
|
||||
extensions={extensions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
JsonEditor.displayName = 'JsonEditor';
|
||||
export { JsonEditor };
|
||||
@@ -0,0 +1,209 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { t } from 'i18next';
|
||||
import { Check, Copy, Info, AlertTriangle, Lightbulb } from 'lucide-react';
|
||||
import React, { useState } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import breaks from 'remark-breaks';
|
||||
import gfm from 'remark-gfm';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { MarkdownVariant } from '@activepieces/shared';
|
||||
|
||||
import { Alert, AlertDescription } from '../ui/alert';
|
||||
import { Button } from '../ui/button';
|
||||
|
||||
function applyVariables(markdown: string, variables: Record<string, string>) {
|
||||
if (typeof markdown !== 'string') {
|
||||
return '';
|
||||
}
|
||||
let result = markdown.split('<br>').join('\n');
|
||||
result = result.replace(/\{\{(.*?)\}\}/g, (_, variableName) => {
|
||||
return variables[variableName] ?? '';
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
type MarkdownProps = {
|
||||
markdown: string | undefined;
|
||||
variables?: Record<string, string>;
|
||||
variant?: MarkdownVariant;
|
||||
className?: string;
|
||||
loading?: string;
|
||||
};
|
||||
|
||||
const Container = ({
|
||||
variant,
|
||||
children,
|
||||
}: {
|
||||
variant?: MarkdownVariant;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<Alert
|
||||
className={cn('rounded-md border', {
|
||||
'dark:bg-amber-950 bg-amber-50 border-none dark:text-amber-600 text-amber-700':
|
||||
variant === MarkdownVariant.WARNING,
|
||||
'bg-success-100 text-success-300 border-none':
|
||||
variant === MarkdownVariant.TIP,
|
||||
'p-0 bg-transparent border-none':
|
||||
variant === MarkdownVariant.BORDERLESS,
|
||||
})}
|
||||
>
|
||||
{variant !== MarkdownVariant.BORDERLESS && (
|
||||
<>
|
||||
{(variant === MarkdownVariant.INFO || variant === undefined) && (
|
||||
<Info className="w-4 h-4 mt-1" />
|
||||
)}
|
||||
{variant === MarkdownVariant.WARNING && (
|
||||
<AlertTriangle className="w-4 h-4 mt-1 stroke-amber-700" />
|
||||
)}
|
||||
{variant === MarkdownVariant.TIP && (
|
||||
<Lightbulb className="w-4 h-4 mt-1" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<AlertDescription className="grow w-full">{children}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
const ApMarkdown = React.memo(
|
||||
({ markdown, variables, variant, className, loading }: MarkdownProps) => {
|
||||
const [copiedText, setCopiedText] = useState<string | null>(null);
|
||||
|
||||
const { mutate: copyToClipboard } = useMutation({
|
||||
mutationFn: async (text: string) => {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopiedText(text);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
setCopiedText(null);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t('Failed to copy to clipboard'), {
|
||||
duration: 3000,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (loading && loading.length > 0) {
|
||||
return (
|
||||
<Container variant={variant}>
|
||||
<div className="flex items-center gap-2">{loading}</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (!markdown) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const markdownProcessed = applyVariables(markdown, variables ?? {});
|
||||
|
||||
return (
|
||||
<Container variant={variant}>
|
||||
<ReactMarkdown
|
||||
className={cn('grow w-full ', className)}
|
||||
remarkPlugins={[gfm, breaks]}
|
||||
components={{
|
||||
code(props) {
|
||||
const isLanguageText = props.className?.includes('language-text');
|
||||
if (!isLanguageText) {
|
||||
return <code {...props} className="text-wrap" />;
|
||||
}
|
||||
const codeContent = String(props.children).trim();
|
||||
const isCopying = codeContent === copiedText;
|
||||
return (
|
||||
<div className="relative w-full items-center flex bg-background border border-solid text-sm rounded block w-full gap-1 p-1.5">
|
||||
<input
|
||||
type="text"
|
||||
className="grow bg-background"
|
||||
value={codeContent}
|
||||
disabled
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="bg-background rounded p-2 inline-flex items-center justify-center h-8"
|
||||
onClick={() => copyToClipboard(codeContent)}
|
||||
>
|
||||
{isCopying ? (
|
||||
<Check className="w-3 h-3" />
|
||||
) : (
|
||||
<Copy className="w-3 h-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
h1: ({ node, ...props }) => (
|
||||
<h1
|
||||
className="scroll-m-20 text-xl font-extrabold tracking-tight lg:text-3xl"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h2: ({ node, ...props }) => (
|
||||
<h2
|
||||
className="scroll-m-20 text-lg text-xl font-semibold tracking-tight first:mt-0"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h3: ({ node, ...props }) => (
|
||||
<h3
|
||||
className="scroll-m-20 text-lg font-semibold tracking-tight"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
p: ({ node, ...props }) => (
|
||||
<p className="leading-7 not-first:mt-2 w-full" {...props} />
|
||||
),
|
||||
ul: ({ node, ...props }) => (
|
||||
<ul className="mt-4 ml-6 list-disc [&>li]:mt-2" {...props} />
|
||||
),
|
||||
ol: ({ node, ...props }) => (
|
||||
<ol className="mt-4 ml-6 list-decimal [&>li]:mt-2" {...props} />
|
||||
),
|
||||
li: ({ node, ...props }) => <li {...props} />,
|
||||
a: ({ node, ...props }) => (
|
||||
<a
|
||||
className="font-medium text-primary underline underline-offset-4"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
blockquote: ({ node, ...props }) => (
|
||||
<blockquote
|
||||
className="mt-4 first:mt-0 border-l-2 pl-6 italic"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
hr: ({ node, ...props }) => (
|
||||
<hr className="my-4 border-t border-border/50" {...props} />
|
||||
),
|
||||
img: ({ node, ...props }) => <img className="my-8" {...props} />,
|
||||
b: ({ node, ...props }) => <b {...props} />,
|
||||
em: ({ node, ...props }) => <em {...props} />,
|
||||
table: ({ node, ...props }) => (
|
||||
<table className="w-full my-4 border-collapse" {...props} />
|
||||
),
|
||||
thead: ({ node, ...props }) => (
|
||||
<thead className="bg-muted" {...props} />
|
||||
),
|
||||
tr: ({ node, ...props }) => (
|
||||
<tr className="border-b border-border" {...props} />
|
||||
),
|
||||
th: ({ node, ...props }) => (
|
||||
<th className="text-left p-2 font-medium" {...props} />
|
||||
),
|
||||
td: ({ node, ...props }) => <td className="p-2" {...props} />,
|
||||
}}
|
||||
>
|
||||
{markdownProcessed.trim()}
|
||||
</ReactMarkdown>
|
||||
</Container>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ApMarkdown.displayName = 'ApMarkdown';
|
||||
export { ApMarkdown };
|
||||
@@ -0,0 +1,160 @@
|
||||
import deepEqual from 'deep-equal';
|
||||
import { t } from 'i18next';
|
||||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
MultiSelect,
|
||||
MultiSelectContent,
|
||||
MultiSelectItem,
|
||||
MultiSelectList,
|
||||
MultiSelectSearch,
|
||||
MultiSelectTrigger,
|
||||
MultiSelectValue,
|
||||
} from '@/components/custom/multi-select';
|
||||
import { CommandEmpty } from '@/components/ui/command';
|
||||
|
||||
type MultiSelectPiecePropertyProps = {
|
||||
placeholder: string;
|
||||
options: {
|
||||
value: unknown;
|
||||
label: string;
|
||||
}[];
|
||||
onChange: (value: unknown[] | null) => void;
|
||||
initialValues?: unknown[];
|
||||
disabled?: boolean;
|
||||
showDeselect?: boolean;
|
||||
showRefresh?: boolean;
|
||||
loading?: boolean;
|
||||
onRefresh?: () => void;
|
||||
refreshOnSearch?: (term: string) => void;
|
||||
/**Use to show the selected option when search doesn't return the selected option */
|
||||
cachedOptions?: {
|
||||
value: unknown;
|
||||
label: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
const MultiSelectPieceProperty = ({
|
||||
placeholder,
|
||||
options,
|
||||
onChange,
|
||||
disabled,
|
||||
initialValues,
|
||||
showDeselect,
|
||||
showRefresh,
|
||||
onRefresh,
|
||||
loading,
|
||||
refreshOnSearch,
|
||||
cachedOptions = [],
|
||||
}: MultiSelectPiecePropertyProps) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const filteredOptions = options
|
||||
.map((option, index) => ({
|
||||
...option,
|
||||
originalIndex: index,
|
||||
}))
|
||||
.filter((option) => {
|
||||
if (refreshOnSearch) {
|
||||
return true;
|
||||
}
|
||||
return option.label?.toLowerCase()?.includes(searchTerm?.toLowerCase());
|
||||
});
|
||||
|
||||
const selectedIndicies = initialValues
|
||||
? initialValues
|
||||
.map((value) =>
|
||||
[...cachedOptions, ...options].findIndex((option) =>
|
||||
deepEqual(option.value, value),
|
||||
),
|
||||
)
|
||||
.filter((index) => index > -1)
|
||||
.map((index) => String(index))
|
||||
: [];
|
||||
const sendChanges = (indicides: string[]) => {
|
||||
const newSelectedIndicies = indicides.filter(
|
||||
(index) => index !== undefined,
|
||||
);
|
||||
if (newSelectedIndicies.length === 0) {
|
||||
onChange([]);
|
||||
} else {
|
||||
onChange(
|
||||
newSelectedIndicies.map((index) => options[Number(index)].value),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
modal={true}
|
||||
value={selectedIndicies}
|
||||
onValueChange={sendChanges}
|
||||
disabled={disabled}
|
||||
onSearch={(searchTerm) => {
|
||||
setSearchTerm(searchTerm ?? '');
|
||||
if (refreshOnSearch) {
|
||||
refreshOnSearch(searchTerm ?? '');
|
||||
}
|
||||
}}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setSearchTerm('');
|
||||
if (refreshOnSearch && searchTerm.length > 0) {
|
||||
refreshOnSearch('');
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MultiSelectTrigger
|
||||
showDeselect={showDeselect && !disabled}
|
||||
onDeselect={() => onChange([])}
|
||||
showRefresh={showRefresh && !disabled}
|
||||
onRefresh={onRefresh}
|
||||
loading={loading}
|
||||
>
|
||||
{selectedIndicies.length < 10 ? (
|
||||
<MultiSelectValue placeholder={placeholder} />
|
||||
) : (
|
||||
t('{number} items selected', { number: selectedIndicies.length })
|
||||
)}
|
||||
</MultiSelectTrigger>
|
||||
<MultiSelectContent>
|
||||
<MultiSelectSearch placeholder={placeholder} />
|
||||
<MultiSelectList>
|
||||
{!loading && (
|
||||
<>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onChange(filteredOptions.map((opt) => opt.value));
|
||||
}}
|
||||
>
|
||||
{filteredOptions.length > 1 && (
|
||||
<MultiSelectItem>{t('Select All')}</MultiSelectItem>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{filteredOptions.map((opt) => (
|
||||
<MultiSelectItem
|
||||
key={opt.originalIndex}
|
||||
value={String(opt.originalIndex)}
|
||||
>
|
||||
{opt.label}
|
||||
</MultiSelectItem>
|
||||
))}
|
||||
{filteredOptions.length === 0 && (
|
||||
<CommandEmpty>{t('No results found.')}</CommandEmpty>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{loading && (
|
||||
<MultiSelectItem disabled>{t('Loading...')}</MultiSelectItem>
|
||||
)}
|
||||
</MultiSelectList>
|
||||
</MultiSelectContent>
|
||||
</MultiSelect>
|
||||
);
|
||||
};
|
||||
|
||||
MultiSelectPieceProperty.displayName = 'MultiSelectPieceProperty';
|
||||
export { MultiSelectPieceProperty };
|
||||
@@ -0,0 +1,614 @@
|
||||
'use client';
|
||||
|
||||
// Used form here https://github.com/shadcn-ui/ui/pull/2773/files
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||
import { Primitive } from '@radix-ui/react-primitive';
|
||||
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
||||
import { t } from 'i18next'; // Use t function from react-i18next
|
||||
import { Check, ChevronsUpDown, RefreshCcw, X } from 'lucide-react';
|
||||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { SelectUtilButton } from '@/components/custom/select-util-button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { Badge } from '../ui/badge';
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from '../ui/command';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '../ui/tooltip';
|
||||
|
||||
export interface MultiSelectOptionItem {
|
||||
value: unknown;
|
||||
label?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface MultiSelectContextValue {
|
||||
value: string[];
|
||||
|
||||
open: boolean;
|
||||
|
||||
onSelect(value: string, item: MultiSelectOptionItem): void;
|
||||
|
||||
onDeselect(value: string, item: MultiSelectOptionItem): void;
|
||||
|
||||
onSearch?(keyword: string | undefined): void;
|
||||
|
||||
filter?: boolean | ((keyword: string, current: string) => boolean);
|
||||
|
||||
disabled?: boolean;
|
||||
|
||||
maxCount?: number;
|
||||
|
||||
itemCache: Map<string, MultiSelectOptionItem>;
|
||||
}
|
||||
|
||||
const MultiSelectContext = React.createContext<
|
||||
MultiSelectContextValue | undefined
|
||||
>(undefined);
|
||||
|
||||
const useMultiSelect = () => {
|
||||
const context = React.useContext(MultiSelectContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
t('useMultiSelect must be used within MultiSelectProvider'),
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
type MultiSelectProps = React.ComponentPropsWithoutRef<
|
||||
typeof PopoverPrimitive.Root
|
||||
> & {
|
||||
value?: string[];
|
||||
onValueChange?(value: string[], items: MultiSelectOptionItem[]): void;
|
||||
onSelect?(value: string, item: MultiSelectOptionItem): void;
|
||||
onDeselect?(value: string, item: MultiSelectOptionItem): void;
|
||||
defaultValue?: string[];
|
||||
onSearch?(keyword: string | undefined): void;
|
||||
filter?: boolean | ((keyword: string, current: string) => boolean);
|
||||
disabled?: boolean;
|
||||
maxCount?: number;
|
||||
};
|
||||
|
||||
const MultiSelect: React.FC<MultiSelectProps> = ({
|
||||
value: valueProp,
|
||||
onValueChange: onValueChangeProp,
|
||||
onDeselect: onDeselectProp,
|
||||
onSelect: onSelectProp,
|
||||
defaultValue,
|
||||
open: openProp,
|
||||
onOpenChange,
|
||||
defaultOpen,
|
||||
onSearch,
|
||||
filter,
|
||||
disabled,
|
||||
maxCount,
|
||||
...popoverProps
|
||||
}) => {
|
||||
const itemCache = React.useRef(
|
||||
new Map<string, MultiSelectOptionItem>(),
|
||||
).current;
|
||||
|
||||
const handleValueChange = React.useCallback(
|
||||
(state: string[]) => {
|
||||
if (onValueChangeProp) {
|
||||
const items = state.map((value) => itemCache.get(value)!);
|
||||
|
||||
onValueChangeProp(state, items);
|
||||
}
|
||||
},
|
||||
[onValueChangeProp],
|
||||
);
|
||||
|
||||
const [value, setValue] = useControllableState({
|
||||
prop: valueProp,
|
||||
defaultProp: defaultValue,
|
||||
onChange: handleValueChange,
|
||||
});
|
||||
|
||||
const [open, setOpen] = useControllableState({
|
||||
prop: openProp,
|
||||
defaultProp: defaultOpen,
|
||||
onChange: onOpenChange,
|
||||
});
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(value: string, item: MultiSelectOptionItem) => {
|
||||
setValue((prev) => {
|
||||
if (prev?.includes(value)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
onSelectProp?.(value, item);
|
||||
|
||||
return prev ? [...prev, value] : [value];
|
||||
});
|
||||
},
|
||||
[onSelectProp, setValue],
|
||||
);
|
||||
|
||||
const handleDeselect = React.useCallback(
|
||||
(value: string, item: MultiSelectOptionItem) => {
|
||||
setValue((prev) => {
|
||||
if (!prev || !prev.includes(value)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
onDeselectProp?.(value, item);
|
||||
|
||||
return prev.filter((v) => v !== value);
|
||||
});
|
||||
},
|
||||
[onDeselectProp, setValue],
|
||||
);
|
||||
|
||||
const contextValue = React.useMemo(() => {
|
||||
return {
|
||||
value: value || [],
|
||||
open: open || false,
|
||||
onSearch,
|
||||
filter,
|
||||
disabled,
|
||||
maxCount,
|
||||
onSelect: handleSelect,
|
||||
onDeselect: handleDeselect,
|
||||
itemCache,
|
||||
};
|
||||
}, [
|
||||
value,
|
||||
open,
|
||||
onSearch,
|
||||
filter,
|
||||
disabled,
|
||||
maxCount,
|
||||
handleSelect,
|
||||
handleDeselect,
|
||||
itemCache,
|
||||
]);
|
||||
|
||||
return (
|
||||
<MultiSelectContext.Provider value={contextValue}>
|
||||
<PopoverPrimitive.Root
|
||||
{...popoverProps}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
/>
|
||||
</MultiSelectContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
MultiSelect.displayName = 'MultiSelect';
|
||||
|
||||
type MultiSelectTriggerElement = React.ElementRef<typeof Primitive.button>;
|
||||
|
||||
type MultiSelectTriggerProps = ComponentPropsWithoutRef<
|
||||
typeof Primitive.button
|
||||
> & {
|
||||
showDeselect?: boolean;
|
||||
onDeselect?: () => void;
|
||||
showRefresh?: boolean;
|
||||
onRefresh?: () => void;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
const PreventClick = (e: React.MouseEvent | React.TouchEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const MultiSelectTrigger = React.forwardRef<
|
||||
MultiSelectTriggerElement,
|
||||
MultiSelectTriggerProps
|
||||
>(
|
||||
(
|
||||
{ className, children, showDeselect, onDeselect, loading, ...props },
|
||||
forwardedRef,
|
||||
) => {
|
||||
const { disabled } = useMultiSelect();
|
||||
|
||||
return (
|
||||
<PopoverPrimitive.Trigger ref={forwardedRef as any} asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-disabled={disabled}
|
||||
disabled={disabled}
|
||||
role="combobox"
|
||||
loading={loading}
|
||||
className={cn(
|
||||
'flex min-h-10 h-auto w-full items-center justify-between cursor-pointer gap-2 whitespace-nowrap rounded-sm border border-input bg-transparent px-4 py-1 text-sm ring-offset-background focus:outline-hidden focus:ring-1 focus:ring-ring [&>span]:line-clamp-1',
|
||||
{
|
||||
'cursor-not-allowed opacity-80': disabled,
|
||||
'cursor-pointer': !disabled,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
onClick={disabled ? PreventClick : props.onClick}
|
||||
onTouchStart={disabled ? PreventClick : props.onTouchStart}
|
||||
>
|
||||
{children}
|
||||
<div className="flex gap-2 items-center">
|
||||
{showDeselect && (
|
||||
<SelectUtilButton
|
||||
tooltipText={t('Unset')}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDeselect?.();
|
||||
}}
|
||||
Icon={X}
|
||||
></SelectUtilButton>
|
||||
)}
|
||||
{props.showRefresh && (
|
||||
<SelectUtilButton
|
||||
tooltipText={t('Refresh')}
|
||||
onClick={props.onRefresh}
|
||||
Icon={RefreshCcw}
|
||||
></SelectUtilButton>
|
||||
)}
|
||||
<ChevronsUpDown
|
||||
aria-hidden
|
||||
className="h-4 w-4 opacity-50 shrink-0"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverPrimitive.Trigger>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
MultiSelectTrigger.displayName = 'MultiSelectTrigger';
|
||||
|
||||
interface MultiSelectValueProps
|
||||
extends ComponentPropsWithoutRef<typeof Primitive.div> {
|
||||
placeholder?: string;
|
||||
maxDisplay?: number;
|
||||
maxItemLength?: number;
|
||||
}
|
||||
|
||||
const MultiSelectValue = React.forwardRef<
|
||||
React.ElementRef<typeof Primitive.div>,
|
||||
MultiSelectValueProps
|
||||
>(
|
||||
(
|
||||
{ className, placeholder, maxDisplay, maxItemLength, ...props },
|
||||
forwardRef,
|
||||
) => {
|
||||
const { value, itemCache, onDeselect, disabled } = useMultiSelect();
|
||||
const [firstRendered, setFirstRendered] = React.useState(false);
|
||||
|
||||
const remainingPiecesCount =
|
||||
maxDisplay && value.length > maxDisplay ? value.length - maxDisplay : 0;
|
||||
const renderItems = remainingPiecesCount
|
||||
? value.slice(0, maxDisplay)
|
||||
: value;
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
setFirstRendered(true);
|
||||
}, []);
|
||||
|
||||
if (!value.length || !firstRendered) {
|
||||
return (
|
||||
<span className="pointer-events-none text-muted-foreground opacity-80">
|
||||
{placeholder}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-1 overflow-x-hidden flex-wrap items-center gap-1.5',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={forwardRef}
|
||||
>
|
||||
{renderItems.map((value) => {
|
||||
const item = itemCache.get(value);
|
||||
const content = item?.label || value;
|
||||
const child =
|
||||
maxItemLength &&
|
||||
typeof content === 'string' &&
|
||||
content.length > maxItemLength
|
||||
? `${content.slice(0, maxItemLength)}...`
|
||||
: content;
|
||||
|
||||
const el = (
|
||||
<Badge
|
||||
variant="outline"
|
||||
key={value}
|
||||
className={cn('pr-1.5 group/multi-select-badge rounded-full', {
|
||||
'cursor-pointer': !disabled,
|
||||
'cursor-not-allowed opacity-80': disabled,
|
||||
})}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDeselect(value, item!);
|
||||
}}
|
||||
>
|
||||
<span>{child}</span>
|
||||
{!disabled && (
|
||||
<X className="h-3 w-3 ml-1 text-muted-foreground group-hover/multi-select-badge:text-foreground" />
|
||||
)}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
if (child !== content) {
|
||||
return (
|
||||
<Tooltip key={value}>
|
||||
<TooltipTrigger className="inline-flex">{el}</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="start" className="z-51">
|
||||
{content}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return el;
|
||||
})}
|
||||
{remainingPiecesCount ? (
|
||||
<span className="text-muted-foreground text-xs leading-4 py-.5">
|
||||
{t('+{remainingPiecesCount} more', {
|
||||
remainingPiecesCount: remainingPiecesCount,
|
||||
})}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
MultiSelectValue.displayName = 'MultiSelectValue';
|
||||
|
||||
const MultiSelectSearch = React.forwardRef<
|
||||
React.ElementRef<typeof CommandInput>,
|
||||
ComponentPropsWithoutRef<typeof CommandInput>
|
||||
>((props, ref) => {
|
||||
const { onSearch } = useMultiSelect();
|
||||
|
||||
return <CommandInput ref={ref} {...props} onValueChange={onSearch} />;
|
||||
});
|
||||
|
||||
MultiSelectSearch.displayName = 'MultiSelectSearch';
|
||||
|
||||
const MultiSelectList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandList>,
|
||||
ComponentPropsWithoutRef<typeof CommandList>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<CommandList ref={ref} className={cn('py-1 px-0 ', className)} {...props}>
|
||||
<ScrollArea viewPortClassName="max-h-[200px]">
|
||||
{props.children}
|
||||
</ScrollArea>
|
||||
</CommandList>
|
||||
);
|
||||
});
|
||||
|
||||
MultiSelectList.displayName = 'MultiSelectList';
|
||||
|
||||
type MultiSelectContentProps = ComponentPropsWithoutRef<
|
||||
typeof PopoverPrimitive.Content
|
||||
>;
|
||||
|
||||
const MultiSelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
MultiSelectContentProps
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const context = useMultiSelect();
|
||||
|
||||
const fragmentRef = React.useRef<DocumentFragment>();
|
||||
|
||||
if (!fragmentRef.current && typeof window !== 'undefined') {
|
||||
fragmentRef.current = document.createDocumentFragment();
|
||||
}
|
||||
|
||||
if (!context.open) {
|
||||
return fragmentRef.current
|
||||
? createPortal(<Command>{children}</Command>, fragmentRef.current)
|
||||
: null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PopoverPrimitive.Portal forceMount>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
collisionPadding={10}
|
||||
className={cn(
|
||||
'z-50 w-full rounded-md border bg-background p-0 text-foreground shadow-md outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
)}
|
||||
style={
|
||||
{
|
||||
'--radix-select-content-transform-origin':
|
||||
'var(--radix-popper-transform-origin)',
|
||||
'--radix-select-content-available-width':
|
||||
'var(--radix-popper-available-width)',
|
||||
'--radix-select-content-available-height':
|
||||
'var(--radix-popper-available-height)',
|
||||
'--radix-select-trigger-width': 'var(--radix-popper-anchor-width)',
|
||||
'--radix-select-trigger-height':
|
||||
'var(--radix-popper-anchor-height)',
|
||||
} as any
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<Command
|
||||
className={cn(
|
||||
'px-1 max-h-96 w-full min-w-(--radix-select-trigger-width)',
|
||||
className,
|
||||
)}
|
||||
shouldFilter={!context.onSearch}
|
||||
>
|
||||
{children}
|
||||
</Command>
|
||||
</PopoverPrimitive.Content>
|
||||
</PopoverPrimitive.Portal>
|
||||
);
|
||||
});
|
||||
MultiSelectContent.displayName = 'MultiSelectContent';
|
||||
|
||||
type MultiSelectItemProps = ComponentPropsWithoutRef<typeof CommandItem> &
|
||||
Partial<MultiSelectOptionItem> & {
|
||||
onSelect?: (value: string, item: MultiSelectOptionItem) => void;
|
||||
onDeselect?: (value: string, item: MultiSelectOptionItem) => void;
|
||||
};
|
||||
|
||||
const MultiSelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandItem>,
|
||||
MultiSelectItemProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
value,
|
||||
onSelect: onSelectProp,
|
||||
onDeselect: onDeselectProp,
|
||||
children,
|
||||
label,
|
||||
disabled: disabledProp,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
forwardedRef,
|
||||
) => {
|
||||
const {
|
||||
value: contextValue,
|
||||
maxCount,
|
||||
onSelect,
|
||||
onDeselect,
|
||||
itemCache,
|
||||
} = useMultiSelect();
|
||||
|
||||
const item = React.useMemo(() => {
|
||||
return value
|
||||
? {
|
||||
value,
|
||||
label:
|
||||
label || (typeof children === 'string' ? children : undefined),
|
||||
}
|
||||
: undefined;
|
||||
}, [value, label, children]);
|
||||
|
||||
const selected = Boolean(value && contextValue.includes(value));
|
||||
|
||||
React.useEffect(() => {
|
||||
if (value) {
|
||||
itemCache.set(value, item!);
|
||||
}
|
||||
}, [selected, value, item]);
|
||||
|
||||
const disabled = Boolean(
|
||||
disabledProp ||
|
||||
(!selected && maxCount && contextValue.length >= maxCount),
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
if (selected) {
|
||||
onDeselectProp?.(value!, item!);
|
||||
onDeselect(value!, item!);
|
||||
} else {
|
||||
itemCache.set(value!, item!);
|
||||
onSelectProp?.(value!, item!);
|
||||
onSelect(value!, item!);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
{...props}
|
||||
value={value}
|
||||
className={cn(
|
||||
'cursor-pointer',
|
||||
disabled && 'text-muted-foreground cursor-not-allowed',
|
||||
className,
|
||||
)}
|
||||
disabled={disabled}
|
||||
onSelect={!disabled && value ? handleClick : undefined}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
<span className="mr-2 whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{children || label || value}
|
||||
</span>
|
||||
{selected ? <Check className="h-4 w-4 ml-auto shrink-0" /> : null}
|
||||
</CommandItem>
|
||||
);
|
||||
},
|
||||
);
|
||||
MultiSelectItem.displayName = 'MultiSelectItem';
|
||||
|
||||
const MultiSelectGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandGroup>,
|
||||
ComponentPropsWithoutRef<typeof CommandGroup>
|
||||
>((props, forwardRef) => {
|
||||
return <CommandGroup {...props} ref={forwardRef} />;
|
||||
});
|
||||
|
||||
MultiSelectGroup.displayName = 'MultiSelectGroup';
|
||||
|
||||
const MultiSelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandSeparator>,
|
||||
ComponentPropsWithoutRef<typeof CommandSeparator>
|
||||
>((props, forwardRef) => {
|
||||
return <CommandSeparator {...props} ref={forwardRef} />;
|
||||
});
|
||||
|
||||
MultiSelectSeparator.displayName = 'MultiSelectSeparator';
|
||||
|
||||
const MultiSelectEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandEmpty>,
|
||||
ComponentPropsWithoutRef<typeof CommandEmpty>
|
||||
>(({ children = 'No Content', ...props }, forwardRef) => {
|
||||
return (
|
||||
<CommandEmpty {...props} ref={forwardRef}>
|
||||
{children}
|
||||
</CommandEmpty>
|
||||
);
|
||||
});
|
||||
|
||||
MultiSelectEmpty.displayName = 'MultiSelectEmpty';
|
||||
|
||||
export interface MultiSelectOptionSeparator {
|
||||
type: 'separator';
|
||||
}
|
||||
|
||||
export interface MultiSelectOptionGroup {
|
||||
heading?: React.ReactNode;
|
||||
value?: string;
|
||||
children: MultiSelectOption[];
|
||||
}
|
||||
|
||||
export type MultiSelectOption = {
|
||||
value: unknown;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export {
|
||||
MultiSelect,
|
||||
MultiSelectTrigger,
|
||||
MultiSelectValue,
|
||||
MultiSelectSearch,
|
||||
MultiSelectContent,
|
||||
MultiSelectList,
|
||||
MultiSelectItem,
|
||||
MultiSelectGroup,
|
||||
MultiSelectSeparator,
|
||||
MultiSelectEmpty,
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { useEmbedding } from '@/components/embed-provider';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { SidebarTrigger } from '@/components/ui/sidebar-shadcn';
|
||||
|
||||
interface PageHeaderProps {
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
leftContent?: ReactNode;
|
||||
rightContent?: ReactNode;
|
||||
showBorder?: boolean;
|
||||
className?: string;
|
||||
hideSidebarTrigger?: boolean;
|
||||
}
|
||||
|
||||
export const PageHeader = ({
|
||||
title,
|
||||
description,
|
||||
leftContent,
|
||||
rightContent,
|
||||
showBorder = false,
|
||||
className = '',
|
||||
hideSidebarTrigger = false,
|
||||
}: PageHeaderProps) => {
|
||||
const { embedState } = useEmbedding();
|
||||
|
||||
if (embedState.hidePageHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const showSidebarTrigger = !hideSidebarTrigger && !embedState.isEmbedded;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-between py-3 w-full px-4 ${
|
||||
showBorder ? 'border-b' : ''
|
||||
} ${className}`}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
{showSidebarTrigger && <SidebarTrigger />}
|
||||
{showSidebarTrigger && (
|
||||
<Separator orientation="vertical" className="h-5 mr-2" />
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
{typeof title === 'string' ? (
|
||||
<h1 className="text-base font-normal">{title}</h1>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</div>
|
||||
{description && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{leftContent}
|
||||
</div>
|
||||
|
||||
{rightContent}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { t } from 'i18next';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
export const PermissionNeededTooltip = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
{ children: React.ReactNode; hasPermission: boolean }
|
||||
>(({ children, hasPermission }, ref) => {
|
||||
return (
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger ref={ref} asChild disabled={!hasPermission}>
|
||||
<div>{children}</div>
|
||||
</TooltipTrigger>
|
||||
{!hasPermission && (
|
||||
<TooltipContent side="top">{t('Permission needed')}</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
PermissionNeededTooltip.displayName = 'PermissionNeededWrapper';
|
||||
@@ -0,0 +1,68 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { RadioGroup, RadioGroupItem } from '../ui/radio-group';
|
||||
|
||||
import { CardListItem } from './card-list';
|
||||
|
||||
export type RadioGroupListItem<T> = {
|
||||
label: string;
|
||||
value: T;
|
||||
labelExtra?: React.ReactNode;
|
||||
description?: string;
|
||||
};
|
||||
const RadioGroupList = <T,>({
|
||||
items,
|
||||
onChange,
|
||||
value,
|
||||
onHover,
|
||||
className,
|
||||
}: {
|
||||
items: RadioGroupListItem<T>[];
|
||||
onChange: (value: T) => void;
|
||||
value: T | null;
|
||||
onHover?: (value: T | null) => void;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn('space-y-4', className)}>
|
||||
<RadioGroup value={JSON.stringify(value)}>
|
||||
{items.map((item, index) => {
|
||||
const selected = item.value === value;
|
||||
return (
|
||||
<CardListItem
|
||||
key={index}
|
||||
className={cn(
|
||||
`p-4 rounded-lg border block hover:border-primary/50 hover:bg-muted/50`,
|
||||
{
|
||||
'border-primary bg-primary/5': selected,
|
||||
},
|
||||
)}
|
||||
onClick={() => onChange(item.value)}
|
||||
onMouseEnter={() => onHover && onHover(item.value)}
|
||||
onMouseLeave={() => onHover && onHover(null)}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h4 className="text-md font-medium flex items-center gap-2">
|
||||
{item.label}
|
||||
{item.labelExtra}
|
||||
</h4>
|
||||
<div className="shrink-0 w-5 h-5">
|
||||
<RadioGroupItem
|
||||
value={JSON.stringify(item.value)}
|
||||
className="scale-125"
|
||||
></RadioGroupItem>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{item.description}
|
||||
</div>
|
||||
</CardListItem>
|
||||
);
|
||||
})}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RadioGroupList.displayName = 'RadioGroupList';
|
||||
export { RadioGroupList };
|
||||
@@ -0,0 +1,284 @@
|
||||
import deepEqual from 'deep-equal';
|
||||
import { t } from 'i18next';
|
||||
import { Check, ChevronsUpDown, RefreshCcw, X } from 'lucide-react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
|
||||
import { SelectUtilButton } from '@/components/custom/select-util-button';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { Button } from '../ui/button';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
|
||||
type SelectOption<T> = {
|
||||
value: T;
|
||||
label: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
type SearchableSelectProps<T> = {
|
||||
options: SelectOption<T>[];
|
||||
onChange: (value: T | null) => void;
|
||||
value: T | undefined;
|
||||
placeholder: string;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
showDeselect?: boolean;
|
||||
onRefresh?: () => void;
|
||||
showRefresh?: boolean;
|
||||
onClose?: () => void;
|
||||
triggerClassName?: string;
|
||||
valuesRendering?: (value: unknown) => React.ReactNode;
|
||||
openState?: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
};
|
||||
refreshOnSearch?: (searchValue: string) => void;
|
||||
/**Use to show the selected option when search doesn't return the selected option */
|
||||
cachedOptions?: {
|
||||
value: T;
|
||||
label: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
const useOpenState = (openStateInitializer?: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
if (openStateInitializer) {
|
||||
return openStateInitializer;
|
||||
}
|
||||
return {
|
||||
open: isOpen,
|
||||
setOpen: setIsOpen,
|
||||
};
|
||||
};
|
||||
export const SearchableSelect = <T,>({
|
||||
options,
|
||||
onChange,
|
||||
value,
|
||||
placeholder,
|
||||
disabled,
|
||||
loading,
|
||||
showDeselect,
|
||||
onRefresh,
|
||||
showRefresh,
|
||||
onClose,
|
||||
triggerClassName,
|
||||
valuesRendering,
|
||||
openState: openStateInitializer,
|
||||
refreshOnSearch,
|
||||
cachedOptions = [],
|
||||
}: SearchableSelectProps<T>) => {
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const { open, setOpen } = useOpenState(openStateInitializer);
|
||||
const triggerWidth = `${triggerRef.current?.clientWidth ?? 0}px`;
|
||||
const selectedOption =
|
||||
[...cachedOptions, ...options].find((option) =>
|
||||
deepEqual(option.value, value),
|
||||
) ?? undefined;
|
||||
const filterOptionsIndices = options
|
||||
.map((option, index) => {
|
||||
return {
|
||||
label: option.label,
|
||||
value: option.value,
|
||||
index: index,
|
||||
description: option.description ?? '',
|
||||
};
|
||||
})
|
||||
.filter((option) => {
|
||||
if (refreshOnSearch || searchTerm.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
option.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
option.description.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
})
|
||||
.map((option) => option.index);
|
||||
|
||||
const onSelect = (index: string) => {
|
||||
const optionIndex =
|
||||
Number.isInteger(parseInt(index)) && !Number.isNaN(parseInt(index))
|
||||
? parseInt(index)
|
||||
: -1;
|
||||
setSearchTerm('');
|
||||
|
||||
if (optionIndex === -1) {
|
||||
return;
|
||||
}
|
||||
const option = options[optionIndex];
|
||||
onChange(option.value);
|
||||
};
|
||||
return (
|
||||
<Popover
|
||||
modal={true}
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
onClose?.();
|
||||
}
|
||||
if (refreshOnSearch && searchTerm.length > 0) {
|
||||
refreshOnSearch('');
|
||||
setSearchTerm('');
|
||||
}
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger
|
||||
asChild
|
||||
className={cn({
|
||||
'cursor-not-allowed opacity-80 ': disabled,
|
||||
})}
|
||||
onClick={(e) => {
|
||||
if (disabled) {
|
||||
e.preventDefault();
|
||||
}
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="relative">
|
||||
<Button
|
||||
ref={triggerRef}
|
||||
variant="outline"
|
||||
disabled={disabled}
|
||||
role="combobox"
|
||||
loading={loading}
|
||||
aria-expanded={open}
|
||||
className={cn('w-full justify-between', triggerClassName)}
|
||||
onClick={(e) => {
|
||||
setOpen(!open);
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<span className="flex w-full truncate select-none">
|
||||
{selectedOption
|
||||
? valuesRendering
|
||||
? valuesRendering(selectedOption.value)
|
||||
: selectedOption.label
|
||||
: placeholder}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
<div className="right-10 top-2 absolute flex gap-2 z-50 items-center">
|
||||
{showDeselect && !disabled && selectedOption && !loading && (
|
||||
<SelectUtilButton
|
||||
tooltipText={t('Unset')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onChange(null);
|
||||
}}
|
||||
Icon={X}
|
||||
></SelectUtilButton>
|
||||
)}
|
||||
{showRefresh && !loading && (
|
||||
<SelectUtilButton
|
||||
tooltipText={t('Refresh')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (onRefresh) {
|
||||
onRefresh();
|
||||
}
|
||||
}}
|
||||
Icon={RefreshCcw}
|
||||
></SelectUtilButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
style={{
|
||||
maxWidth: triggerWidth,
|
||||
minWidth: triggerWidth,
|
||||
}}
|
||||
className="min-w-full w-full p-0"
|
||||
>
|
||||
<Command className="w-full" shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder={t(placeholder)}
|
||||
value={searchTerm}
|
||||
onValueChange={(e) => {
|
||||
setSearchTerm(e);
|
||||
if (refreshOnSearch) {
|
||||
refreshOnSearch(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{filterOptionsIndices.length === 0 && (
|
||||
<CommandEmpty>{t('No results found.')}</CommandEmpty>
|
||||
)}
|
||||
|
||||
<CommandGroup>
|
||||
<CommandList>
|
||||
<ScrollArea
|
||||
className="h-full"
|
||||
viewPortClassName={'max-h-[200px]'}
|
||||
>
|
||||
{filterOptionsIndices &&
|
||||
!loading &&
|
||||
filterOptionsIndices.map((filterIndex) => {
|
||||
const option = options[filterIndex];
|
||||
if (!option) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<CommandItem
|
||||
key={filterIndex}
|
||||
value={String(filterIndex)}
|
||||
onSelect={(currentValue) => {
|
||||
setOpen(false);
|
||||
onSelect(currentValue);
|
||||
}}
|
||||
className="flex gap-2 flex-col items-start"
|
||||
>
|
||||
<div className="flex gap-2 items-center justify-between w-full">
|
||||
{option.label === '' ? (
|
||||
<span className=""> </span>
|
||||
) : valuesRendering ? (
|
||||
valuesRendering(option.value)
|
||||
) : (
|
||||
option.label
|
||||
)}
|
||||
<Check
|
||||
className={cn('shrink-0 w-4 h-4', {
|
||||
hidden: selectedOption?.value !== option.value,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{option.description && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{option.description}
|
||||
</div>
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
{loading && (
|
||||
<CommandItem disabled>{t('Loading...')}</CommandItem>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</CommandList>
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
SearchableSelect.displayName = 'SearchableSelect';
|
||||
@@ -0,0 +1,36 @@
|
||||
import { TooltipTrigger } from '@radix-ui/react-tooltip';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent } from '@/components/ui/tooltip';
|
||||
|
||||
const SelectUtilButton = ({
|
||||
onClick,
|
||||
Icon,
|
||||
tooltipText,
|
||||
}: {
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
Icon: LucideIcon;
|
||||
tooltipText?: string;
|
||||
}) => {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="opacity-50 shrink-0 h-6 w-6 rounded-xs"
|
||||
size={'icon'}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon className="w-4 h-4"></Icon>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
{tooltipText && (
|
||||
<TooltipContent side="bottom">{tooltipText}</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
SelectUtilButton.displayName = 'SelectUtilButton';
|
||||
export { SelectUtilButton };
|
||||
@@ -0,0 +1,55 @@
|
||||
import React, { useRef, useState, useCallback, useEffect } from 'react';
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface TextWithTooltipProps {
|
||||
tooltipMessage: string;
|
||||
children: React.ReactElement<
|
||||
React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> }
|
||||
>;
|
||||
}
|
||||
export const TextWithTooltip = ({
|
||||
tooltipMessage,
|
||||
children,
|
||||
}: TextWithTooltipProps) => {
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
const [isTruncated, setIsTruncated] = useState(false);
|
||||
|
||||
const checkTruncation = useCallback(() => {
|
||||
if (textRef.current) {
|
||||
setIsTruncated(textRef.current.scrollWidth > textRef.current.clientWidth);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkTruncation();
|
||||
window.addEventListener('resize', checkTruncation);
|
||||
return () => window.removeEventListener('resize', checkTruncation);
|
||||
}, [checkTruncation]);
|
||||
|
||||
const childWithRef = React.cloneElement(children, {
|
||||
ref: textRef,
|
||||
className: cn('truncate', children.props.className),
|
||||
});
|
||||
|
||||
if (!isTruncated) {
|
||||
return childWithRef;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{childWithRef}</TooltipTrigger>
|
||||
<TooltipContent className="max-w-md wrap-break-word whitespace-normal">
|
||||
<p>{tooltipMessage}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
|
||||
type DataListProps = {
|
||||
data?: Record<string, any>;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function formatValue(value: any): string {
|
||||
if (Array.isArray(value)) return value.join(', ');
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export const DataList: React.FC<DataListProps> = ({
|
||||
data = {},
|
||||
className = '',
|
||||
}) => {
|
||||
const entries = Object.entries(data).filter(
|
||||
([_, value]) => value !== null && value !== undefined,
|
||||
);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return (
|
||||
<div className={`text-sm text-muted-foreground italic ${className}`}>
|
||||
No data available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<dl
|
||||
className={`grid gap-y-2 text-sm leading-relaxed ${className}`}
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
>
|
||||
{entries.map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="grid grid-cols-[auto_1fr] gap-x-3 items-start"
|
||||
>
|
||||
<dt className="font-medium text-muted-foreground capitalize">
|
||||
{key}
|
||||
</dt>
|
||||
<dd className="text-primary">{formatValue(value)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { t } from 'i18next';
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
interface ConfirmationDeleteDialogProps {
|
||||
title: string;
|
||||
message: React.ReactNode | string;
|
||||
mutationFn: () => Promise<void>;
|
||||
entityName: string;
|
||||
children?: React.ReactNode;
|
||||
open?: boolean;
|
||||
isDanger?: boolean;
|
||||
buttonText?: string;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
showToast?: boolean;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
export const ConfirmationDeleteDialog = ({
|
||||
title,
|
||||
message,
|
||||
mutationFn,
|
||||
showToast,
|
||||
isDanger,
|
||||
entityName,
|
||||
buttonText,
|
||||
children,
|
||||
open,
|
||||
onError,
|
||||
onOpenChange,
|
||||
}: ConfirmationDeleteDialogProps) => {
|
||||
const [isControlled] = useState(
|
||||
open !== undefined && onOpenChange !== undefined,
|
||||
);
|
||||
const [isUncontrolledOpen, setIsUncontrolledOpen] = useState(false);
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn,
|
||||
onSuccess: () => {
|
||||
handleClose();
|
||||
if (showToast) {
|
||||
toast.success(t('Removed {entityName}', { entityName }));
|
||||
}
|
||||
},
|
||||
onError,
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
if (isControlled) {
|
||||
onOpenChange?.(false);
|
||||
} else {
|
||||
setIsUncontrolledOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isOpen = isControlled ? open : isUncontrolledOpen;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={isControlled ? onOpenChange : setIsUncontrolledOpen}
|
||||
>
|
||||
{children && <DialogTrigger asChild>{children}</DialogTrigger>}
|
||||
|
||||
<DialogContent onClick={(e) => e.stopPropagation()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription className="pt-2">{message}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={isPending}
|
||||
onClick={() => handleClose()}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
loading={isPending}
|
||||
onClick={() => mutate()}
|
||||
>
|
||||
{isDanger && <TriangleAlert className="size-4 mr-2" />}
|
||||
{buttonText || t('Remove')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type EmbeddingState = {
|
||||
isEmbedded: boolean;
|
||||
hideSideNav: boolean;
|
||||
hideFlowsPageNavbar: boolean;
|
||||
disableNavigationInBuilder: boolean;
|
||||
hideFolders: boolean;
|
||||
hideFlowNameInBuilder: boolean;
|
||||
hideExportAndImportFlow: boolean;
|
||||
sdkVersion?: string;
|
||||
predefinedConnectionName?: string;
|
||||
fontUrl?: string;
|
||||
fontFamily?: string;
|
||||
useDarkBackground: boolean;
|
||||
hideHomeButtonInBuilder: boolean;
|
||||
emitHomeButtonClickedEvent: boolean;
|
||||
homeButtonIcon: 'back' | 'logo';
|
||||
hideDuplicateFlow: boolean;
|
||||
hidePageHeader: boolean;
|
||||
};
|
||||
|
||||
const defaultState: EmbeddingState = {
|
||||
isEmbedded: false,
|
||||
hideSideNav: false,
|
||||
hideFlowsPageNavbar: false,
|
||||
disableNavigationInBuilder: false,
|
||||
hideFolders: false,
|
||||
hideFlowNameInBuilder: false,
|
||||
hideExportAndImportFlow: false,
|
||||
useDarkBackground: window.opener !== null,
|
||||
hideHomeButtonInBuilder: false,
|
||||
emitHomeButtonClickedEvent: false,
|
||||
homeButtonIcon: 'logo',
|
||||
hideDuplicateFlow: false,
|
||||
hidePageHeader: false,
|
||||
};
|
||||
|
||||
const EmbeddingContext = createContext<{
|
||||
embedState: EmbeddingState;
|
||||
setEmbedState: React.Dispatch<React.SetStateAction<EmbeddingState>>;
|
||||
}>({
|
||||
embedState: defaultState,
|
||||
setEmbedState: () => {},
|
||||
});
|
||||
|
||||
export const useEmbedding = () => useContext(EmbeddingContext);
|
||||
|
||||
type EmbeddingProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const EmbeddingProvider = ({ children }: EmbeddingProviderProps) => {
|
||||
const [state, setState] = useState<EmbeddingState>(defaultState);
|
||||
|
||||
return (
|
||||
<EmbeddingContext.Provider
|
||||
value={{ embedState: state, setEmbedState: setState }}
|
||||
>
|
||||
<div
|
||||
className={cn({
|
||||
'bg-black/80 h-screen w-screen':
|
||||
state.useDarkBackground && state.isEmbedded,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</EmbeddingContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
EmbeddingProvider.displayName = 'EmbeddingProvider';
|
||||
|
||||
export { EmbeddingProvider };
|
||||
@@ -0,0 +1,253 @@
|
||||
import { t } from 'i18next';
|
||||
import { Copy, Download, Eye, EyeOff } from 'lucide-react';
|
||||
import React, { useLayoutEffect, useMemo } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import ReactJson from 'react-json-view';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { cn, isStepFileUrl } from '@/lib/utils';
|
||||
import { isNil } from '@activepieces/shared';
|
||||
|
||||
import { Button } from './ui/button';
|
||||
|
||||
type JsonViewerProps = {
|
||||
json: any;
|
||||
title: string;
|
||||
hideDownload?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type FileButtonProps = {
|
||||
fileUrl: string;
|
||||
handleDownloadFile: (fileUrl: string) => void;
|
||||
};
|
||||
const FileButton = ({ fileUrl, handleDownloadFile }: FileButtonProps) => {
|
||||
const readonly = fileUrl.includes('file://');
|
||||
return (
|
||||
<div className="flex items-center gap-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={readonly}
|
||||
onClick={() => handleDownloadFile(fileUrl)}
|
||||
className="flex items-center gap-2 p-2 max-h-[20px] text-xs"
|
||||
>
|
||||
{readonly ? (
|
||||
<EyeOff className="w-4 h-4" />
|
||||
) : (
|
||||
<Eye className="w-4 h-4" />
|
||||
)}
|
||||
{t('Download File')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const removeDoubleQuotes = (str: string): string =>
|
||||
str.startsWith('"') && str.endsWith('"') ? str.slice(1, -1) : str;
|
||||
|
||||
const removeUndefined = (obj: any): any => {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(removeUndefined);
|
||||
} else if (typeof obj === 'object' && obj !== null) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj)
|
||||
.filter(([_, value]) => value !== undefined)
|
||||
.map(([key, value]) => [key, removeUndefined(value)]),
|
||||
);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
const JsonViewer = React.memo(
|
||||
({
|
||||
json: unclearJson,
|
||||
title,
|
||||
hideDownload = false,
|
||||
className,
|
||||
}: JsonViewerProps) => {
|
||||
const { theme } = useTheme();
|
||||
const json = useMemo(() => {
|
||||
return removeUndefined(unclearJson);
|
||||
}, [unclearJson]);
|
||||
|
||||
const viewerTheme = theme === 'dark' ? 'bright' : 'rjv-default';
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(JSON.stringify(json, null, 2));
|
||||
toast.success(t('Copied to clipboard'), {
|
||||
duration: 1000,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
const blob = new Blob([JSON.stringify(json, null, 2)], {
|
||||
type: 'application/json',
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
handleDownloadFile(url);
|
||||
};
|
||||
|
||||
const handleDownloadFile = (fileUrl: string, ext = '') => {
|
||||
const link = document.createElement('a');
|
||||
link.href = fileUrl;
|
||||
link.download = `${title}${ext}`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(fileUrl);
|
||||
};
|
||||
useLayoutEffect(() => {
|
||||
if (typeof json === 'object') {
|
||||
const stringValuesHTML = Array.from(
|
||||
document.getElementsByClassName('string-value'),
|
||||
);
|
||||
|
||||
const stepFileUrlsHTML = stringValuesHTML.filter(
|
||||
(el) =>
|
||||
isStepFileUrl(el.innerHTML) ||
|
||||
isStepFileUrl(el.parentElement!.nextElementSibling?.innerHTML),
|
||||
);
|
||||
|
||||
stepFileUrlsHTML.forEach((el: Element) => {
|
||||
const fileUrl = removeDoubleQuotes(el.innerHTML)
|
||||
.trim()
|
||||
.replace('\n', '');
|
||||
el.className += ' hidden';
|
||||
|
||||
const rootElem = document.createElement('div');
|
||||
const root = createRoot(rootElem);
|
||||
|
||||
el.parentElement!.replaceChildren(el as Node, rootElem as Node);
|
||||
const isProductionFile = fileUrl.includes('file://');
|
||||
|
||||
root.render(
|
||||
<div data-file-root="true">
|
||||
{isProductionFile ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FileButton
|
||||
fileUrl={fileUrl}
|
||||
handleDownloadFile={handleDownloadFile}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
{t('File is not available after execution.')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<FileButton
|
||||
fileUrl={fileUrl}
|
||||
handleDownloadFile={handleDownloadFile}
|
||||
/>
|
||||
)}
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (isStepFileUrl(json)) {
|
||||
return (
|
||||
<FileButton fileUrl={json} handleDownloadFile={handleDownloadFile} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-lg border border-solid border-dividers overflow-hidden relative',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="px-3 py-2 flex border-solid border-b border-dividers justify-center items-center">
|
||||
<div className="grow justify-center items-center">
|
||||
<span className="text-md">{title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-0">
|
||||
{!hideDownload && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
size={'sm'}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{t('Download JSON')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant={'ghost'} size={'sm'} onClick={handleCopy}>
|
||||
<Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{t('Copy to clipboard')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
<>
|
||||
{isNil(json) ? (
|
||||
<pre className="text-sm whitespace-pre-wrap overflow-x-auto p-2">
|
||||
{json === null ? 'null' : 'undefined'}
|
||||
</pre>
|
||||
) : (
|
||||
<>
|
||||
{typeof json !== 'string' && typeof json !== 'object' && (
|
||||
<pre className="text-sm whitespace-pre-wrap break-all overflow-x-auto p-2">
|
||||
{JSON.stringify(json)}
|
||||
</pre>
|
||||
)}
|
||||
{typeof json === 'string' && (
|
||||
<pre className="text-sm whitespace-pre-wrap break-all overflow-x-auto p-2">
|
||||
{json}
|
||||
</pre>
|
||||
)}
|
||||
{typeof json === 'object' && (
|
||||
<div className="max-w-full">
|
||||
<ReactJson
|
||||
style={{
|
||||
overflowX: 'auto',
|
||||
padding: '0.5rem',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
theme={viewerTheme}
|
||||
enableClipboard={false}
|
||||
groupArraysAfterLength={20}
|
||||
displayDataTypes={false}
|
||||
name={false}
|
||||
quotesOnKeys={false}
|
||||
src={json}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
JsonViewer.displayName = 'JsonViewer';
|
||||
export { JsonViewer };
|
||||
@@ -0,0 +1,45 @@
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
type ShowPoweredByProps = {
|
||||
show: boolean;
|
||||
position?: 'sticky' | 'absolute' | 'static';
|
||||
};
|
||||
const ShowPoweredBy = ({ show, position = 'sticky' }: ShowPoweredByProps) => {
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cn('bottom-3 right-5 pointer-events-none z-10000', position, {
|
||||
'-mt-[30px]': position === 'sticky',
|
||||
'mr-5': position === 'sticky',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'justify-end p-1 text-muted-foreground/70 text-sm items-center flex gap-1 transition group ',
|
||||
{
|
||||
'justify-center': position === 'static',
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className=" text-sm transition">Built with</div>
|
||||
<div className="justify-center flex items-center gap-1">
|
||||
<svg
|
||||
width={15}
|
||||
height={15}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
className="transition fill-muted-foreground/70"
|
||||
>
|
||||
<path d="M6.46013 5.81759C5.30809 4.10962 5.75876 1.79113 7.46672 0.639093C9.17469 -0.512944 11.4932 -0.0622757 12.6452 1.64569L20.4261 13.1813C21.5781 14.8893 21.1274 17.2077 19.4195 18.3598C17.7115 19.5118 15.393 19.0611 14.241 17.3532L10.8676 12.3519C10.4339 11.8054 9.55114 11.8905 9.02108 12.4205C8.58152 12.8601 8.43761 13.9846 8.31301 14.9582C8.29474 15.1009 8.27689 15.2405 8.25858 15.3741C8.19097 16.0114 7.97092 16.6418 7.58762 17.2101C6.33511 19.067 3.81375 19.5565 1.95682 18.304C0.0998936 17.0515 -0.390738 14.5304 0.861776 12.6734C1.51136 11.7104 2.50224 11.1151 3.56472 10.9399L3.56322 10.9384C6.63307 10.4932 7.20222 7.02864 6.64041 6.08487L6.46013 5.81759Z" />
|
||||
</svg>
|
||||
<div className="font-semibold">activepieces</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ShowPoweredBy.displayName = 'ShowPoweredBy';
|
||||
export { ShowPoweredBy };
|
||||
@@ -0,0 +1,117 @@
|
||||
import { t } from 'i18next';
|
||||
import { Copy, Check } from 'lucide-react';
|
||||
import React, { useState } from 'react';
|
||||
import ReactJson from 'react-json-view';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useTheme } from './theme-provider';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
interface SimpleJsonViewerProps {
|
||||
data: any;
|
||||
readOnly?: boolean;
|
||||
hideCopyButton?: boolean;
|
||||
maxHeight?: number;
|
||||
}
|
||||
|
||||
export const SimpleJsonViewer: React.FC<SimpleJsonViewerProps> = ({
|
||||
data,
|
||||
readOnly = true,
|
||||
hideCopyButton = false,
|
||||
maxHeight = 400,
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const { theme } = useTheme();
|
||||
|
||||
const formattedJson =
|
||||
typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(formattedJson);
|
||||
setCopied(true);
|
||||
toast.success(t('Copied to clipboard'), {
|
||||
duration: 1000,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const viewerTheme = theme === 'dark' ? 'bright' : 'rjv-default';
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full relative text-foreground overflow-hidden"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
{!hideCopyButton && (
|
||||
<div className="absolute top-2 right-5 z-10">
|
||||
<Button
|
||||
variant="transparent"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
className="p-0 "
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<Copy
|
||||
className={`w-4 h-4 ${
|
||||
theme === 'dark' ? 'text-white' : 'text-black'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="p-2"
|
||||
style={{
|
||||
maxHeight: typeof maxHeight === 'number' ? `${maxHeight}px` : '400px',
|
||||
overflow: 'auto',
|
||||
overflowX: 'auto',
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
{typeof data === 'string' ? (
|
||||
<pre className="text-sm whitespace-pre-wrap break-all overflow-x-auto p-2">
|
||||
{data}
|
||||
</pre>
|
||||
) : (
|
||||
<div style={{ minWidth: 0, width: '100%', height: '100%' }}>
|
||||
<ReactJson
|
||||
style={{
|
||||
overflowX: 'auto',
|
||||
padding: '0.5rem',
|
||||
fontSize: '14px',
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
boxSizing: 'border-box',
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
theme={viewerTheme}
|
||||
enableClipboard={false}
|
||||
groupArraysAfterLength={20}
|
||||
displayDataTypes={false}
|
||||
name={false}
|
||||
quotesOnKeys={false}
|
||||
src={data}
|
||||
collapsed={false}
|
||||
displayObjectSize={false}
|
||||
iconStyle="triangle"
|
||||
shouldCollapse={false}
|
||||
onEdit={readOnly ? false : undefined}
|
||||
onAdd={readOnly ? false : undefined}
|
||||
onDelete={readOnly ? false : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
import { io } from 'socket.io-client';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { API_BASE_URL } from '@/lib/api';
|
||||
import { authenticationSession } from '@/lib/authentication-session';
|
||||
|
||||
const socket = io(API_BASE_URL, {
|
||||
transports: ['websocket'],
|
||||
path: '/api/socket.io',
|
||||
autoConnect: false,
|
||||
reconnection: true,
|
||||
});
|
||||
|
||||
const SocketContext = React.createContext<typeof socket>(socket);
|
||||
|
||||
export const SocketProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const token = authenticationSession.getToken();
|
||||
const toastIdRef = useRef<string | null>(null);
|
||||
|
||||
useEffectOnce(() => {
|
||||
if (token) {
|
||||
socket.auth = { token };
|
||||
if (!socket.connected) {
|
||||
socket.connect();
|
||||
|
||||
socket.on('connect', () => {
|
||||
if (toastIdRef.current) {
|
||||
toast.dismiss(toastIdRef.current);
|
||||
toastIdRef.current = null;
|
||||
}
|
||||
console.log('connected to socket');
|
||||
});
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
if (!toastIdRef.current) {
|
||||
const id = toast('Connection Lost', {
|
||||
id: 'websocket-disconnected',
|
||||
description: 'We are trying to reconnect...',
|
||||
duration: Infinity,
|
||||
});
|
||||
toastIdRef.current = id?.toString() ?? null;
|
||||
}
|
||||
if (reason === 'io server disconnect') {
|
||||
socket.connect();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
socket.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<SocketContext.Provider value={socket}>{children}</SocketContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useSocket = () => React.useContext(SocketContext);
|
||||
@@ -0,0 +1,143 @@
|
||||
import { AnalyticsBrowser } from '@segment/analytics-next';
|
||||
import posthog from 'posthog-js';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useDeepCompareEffect } from 'react-use';
|
||||
|
||||
import { flagsHooks } from '@/hooks/flags-hooks';
|
||||
import { userHooks } from '@/hooks/user-hooks';
|
||||
import {
|
||||
ApFlagId,
|
||||
isNil,
|
||||
TelemetryEvent,
|
||||
UserWithMetaInformationAndProject,
|
||||
} from '@activepieces/shared';
|
||||
|
||||
interface TelemetryProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const TelemetryProvider = ({ children }: TelemetryProviderProps) => {
|
||||
const { data: currentUser } = userHooks.useCurrentUser();
|
||||
const [analytics, setAnalytics] = useState<AnalyticsBrowser | null>(null);
|
||||
const initializedUserEmail = useRef<string | null>(null);
|
||||
|
||||
const [user, setUser] = useState<UserWithMetaInformationAndProject | null>(
|
||||
currentUser ?? null,
|
||||
);
|
||||
const { data: telemetryEnabled } = flagsHooks.useFlag<boolean>(
|
||||
ApFlagId.TELEMETRY_ENABLED,
|
||||
);
|
||||
const { data: flagCurrentVersion } = flagsHooks.useFlag<string>(
|
||||
ApFlagId.CURRENT_VERSION,
|
||||
);
|
||||
const { data: flagEnvironment } = flagsHooks.useFlag<string>(
|
||||
ApFlagId.ENVIRONMENT,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleStorageChange = (_event: StorageEvent) => {
|
||||
setUser(currentUser ?? null);
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
if (isNil(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (telemetryEnabled && user?.email !== initializedUserEmail.current) {
|
||||
initTelemetry();
|
||||
}
|
||||
}, [telemetryEnabled, user]);
|
||||
|
||||
const initTelemetry = () => {
|
||||
if (isNil(user)) {
|
||||
return;
|
||||
}
|
||||
console.log('Telemetry enabled');
|
||||
const newAnalytics = AnalyticsBrowser.load({
|
||||
writeKey: 'Znobm6clOFLZNdMFpZ1ncf6VDmlCVSmj',
|
||||
});
|
||||
|
||||
newAnalytics.addSourceMiddleware(({ payload, next }) => {
|
||||
const path = payload?.obj?.properties?.['path'];
|
||||
const ignoredPaths = ['/embed'];
|
||||
if (ignoredPaths.includes(path)) {
|
||||
return;
|
||||
}
|
||||
next(payload);
|
||||
});
|
||||
|
||||
const currentVersion = flagCurrentVersion || '0.0.0';
|
||||
const environment = flagEnvironment || '0.0.0';
|
||||
|
||||
newAnalytics.identify(user.id, {
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
activepiecesVersion: currentVersion,
|
||||
activepiecesEnvironment: environment,
|
||||
ui: 'react',
|
||||
});
|
||||
|
||||
newAnalytics.ready(() => {
|
||||
posthog.init('phc_7F92HoXJPeGnTKmYv0eOw62FurPMRW9Aqr0TPrDzvHh', {
|
||||
autocapture: false,
|
||||
capture_pageview: false,
|
||||
segment: (window as any).analytics,
|
||||
loaded: () => newAnalytics.page(),
|
||||
});
|
||||
|
||||
posthog.identify(user.id, {
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
activepiecesVersion: currentVersion,
|
||||
activepiecesEnvironment: environment,
|
||||
});
|
||||
});
|
||||
setAnalytics(newAnalytics);
|
||||
initializedUserEmail.current = user.email;
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
if (analytics) {
|
||||
analytics.reset();
|
||||
}
|
||||
posthog.reset();
|
||||
console.log('Telemetry removed');
|
||||
initializedUserEmail.current = null;
|
||||
};
|
||||
|
||||
const capture = (event: TelemetryEvent) => {
|
||||
if (telemetryEnabled && analytics) {
|
||||
analytics.track(event.name, event.payload);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TelemetryContext.Provider value={{ capture, reset }}>
|
||||
{children}
|
||||
</TelemetryContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
interface TelemetryContextType {
|
||||
capture: (event: TelemetryEvent) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const TelemetryContext = React.createContext<TelemetryContextType>({
|
||||
capture: () => {},
|
||||
reset: () => {},
|
||||
});
|
||||
|
||||
export const useTelemetry = () => React.useContext(TelemetryContext);
|
||||
|
||||
export default TelemetryProvider;
|
||||
@@ -0,0 +1,127 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import * as RippleHook from 'use-ripple-hook';
|
||||
|
||||
import { flagsHooks } from '@/hooks/flags-hooks';
|
||||
import { colorsUtils } from '@/lib/color-util';
|
||||
|
||||
type Theme = 'dark' | 'light' | 'system';
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
};
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: 'system',
|
||||
setTheme: () => null,
|
||||
};
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
const setFavicon = (url: string) => {
|
||||
let link: HTMLLinkElement | null =
|
||||
document.querySelector("link[rel*='icon']");
|
||||
if (!link) {
|
||||
link = document.createElement('link');
|
||||
link.rel = 'shortcut icon';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
link.href = url;
|
||||
};
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = 'system',
|
||||
storageKey = 'ap-ui-theme',
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
|
||||
);
|
||||
const branding = flagsHooks.useWebsiteBranding();
|
||||
useEffect(() => {
|
||||
if (!branding) {
|
||||
console.warn('Website brand is not defined');
|
||||
return;
|
||||
}
|
||||
const root = window.document.documentElement;
|
||||
|
||||
const resolvedTheme = theme === 'system' ? 'light' : theme;
|
||||
root.classList.remove('light', 'dark');
|
||||
document.title = branding.websiteName;
|
||||
document.documentElement.style.setProperty(
|
||||
'--primary',
|
||||
colorsUtils.hexToHslString(branding.colors.primary.default),
|
||||
);
|
||||
|
||||
setFavicon(branding.logos.favIconUrl);
|
||||
switch (resolvedTheme) {
|
||||
case 'light': {
|
||||
document.documentElement.style.setProperty(
|
||||
'--primary-100',
|
||||
colorsUtils.hexToHslString(branding.colors.primary.light),
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--primary-300',
|
||||
colorsUtils.hexToHslString(branding.colors.primary.dark),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'dark': {
|
||||
document.documentElement.style.setProperty(
|
||||
'--primary-100',
|
||||
colorsUtils.hexToHslString(branding.colors.primary.dark),
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--primary-300',
|
||||
colorsUtils.hexToHslString(branding.colors.primary.light),
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
root.classList.add(resolvedTheme);
|
||||
}, [theme, branding]);
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
|
||||
if (context === undefined)
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useApRipple = () => {
|
||||
const { theme } = useTheme();
|
||||
return RippleHook.default({
|
||||
color:
|
||||
theme === 'dark'
|
||||
? 'rgba(233, 233, 233, 0.2)'
|
||||
: 'rgba(155, 155, 155, 0.2)',
|
||||
cancelAutomatically: true,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
'use client';
|
||||
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Accordion = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Root>) => {
|
||||
return (
|
||||
<AccordionPrimitive.Root
|
||||
className={cn('rounded-md border', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item ref={ref} className={cn(className)} {...props} />
|
||||
));
|
||||
AccordionItem.displayName = 'AccordionItem';
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-1 px-4 py-3 items-center justify-between text-sm font-medium transition-all text-left [&[data-state=open]>svg]:rotate-180',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'overflow-hidden px-3 pb-3 text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
@@ -0,0 +1,63 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const alertVariants = cva(
|
||||
'flex relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-background text-foreground',
|
||||
warning:
|
||||
'border-warning/50 text-warning-300 dark:border-warning [&>svg]:text-warning',
|
||||
destructive:
|
||||
'border-destructive/50 text-destructive-300 dark:border-destructive [&>svg]:text-destructive',
|
||||
primary:
|
||||
'border-primary/50 text-primary bg-primary-100/10 dark:border-primary [&>svg]:text-primary',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Alert.displayName = 'Alert';
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertTitle.displayName = 'AlertTitle';
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-sm [&_p]:leading-relaxed', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDescription.displayName = 'AlertDescription';
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
@@ -0,0 +1,107 @@
|
||||
import { t } from 'i18next';
|
||||
import { Check } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from './command';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from './popover';
|
||||
import { ScrollArea } from './scroll-area';
|
||||
|
||||
type Props<T extends string> = {
|
||||
selectedValue: T;
|
||||
onSelectedValueChange: (value: T) => void;
|
||||
items: { value: T; label: string }[];
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
listRef?: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export function AutoComplete<T extends string>({
|
||||
selectedValue,
|
||||
onSelectedValueChange,
|
||||
items,
|
||||
children,
|
||||
className,
|
||||
open,
|
||||
setOpen,
|
||||
listRef,
|
||||
}: Props<T>) {
|
||||
const onSelectItem = (inputValue: string) => {
|
||||
onSelectedValueChange(inputValue as T);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
asChild
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
onInteractOutside={(e) => {
|
||||
if (
|
||||
e.target instanceof Element &&
|
||||
e.target.hasAttribute('cmdk-input')
|
||||
) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
className="w-(--radix-popover-trigger-width) p-0"
|
||||
>
|
||||
<Command className={className} ref={listRef}>
|
||||
<CommandList className="bg-background">
|
||||
<ScrollArea
|
||||
className={cn('', {
|
||||
'h-50': items.length >= 5,
|
||||
'h-10': items.length === 1,
|
||||
'h-20': items.length === 2,
|
||||
'h-30': items.length === 3,
|
||||
'h-40': items.length === 4,
|
||||
})}
|
||||
>
|
||||
{items.length > 0 ? (
|
||||
<CommandGroup>
|
||||
{items.map((option) => (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onSelect={onSelectItem}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'h-4 w-4',
|
||||
selectedValue === option.value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
{option.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : (
|
||||
<CommandEmpty>{t('No items')}</CommandEmpty>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn('aspect-square h-full w-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-full w-full items-center justify-center rounded-full border-none',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
||||
@@ -0,0 +1,35 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'border-transparent bg-primary text-primary-foreground',
|
||||
accent: 'border-transparent bg-accent text-accent-foreground',
|
||||
destructive: 'border-transparent bg-destructive-100',
|
||||
outline: 'text-foreground',
|
||||
ghost: 'border-none',
|
||||
success: 'bg-success-100',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { ChevronRight, MoreHorizontal } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Breadcrumb = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.ComponentPropsWithoutRef<'nav'> & {
|
||||
separator?: React.ReactNode;
|
||||
}
|
||||
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
|
||||
Breadcrumb.displayName = 'Breadcrumb';
|
||||
|
||||
const BreadcrumbList = React.forwardRef<
|
||||
HTMLOListElement,
|
||||
React.ComponentPropsWithoutRef<'ol'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-wrap items-center gap-1.5 wrap-break-word text-sm text-muted-foreground sm:gap-2.5',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbList.displayName = 'BreadcrumbList';
|
||||
|
||||
const BreadcrumbItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentPropsWithoutRef<'li'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
className={cn('inline-flex items-center gap-1.5', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbItem.displayName = 'BreadcrumbItem';
|
||||
|
||||
const BreadcrumbLink = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentPropsWithoutRef<'a'> & {
|
||||
asChild?: boolean;
|
||||
}
|
||||
>(({ asChild, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'a';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn('transition-colors hover:text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
BreadcrumbLink.displayName = 'BreadcrumbLink';
|
||||
|
||||
const BreadcrumbPage = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.ComponentPropsWithoutRef<'span'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn('font-normal text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbPage.displayName = 'BreadcrumbPage';
|
||||
|
||||
const BreadcrumbSeparator = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'li'>) => (
|
||||
<li
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn('[&>svg]:w-3.5 [&>svg]:h-3.5', className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
);
|
||||
BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
|
||||
|
||||
const BreadcrumbEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'span'>) => (
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn('flex h-9 w-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
};
|
||||
144
activepieces-fork/packages/react-ui/src/components/ui/button.tsx
Normal file
144
activepieces-fork/packages/react-ui/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { Shortcut } from './shortcut';
|
||||
import { LoadingSpinner } from './spinner';
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-hidden focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary stroke-background text-primary-foreground enabled:hover:bg-primary/90',
|
||||
basic:
|
||||
'text-primary font-medium underline-offset-4 enabled:hover:bg-accent',
|
||||
secondary:
|
||||
'text-secondary-foreground bg-secondary enabled:hover:bg-secondary/80 enabled:hover:text-secondary-foreground',
|
||||
destructive:
|
||||
'bg-destructive text-primary-foreground enabled:hover:bg-destructive/90',
|
||||
outline:
|
||||
'border-input bg-background enabled:hover:bg-accent enabled:hover:text-accent-foreground border',
|
||||
accent: 'bg-accent text-accent-foreground enabled:hover:bg-accent/80',
|
||||
ghost:
|
||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
transparent: 'text-primary enabled:hover:bg-transparent',
|
||||
'outline-primary':
|
||||
'text-primary font-medium enabled:hover:bg-primary/10 enabled:hover:border-primary enabled:hover:font-semibold',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
xs: 'h-6 px-2 text-xs py-2',
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
variant: 'link',
|
||||
class: 'px-0',
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
loading?: boolean;
|
||||
keyboardShortcut?: string;
|
||||
onKeyboardShortcut?: () => void;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
loading = false,
|
||||
keyboardShortcut,
|
||||
disabled,
|
||||
onKeyboardShortcut,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
|
||||
const isMac = /(Mac)/i.test(navigator.userAgent);
|
||||
const isEscape = keyboardShortcut?.toLocaleLowerCase() === 'esc';
|
||||
React.useEffect(() => {
|
||||
if (keyboardShortcut) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [keyboardShortcut, disabled]);
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const isEscapePressed = event.key === 'Escape' && isEscape;
|
||||
const isCtrlWithShortcut =
|
||||
keyboardShortcut &&
|
||||
event.key === keyboardShortcut.toLocaleLowerCase() &&
|
||||
(isMac ? event.metaKey : event.ctrlKey);
|
||||
if (isEscapePressed || isCtrlWithShortcut) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (onKeyboardShortcut && !disabled) {
|
||||
onKeyboardShortcut();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }), {})}
|
||||
ref={ref}
|
||||
disabled={disabled || loading}
|
||||
{...props}
|
||||
onClick={(e) => {
|
||||
loading ? e.stopPropagation() : props.onClick && props.onClick(e);
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingSpinner
|
||||
className={cn('size-5', {
|
||||
'stroke-background':
|
||||
variant === 'default' || variant === 'secondary',
|
||||
'stroke-foreground':
|
||||
variant !== 'default' && variant !== 'secondary',
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{keyboardShortcut && (
|
||||
<div className="flex justify-center items-center gap-2">
|
||||
{children}
|
||||
<Shortcut shortcutKey={keyboardShortcut} withCtrl={true} />
|
||||
</div>
|
||||
)}
|
||||
{!keyboardShortcut && children}
|
||||
</>
|
||||
)}
|
||||
</Comp>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -0,0 +1,68 @@
|
||||
'use client';
|
||||
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import { DayPicker } from 'react-day-picker';
|
||||
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
|
||||
// TODO: upgrade to react-day-picker v9 when https://github.com/shadcn-ui/ui/pull/4421 is merged
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: CalendarProps) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn('p-3', className)}
|
||||
classNames={{
|
||||
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
|
||||
month: 'space-y-4',
|
||||
caption: 'flex justify-center pt-1 relative items-center',
|
||||
caption_label: 'text-sm font-medium',
|
||||
nav: 'space-x-1 flex items-center',
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||
),
|
||||
nav_button_previous: 'absolute left-1',
|
||||
nav_button_next: 'absolute right-1',
|
||||
table: 'w-full border-collapse space-y-1',
|
||||
head_row: 'flex',
|
||||
head_cell:
|
||||
'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]',
|
||||
row: 'flex w-full mt-2',
|
||||
cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
|
||||
day: cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'h-9 w-9 p-0 font-normal aria-selected:opacity-100',
|
||||
),
|
||||
day_range_end: 'day-range-end',
|
||||
day_selected:
|
||||
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
|
||||
day_today: 'bg-accent text-accent-foreground',
|
||||
day_outside:
|
||||
'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
|
||||
day_disabled: 'text-muted-foreground opacity-50',
|
||||
day_range_middle:
|
||||
'aria-selected:bg-accent aria-selected:text-accent-foreground',
|
||||
day_hidden: 'invisible',
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: () => <ChevronLeft className="h-4 w-4" />,
|
||||
IconRight: () => <ChevronRight className="h-4 w-4" />,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Calendar.displayName = 'Calendar';
|
||||
|
||||
export { Calendar };
|
||||
@@ -0,0 +1,97 @@
|
||||
import { cva, VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const cardVariants = cva('rounded-lg border bg-background text-foreground', {
|
||||
variants: {
|
||||
variant: {
|
||||
default: ' shadow-xs',
|
||||
interactive:
|
||||
'cursor-pointer hover:border-gray-400 transition-colors duration-200 flex flex-col justify-between',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
});
|
||||
|
||||
interface CardProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof cardVariants> {}
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
||||
({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(cardVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Card.displayName = 'Card';
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('font-semibold leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = 'CardDescription';
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = 'CardContent';
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center p-6 pt-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
@@ -0,0 +1,262 @@
|
||||
'use client';
|
||||
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from '@radix-ui/react-icons';
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from 'embla-carousel-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useCarousel must be used within a <Carousel />');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const Carousel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
orientation = 'horizontal',
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === 'horizontal' ? 'x' : 'y',
|
||||
},
|
||||
plugins,
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect(api);
|
||||
api.on('reInit', onSelect);
|
||||
api.on('select', onSelect);
|
||||
|
||||
return () => {
|
||||
api?.off('select', onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn('relative', className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
Carousel.displayName = 'Carousel';
|
||||
|
||||
const CarouselContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div ref={carouselRef} className="overflow-hidden">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex',
|
||||
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
CarouselContent.displayName = 'CarouselContent';
|
||||
|
||||
const CarouselItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
className={cn(
|
||||
'min-w-0 shrink-0 grow-0 basis-full',
|
||||
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
CarouselItem.displayName = 'CarouselItem';
|
||||
|
||||
const CarouselPrevious = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
'absolute h-8 w-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? '-left-12 top-1/2 -translate-y-1/2'
|
||||
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeftIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
CarouselPrevious.displayName = 'CarouselPrevious';
|
||||
|
||||
const CarouselNext = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
'absolute h-8 w-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? '-right-12 top-1/2 -translate-y-1/2'
|
||||
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRightIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
CarouselNext.displayName = 'CarouselNext';
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
};
|
||||
365
activepieces-fork/packages/react-ui/src/components/ui/chart.tsx
Normal file
365
activepieces-fork/packages/react-ui/src/components/ui/chart.tsx
Normal file
@@ -0,0 +1,365 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import * as RechartsPrimitive from 'recharts';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: '', dark: '.dark' } as const;
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
);
|
||||
};
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig;
|
||||
};
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useChart must be used within a <ChartContainer />');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const ChartContainer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'> & {
|
||||
config: ChartConfig;
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>['children'];
|
||||
}
|
||||
>(({ id, className, children, config, ...props }, ref) => {
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-chart={chartId}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
});
|
||||
ChartContainer.displayName = 'Chart';
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([_, config]) => config.theme || config.color,
|
||||
);
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color;
|
||||
return color ? ` --color-${key}: ${color};` : null;
|
||||
})
|
||||
.join('\n')}
|
||||
}
|
||||
`,
|
||||
)
|
||||
.join('\n'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
|
||||
const ChartTooltipContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<'div'> & {
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: 'line' | 'dot' | 'dashed';
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = 'dot',
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { config } = useChart();
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [item] = payload;
|
||||
const key = `${labelKey || item.dataKey || item.name || 'value'}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const value =
|
||||
!labelKey && typeof label === 'string'
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label;
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn('font-medium', labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={cn('font-medium', labelClassName)}>{value}</div>;
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
]);
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== 'dot';
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || 'value'}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
'flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground',
|
||||
indicator === 'dot' && 'items-center',
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
'shrink-0 rounded-[2px] border-border bg-(--color-bg)',
|
||||
{
|
||||
'h-2.5 w-2.5': indicator === 'dot',
|
||||
'w-1': indicator === 'line',
|
||||
'w-0 border-[1.5px] border-dashed bg-transparent':
|
||||
indicator === 'dashed',
|
||||
'my-0.5': nestLabel && indicator === 'dashed',
|
||||
},
|
||||
)}
|
||||
style={
|
||||
{
|
||||
'--color-bg': indicatorColor,
|
||||
'--color-border': indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-1 justify-between leading-none',
|
||||
nestLabel ? 'items-end' : 'items-center',
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
ChartTooltipContent.displayName = 'ChartTooltip';
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'> &
|
||||
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey },
|
||||
ref,
|
||||
) => {
|
||||
const { config } = useChart();
|
||||
|
||||
if (!payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex items-center justify-center gap-4',
|
||||
verticalAlign === 'top' ? 'pb-3' : 'pt-3',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || 'value'}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
ChartLegendContent.displayName = 'ChartLegend';
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string,
|
||||
) {
|
||||
if (typeof payload !== 'object' || payload === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
'payload' in payload &&
|
||||
typeof payload.payload === 'object' &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined;
|
||||
|
||||
let configLabelKey: string = key;
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === 'string'
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string;
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config];
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { CheckIcon, MinusIcon } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const checkboxVariants = cva(
|
||||
'peer border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary:
|
||||
'data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground data-[state=indeterminate]:border-primary',
|
||||
secondary:
|
||||
'data-[state=checked]:bg-secondary data-[state=checked]:text-secondary-foreground dark:data-[state=checked]:bg-secondary data-[state=checked]:border-secondary data-[state=indeterminate]:bg-secondary data-[state=indeterminate]:text-secondary-foreground data-[state=indeterminate]:border-secondary',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface CheckboxProps
|
||||
extends React.ComponentProps<typeof CheckboxPrimitive.Root>,
|
||||
VariantProps<typeof checkboxVariants> {}
|
||||
|
||||
function Checkbox({ className, variant, checked, ...props }: CheckboxProps) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
checked={checked}
|
||||
className={cn(checkboxVariants({ variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none"
|
||||
>
|
||||
{checked === 'indeterminate' ? (
|
||||
<MinusIcon className="size-3.5" />
|
||||
) : (
|
||||
<CheckIcon className="size-3.5" />
|
||||
)}
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Checkbox, checkboxVariants };
|
||||
export type { CheckboxProps };
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import { forwardRef, useMemo, useState } from 'react';
|
||||
import { HexColorPicker } from 'react-colorful';
|
||||
|
||||
import type { ButtonProps } from '@/components/ui/button';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { useForwardedRef, cn } from '@/lib/utils';
|
||||
|
||||
interface ColorPickerProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
const ColorPicker = forwardRef<
|
||||
HTMLInputElement,
|
||||
Omit<ButtonProps, 'value' | 'onChange' | 'onBlur'> & ColorPickerProps
|
||||
>(
|
||||
(
|
||||
{ disabled, value, onChange, onBlur, name, className, ...props },
|
||||
forwardedRef,
|
||||
) => {
|
||||
const ref = useForwardedRef(forwardedRef);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const parsedValue = useMemo(() => {
|
||||
return value || '#FFFFFF';
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Popover onOpenChange={setOpen} open={open}>
|
||||
<PopoverTrigger asChild disabled={disabled} onBlur={onBlur}>
|
||||
<Button
|
||||
{...props}
|
||||
className={cn('block rounded-full', className)}
|
||||
name={name}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
size="icon"
|
||||
style={{
|
||||
backgroundColor: parsedValue,
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<div />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full">
|
||||
<HexColorPicker color={parsedValue} onChange={onChange} />
|
||||
<Input
|
||||
maxLength={7}
|
||||
onChange={(e) => {
|
||||
onChange(e?.currentTarget?.value);
|
||||
}}
|
||||
ref={ref}
|
||||
value={parsedValue}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
},
|
||||
);
|
||||
ColorPicker.displayName = 'ColorPicker';
|
||||
|
||||
export { ColorPicker };
|
||||
@@ -0,0 +1,160 @@
|
||||
import { type DialogProps } from '@radix-ui/react-dialog';
|
||||
import { Command as CommandPrimitive } from 'cmdk';
|
||||
import { Search } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-full w-full flex-col overflow-hidden rounded-md bg-background text-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
type CommandDialogProps = DialogProps;
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="**:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 **:[[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:size-5 **:[[cmdk-input]]:h-12 **:[[cmdk-item]]:px-2 **:[[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:size-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
// eslint-disable-next-line react/no-unknown-property -- temp fix while using wrong cmdk package
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground',
|
||||
{ 'cursor-not-allowed opacity-50': props.disabled },
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'max-h-[300px] overflow-y-hidden overflow-x-hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 h-px bg-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, disabled, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden aria-selected:bg-accent aria-selected:text-accent-foreground',
|
||||
{ 'pointer-events-none opacity-50': disabled },
|
||||
className,
|
||||
)}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'ml-auto text-xs tracking-widest text-muted-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CommandShortcut.displayName = 'CommandShortcut';
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
@@ -0,0 +1,200 @@
|
||||
'use client';
|
||||
|
||||
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
|
||||
import { Check, ChevronRight, Circle } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const ContextMenu = ContextMenuPrimitive.Root;
|
||||
|
||||
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
||||
|
||||
const ContextMenuGroup = ContextMenuPrimitive.Group;
|
||||
|
||||
const ContextMenuPortal = ContextMenuPrimitive.Portal;
|
||||
|
||||
const ContextMenuSub = ContextMenuPrimitive.Sub;
|
||||
|
||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
||||
|
||||
const ContextMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
));
|
||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const ContextMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'z-50 min-w-32 overflow-hidden rounded-md border bg-background p-1 text-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const ContextMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'z-50 min-w-32 overflow-hidden rounded-md border bg-background p-1 text-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
));
|
||||
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
|
||||
|
||||
const ContextMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
|
||||
|
||||
const ContextMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
ContextMenuCheckboxItem.displayName =
|
||||
ContextMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const ContextMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-4 w-4 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
));
|
||||
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const ContextMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'px-2 py-1.5 text-sm font-semibold text-foreground',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
|
||||
|
||||
const ContextMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
|
||||
|
||||
const ContextMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'ml-auto text-xs tracking-widest text-muted-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
ContextMenuShortcut.displayName = 'ContextMenuShortcut';
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuGroup,
|
||||
ContextMenuPortal,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
interface DataTableBulkActionsProps<TData> {
|
||||
selectedRows: TData[];
|
||||
actions: Array<{
|
||||
render: (
|
||||
selectedRows: TData[],
|
||||
resetSelection: () => void,
|
||||
) => React.ReactNode;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function DataTableBulkActions<TData>({
|
||||
selectedRows,
|
||||
actions,
|
||||
}: DataTableBulkActionsProps<TData>) {
|
||||
return (
|
||||
<div className="flex items-center justify-center space-x-2 ">
|
||||
{actions.map((action, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{action.render(selectedRows, () => {})}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type DataTableCheckboxProps = {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
handleCheckedChange: (checked: boolean) => void;
|
||||
};
|
||||
|
||||
export function DataTableInputCheckbox({
|
||||
label,
|
||||
checked,
|
||||
handleCheckedChange,
|
||||
}: DataTableCheckboxProps) {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'flex items-center space-x-2 border-dashed rounded-md px-3 py-2 h-9',
|
||||
'hover:bg-accent/5',
|
||||
checked && 'bg-accent/10 border-accent text-accent-foreground',
|
||||
)}
|
||||
onClick={() => handleCheckedChange(!checked)}
|
||||
>
|
||||
<Checkbox checked={checked} className="pointer-events-none" />
|
||||
<Label className="text-sm font-medium leading-none select-none cursor-pointer">
|
||||
{label}
|
||||
</Label>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Column } from '@tanstack/react-table';
|
||||
|
||||
interface DataTableColumnHeaderProps<TData, TValue>
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
column: Column<TData, TValue>;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function DataTableColumnHeader<TData, TValue>({
|
||||
title,
|
||||
className,
|
||||
}: DataTableColumnHeaderProps<TData, TValue>) {
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-start space-x-2 py-4 ${className}`}
|
||||
>
|
||||
<div className="text-sm font-semibold text-black dark:text-white">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { Column } from '@tanstack/react-table';
|
||||
import * as React from 'react';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { DateTimePickerWithRange, PresetKey } from '../date-time-picker-range';
|
||||
|
||||
import { DataTableInputCheckbox } from './data-table-checkbox-filter';
|
||||
import { DataTableInputPopover } from './data-table-input-popover';
|
||||
import { DataTableSelectPopover } from './data-table-select-popover';
|
||||
|
||||
import { CURSOR_QUERY_PARAM } from '.';
|
||||
|
||||
type DropdownFilterProps = {
|
||||
type: 'select';
|
||||
options: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: React.ComponentType<{ className?: string }> | string;
|
||||
}[];
|
||||
};
|
||||
|
||||
type InputFilterProps = {
|
||||
type: 'input';
|
||||
};
|
||||
type DateFilterProps = {
|
||||
type: 'date';
|
||||
defaultPresetName?: PresetKey;
|
||||
};
|
||||
type CheckboxjhFilterProps = {
|
||||
type: 'checkbox';
|
||||
};
|
||||
|
||||
export type DataTableFilterProps = {
|
||||
title?: string;
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
} & (
|
||||
| DropdownFilterProps
|
||||
| InputFilterProps
|
||||
| DateFilterProps
|
||||
| CheckboxjhFilterProps
|
||||
);
|
||||
|
||||
export function DataTableFilter<TData, TValue>({
|
||||
title,
|
||||
column,
|
||||
...props
|
||||
}: DataTableFilterProps & { column?: Column<TData, TValue> }) {
|
||||
const facets = column?.getFacetedUniqueValues();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const handleFilterChange = React.useCallback(
|
||||
(filterValue: string | string[] | DateRange | undefined) => {
|
||||
setSearchParams(
|
||||
(prev) => {
|
||||
const newParams = new URLSearchParams(prev);
|
||||
newParams.delete(column?.id as string);
|
||||
newParams.delete(`${column?.id}After`);
|
||||
newParams.delete(`${column?.id}Before`);
|
||||
newParams.delete(CURSOR_QUERY_PARAM);
|
||||
if (!filterValue) {
|
||||
return newParams;
|
||||
}
|
||||
|
||||
if (Array.isArray(filterValue)) {
|
||||
filterValue.forEach((value) =>
|
||||
newParams.append(column?.id as string, value),
|
||||
);
|
||||
} else if (typeof filterValue === 'object' && filterValue !== null) {
|
||||
if (filterValue.from) {
|
||||
newParams.append(
|
||||
`${column?.id}After`,
|
||||
filterValue.from.toISOString(),
|
||||
);
|
||||
}
|
||||
if (filterValue.to) {
|
||||
newParams.append(
|
||||
`${column?.id}Before`,
|
||||
filterValue.to.toISOString(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
newParams.append(column?.id as string, filterValue);
|
||||
}
|
||||
|
||||
return newParams;
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
|
||||
if (Array.isArray(filterValue)) {
|
||||
column?.setFilterValue(filterValue.length ? filterValue : undefined);
|
||||
} else if (typeof filterValue === 'object' && filterValue !== null) {
|
||||
column?.setFilterValue(
|
||||
filterValue.from || filterValue.to ? filterValue : undefined,
|
||||
);
|
||||
} else {
|
||||
column?.setFilterValue(filterValue ? filterValue : undefined);
|
||||
}
|
||||
},
|
||||
[column, setSearchParams],
|
||||
);
|
||||
|
||||
switch (props.type) {
|
||||
case 'input': {
|
||||
const filterValue = searchParams.get(column?.id as string) || '';
|
||||
return (
|
||||
<DataTableInputPopover
|
||||
title={title}
|
||||
filterValue={filterValue}
|
||||
handleFilterChange={handleFilterChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'select': {
|
||||
const filterValue = searchParams.getAll(column?.id as string) as string[];
|
||||
const selectedValues = new Set(filterValue);
|
||||
return (
|
||||
<DataTableSelectPopover
|
||||
title={title}
|
||||
selectedValues={selectedValues}
|
||||
options={props.options}
|
||||
handleFilterChange={handleFilterChange}
|
||||
facets={facets}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'date': {
|
||||
const from = searchParams.get(`${column?.id}After`);
|
||||
const to = searchParams.get(`${column?.id}Before`);
|
||||
|
||||
return (
|
||||
<DateTimePickerWithRange
|
||||
defaultSelectedRange={props.defaultPresetName}
|
||||
presetType="past"
|
||||
onChange={handleFilterChange}
|
||||
from={from ?? undefined}
|
||||
to={to ?? undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'checkbox': {
|
||||
const key = column?.id || 'archivedAt';
|
||||
const isArchived = searchParams.get(key) === 'true';
|
||||
|
||||
const handleCheckedChange = (checked: boolean) => {
|
||||
setSearchParams(
|
||||
(prev) => {
|
||||
const newParams = new URLSearchParams(prev);
|
||||
newParams.delete(key);
|
||||
newParams.delete(CURSOR_QUERY_PARAM);
|
||||
if (checked) {
|
||||
newParams.append(key, 'true');
|
||||
}
|
||||
return newParams;
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
|
||||
column?.setFilterValue(
|
||||
checked
|
||||
? (row: any) => row.getValue('archivedAt') !== null
|
||||
: undefined,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<DataTableInputCheckbox
|
||||
label={title ?? 'Archived'}
|
||||
checked={isArchived}
|
||||
handleCheckedChange={handleCheckedChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { SearchIcon } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
import { Badge } from '../badge';
|
||||
import { Button } from '../button';
|
||||
import { Input } from '../input';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../popover';
|
||||
import { Separator } from '../separator';
|
||||
|
||||
type DataTableInputPopoverProps = {
|
||||
title?: string;
|
||||
filterValue: string;
|
||||
handleFilterChange: (filterValue: string) => void;
|
||||
};
|
||||
|
||||
const DataTableInputPopover = ({
|
||||
title,
|
||||
filterValue,
|
||||
handleFilterChange,
|
||||
}: DataTableInputPopoverProps) => {
|
||||
const [searchQuery, setSearchQuery] = useState(filterValue);
|
||||
const [debouncedQuery] = useDebounce(searchQuery, 300);
|
||||
|
||||
useEffect(() => {
|
||||
handleFilterChange(debouncedQuery);
|
||||
}, [debouncedQuery]);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="border-dashed">
|
||||
<SearchIcon className="mr-2 size-4" />
|
||||
{title}
|
||||
{filterValue.length > 0 && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="mx-2 h-4" />
|
||||
<Badge
|
||||
variant="accent"
|
||||
className="rounded-sm px-1 font-normal max-w-40 truncate"
|
||||
>
|
||||
{filterValue}
|
||||
</Badge>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export { DataTableInputPopover };
|
||||
@@ -0,0 +1,161 @@
|
||||
import { CheckIcon, ListFilterIcon } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { Badge } from '../badge';
|
||||
import { Button } from '../button';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from '../command';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../popover';
|
||||
import { ScrollArea } from '../scroll-area';
|
||||
import { Separator } from '../separator';
|
||||
|
||||
type DataTableSelectPopoverProps = {
|
||||
title?: string;
|
||||
selectedValues: Set<string>;
|
||||
options: readonly {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: React.ComponentType<{ className?: string }> | string;
|
||||
}[];
|
||||
facets?: Map<any, number>;
|
||||
handleFilterChange: (filterValue: string[]) => void;
|
||||
};
|
||||
|
||||
const DataTableSelectPopover = ({
|
||||
title,
|
||||
selectedValues,
|
||||
options,
|
||||
handleFilterChange,
|
||||
facets,
|
||||
}: DataTableSelectPopoverProps) => {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="border-dashed">
|
||||
<ListFilterIcon className="mr-2 size-4" />
|
||||
{title}
|
||||
{selectedValues?.size > 0 && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="mx-2 h-4" />
|
||||
<Badge
|
||||
variant="accent"
|
||||
className="rounded-sm px-1 font-normal lg:hidden"
|
||||
>
|
||||
{selectedValues.size}
|
||||
</Badge>
|
||||
<div className="hidden space-x-1 lg:flex">
|
||||
{selectedValues.size > 2 ? (
|
||||
<Badge
|
||||
variant="accent"
|
||||
className="rounded-sm px-1 font-normal"
|
||||
>
|
||||
{selectedValues.size} selected
|
||||
</Badge>
|
||||
) : (
|
||||
options
|
||||
.filter((option) => selectedValues.has(option.value))
|
||||
.map((option) => (
|
||||
<Badge
|
||||
variant="accent"
|
||||
key={option.value}
|
||||
className="rounded-sm px-1 font-normal"
|
||||
>
|
||||
{option.label}
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="min-w-[200px] max-w-[250px] break-all p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder={title} />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
<ScrollArea viewPortClassName="max-h-[200px]">
|
||||
{options.map((option, index) => {
|
||||
const isSelected = selectedValues.has(option.value);
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
onSelect={() => {
|
||||
if (isSelected) {
|
||||
selectedValues.delete(option.value);
|
||||
} else {
|
||||
selectedValues.add(option.value);
|
||||
}
|
||||
const filterValues = Array.from(selectedValues);
|
||||
handleFilterChange(filterValues);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded border border-secondary',
|
||||
isSelected
|
||||
? 'bg-secondary text-secondary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible',
|
||||
)}
|
||||
>
|
||||
<CheckIcon className={cn('h-4 w-4')} />
|
||||
</div>
|
||||
{typeof option.icon === 'string' ? (
|
||||
<img
|
||||
src={option.icon}
|
||||
alt={option.label}
|
||||
className="mr-2 size-4 object-contain"
|
||||
/>
|
||||
) : (
|
||||
option.icon && (
|
||||
<option.icon className="mr-2 size-4 text-muted-foreground" />
|
||||
)
|
||||
)}
|
||||
<div>
|
||||
<span>{option.label}</span>
|
||||
<span className="hidden">{index}</span>
|
||||
</div>
|
||||
{facets?.get(option.value) && (
|
||||
<span className="ml-auto flex size-4 items-center justify-center font-mono text-xs">
|
||||
{facets.get(option.value)}
|
||||
</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</ScrollArea>
|
||||
</CommandGroup>
|
||||
{selectedValues.size > 0 && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => handleFilterChange([])}
|
||||
className="justify-center text-center"
|
||||
>
|
||||
Clear filters
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export { DataTableSelectPopover };
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
export function DataTableSkeleton({
|
||||
skeletonRowCount = 10,
|
||||
}: {
|
||||
skeletonRowCount?: number;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="p-2">
|
||||
{Array.from({ length: skeletonRowCount }).map((_, rowIndex) => (
|
||||
<TableRowSkeleton key={rowIndex} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TableRowSkeleton() {
|
||||
return (
|
||||
<div
|
||||
id="table-loading"
|
||||
className="w-full h-10 bg-gray-100 mb-4 dark:bg-gray-800 rounded-sm"
|
||||
data-testid="header-cell"
|
||||
>
|
||||
<Skeleton className="w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
type DataTableToolbarProps = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const DataTableToolbar = (params: DataTableToolbarProps) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between pb-4 overflow-auto">
|
||||
<div className="flex flex-1 items-center space-x-2">
|
||||
{params.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
DataTableToolbar.displayName = 'DataTableToolbar';
|
||||
|
||||
export { DataTableToolbar };
|
||||
@@ -0,0 +1,461 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
ColumnDef as TanstackColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { t } from 'i18next';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useDeepCompareEffect } from 'react-use';
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { apId, isNil, SeekPage } from '@activepieces/shared';
|
||||
|
||||
import { Button } from '../button';
|
||||
import { Checkbox } from '../checkbox';
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from '../select';
|
||||
|
||||
import { DataTableBulkActions } from './data-table-bulk-actions';
|
||||
import { DataTableColumnHeader } from './data-table-column-header';
|
||||
import { DataTableFilter, DataTableFilterProps } from './data-table-filter';
|
||||
import { DataTableSkeleton } from './data-table-skeleton';
|
||||
import { DataTableToolbar } from './data-table-toolbar';
|
||||
|
||||
export type DataWithId = {
|
||||
id?: string;
|
||||
};
|
||||
export type RowDataWithActions<TData extends DataWithId> = TData & {
|
||||
delete: () => void;
|
||||
update: (payload: Partial<TData>) => void;
|
||||
};
|
||||
|
||||
export const CURSOR_QUERY_PARAM = 'cursor';
|
||||
export const LIMIT_QUERY_PARAM = 'limit';
|
||||
|
||||
type DataTableAction<TData extends DataWithId> = (
|
||||
row: RowDataWithActions<TData>,
|
||||
) => JSX.Element;
|
||||
|
||||
type ColumnDef<TData, TValue> = TanstackColumnDef<TData, TValue> & {
|
||||
notClickable?: boolean;
|
||||
};
|
||||
|
||||
interface DataTableProps<
|
||||
TData extends DataWithId,
|
||||
TValue,
|
||||
Keys extends string,
|
||||
> {
|
||||
columns: ColumnDef<RowDataWithActions<TData>, TValue>[];
|
||||
page: SeekPage<TData> | undefined;
|
||||
onRowClick?: (
|
||||
row: RowDataWithActions<TData>,
|
||||
newWindow: boolean,
|
||||
e: React.MouseEvent<HTMLTableRowElement, MouseEvent>,
|
||||
) => void;
|
||||
isLoading: boolean;
|
||||
filters?: DataTableFilters<Keys>[];
|
||||
customFilters?: React.ReactNode[];
|
||||
onSelectedRowsChange?: (rows: RowDataWithActions<TData>[]) => void;
|
||||
actions?: DataTableAction<TData>[];
|
||||
hidePagination?: boolean;
|
||||
bulkActions?: BulkAction<TData>[];
|
||||
emptyStateTextTitle: string;
|
||||
emptyStateTextDescription: string;
|
||||
emptyStateIcon: React.ReactNode;
|
||||
selectColumn?: boolean;
|
||||
}
|
||||
|
||||
export type DataTableFilters<Keys extends string> = DataTableFilterProps & {
|
||||
accessorKey: Keys;
|
||||
};
|
||||
|
||||
export type BulkAction<TData extends DataWithId> = {
|
||||
render: (
|
||||
selectedRows: RowDataWithActions<TData>[],
|
||||
resetSelection: () => void,
|
||||
) => React.ReactNode;
|
||||
};
|
||||
|
||||
export function DataTable<
|
||||
TData extends DataWithId,
|
||||
TValue,
|
||||
Keys extends string,
|
||||
>({
|
||||
columns: columnsInitial,
|
||||
page,
|
||||
onRowClick,
|
||||
filters = [],
|
||||
actions = [],
|
||||
isLoading,
|
||||
onSelectedRowsChange,
|
||||
hidePagination,
|
||||
bulkActions = [],
|
||||
emptyStateTextTitle,
|
||||
emptyStateTextDescription,
|
||||
emptyStateIcon,
|
||||
customFilters,
|
||||
selectColumn = false,
|
||||
}: DataTableProps<TData, TValue, Keys>) {
|
||||
const selectColumnDef: ColumnDef<RowDataWithActions<TData>, TValue> = {
|
||||
id: 'select',
|
||||
accessorKey: 'select',
|
||||
notClickable: true,
|
||||
header: ({ table }) => (
|
||||
<div className="flex items-center h-full">
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center h-full">
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
const columnsWithSelect = selectColumn
|
||||
? [selectColumnDef, ...columnsInitial]
|
||||
: columnsInitial;
|
||||
|
||||
const columns =
|
||||
actions.length > 0
|
||||
? columnsWithSelect.concat([
|
||||
{
|
||||
accessorKey: '__actions',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex justify-end gap-4">
|
||||
{actions.map((action, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
{action(row.original)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
])
|
||||
: columnsWithSelect;
|
||||
|
||||
const columnVisibility = columnsInitial.reduce((acc, column) => {
|
||||
if (column.enableHiding && 'accessorKey' in column) {
|
||||
acc[column.accessorKey as string] = false;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const startingCursor = searchParams.get('cursor') || undefined;
|
||||
const startingLimit = searchParams.get('limit') || '10';
|
||||
const [currentCursor, setCurrentCursor] = useState<string | undefined>(
|
||||
startingCursor,
|
||||
);
|
||||
const [nextPageCursor, setNextPageCursor] = useState<string | undefined>(
|
||||
page?.next ?? undefined,
|
||||
);
|
||||
const [previousPageCursor, setPreviousPageCursor] = useState<
|
||||
string | undefined
|
||||
>(page?.previous ?? undefined);
|
||||
|
||||
const enrichPageData = (data: TData[]) => {
|
||||
return data.map((row, index) => ({
|
||||
...row,
|
||||
delete: () => {
|
||||
setDeletedRows((prevDeletedRows) => [...prevDeletedRows, row]);
|
||||
},
|
||||
update: (payload: Partial<TData>) => {
|
||||
setTableData((prevData) => {
|
||||
const newData = [...prevData];
|
||||
newData[index] = { ...newData[index], ...payload };
|
||||
return newData;
|
||||
});
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const [deletedRows, setDeletedRows] = useState<TData[]>([]);
|
||||
const [tableData, setTableData] = useState<RowDataWithActions<TData>[]>(
|
||||
enrichPageData(page?.data ?? []),
|
||||
);
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
setNextPageCursor(page?.next ?? undefined);
|
||||
setPreviousPageCursor(page?.previous ?? undefined);
|
||||
setTableData(enrichPageData(page?.data ?? []));
|
||||
}, [page?.data]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: tableData,
|
||||
columns,
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowId: () => apId(),
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: parseInt(startingLimit),
|
||||
},
|
||||
columnVisibility,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
filters?.forEach((filter) => {
|
||||
const column = table.getColumn(filter.accessorKey);
|
||||
const values = searchParams.getAll(filter.accessorKey);
|
||||
if (column && values) {
|
||||
column.setFilterValue(values);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
onSelectedRowsChange?.(
|
||||
table.getSelectedRowModel().rows.map((row) => row.original),
|
||||
);
|
||||
}, [table.getSelectedRowModel().rows]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchParams(
|
||||
(prev) => {
|
||||
const newParams = new URLSearchParams(prev);
|
||||
|
||||
newParams.set('cursor', currentCursor ?? '');
|
||||
newParams.set('limit', `${table.getState().pagination.pageSize}`);
|
||||
return newParams;
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
}, [currentCursor, table.getState().pagination.pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableData(
|
||||
tableData.filter(
|
||||
(row) => !deletedRows.some((deletedRow) => deletedRow.id === row.id),
|
||||
),
|
||||
);
|
||||
}, [deletedRows]);
|
||||
|
||||
const resetSelection = () => {
|
||||
table.toggleAllRowsSelected(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{((filters && filters.length > 0) || bulkActions.length > 0) && (
|
||||
<DataTableToolbar>
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
{filters &&
|
||||
filters.map((filter) => (
|
||||
<DataTableFilter
|
||||
key={filter.accessorKey}
|
||||
column={table.getColumn(filter.accessorKey)}
|
||||
{...filter}
|
||||
/>
|
||||
))}
|
||||
{customFilters &&
|
||||
customFilters.map((filter, idx) => (
|
||||
<React.Fragment key={idx}>{filter}</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
{bulkActions.length > 0 && (
|
||||
<DataTableBulkActions
|
||||
selectedRows={table
|
||||
.getSelectedRowModel()
|
||||
.rows.map((row) => row.original)}
|
||||
actions={bulkActions.map((action) => ({
|
||||
render: (selectedRows: RowDataWithActions<TData>[]) =>
|
||||
action.render(selectedRows, resetSelection),
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DataTableToolbar>
|
||||
)}
|
||||
|
||||
<div className="rounded-md mt-0 overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="hover:bg-transparent">
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow className="hover:bg-background">
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
<DataTableSkeleton />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
className={cn('cursor-pointer', {
|
||||
'hover:bg-background cursor-default': isNil(onRowClick),
|
||||
})}
|
||||
onClick={(e) => {
|
||||
// Check if the clicked cell is not clickable
|
||||
const clickedCellIndex = (e.target as HTMLElement).closest(
|
||||
'td',
|
||||
)?.cellIndex;
|
||||
if (
|
||||
clickedCellIndex !== undefined &&
|
||||
columns[clickedCellIndex]?.notClickable
|
||||
) {
|
||||
return; // Don't trigger onRowClick for not clickable columns
|
||||
}
|
||||
onRowClick?.(row.original, e.ctrlKey, e);
|
||||
}}
|
||||
onAuxClick={(e) => {
|
||||
// Similar check for auxiliary click (e.g., middle mouse button)
|
||||
const clickedCellIndex = (e.target as HTMLElement).closest(
|
||||
'td',
|
||||
)?.cellIndex;
|
||||
if (
|
||||
clickedCellIndex !== undefined &&
|
||||
columns[clickedCellIndex]?.notClickable
|
||||
) {
|
||||
return;
|
||||
}
|
||||
onRowClick?.(row.original, true, e);
|
||||
}}
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
<div
|
||||
className={cn('flex items-center', {
|
||||
'justify-end': cell.column.id === 'actions',
|
||||
'justify-start': cell.column.id !== 'actions',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
if (cell.column.id === 'select') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow className="hover:bg-background">
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-[350px] text-center"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
{emptyStateIcon ? emptyStateIcon : <></>}
|
||||
<p className="text-lg font-semibold">
|
||||
{emptyStateTextTitle}
|
||||
</p>
|
||||
{emptyStateTextDescription && (
|
||||
<p className="text-sm text-muted-foreground ">
|
||||
{emptyStateTextDescription}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{!hidePagination && (
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<p className="text-sm font-medium">Rows per page</p>
|
||||
<Select
|
||||
value={`${table.getState().pagination.pageSize}`}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value));
|
||||
setCurrentCursor(undefined);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-9 min-w-[70px] w-auto">
|
||||
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
||||
</SelectTrigger>
|
||||
<SelectContent side="top">
|
||||
{[10, 30, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||
{pageSize}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentCursor(previousPageCursor)}
|
||||
disabled={!previousPageCursor}
|
||||
>
|
||||
{t('Previous')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setCurrentCursor(nextPageCursor);
|
||||
}}
|
||||
disabled={!nextPageCursor}
|
||||
>
|
||||
{t('Next')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
import { format, subDays, addDays, startOfDay, endOfDay } from 'date-fns';
|
||||
import { t } from 'i18next';
|
||||
import { Calendar as CalendarIcon, Clock } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { Separator } from './separator';
|
||||
import { TimePicker } from './time-picker';
|
||||
|
||||
export type PresetKey =
|
||||
| '7days'
|
||||
| '14days'
|
||||
| '30days'
|
||||
| '90days'
|
||||
| '7'
|
||||
| '14'
|
||||
| '30'
|
||||
| '90';
|
||||
|
||||
type DateTimePickerWithRangeProps = {
|
||||
onChange: (date: DateRange | undefined) => void;
|
||||
className?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
maxDate?: Date;
|
||||
minDate?: Date;
|
||||
presetType: 'past' | 'future';
|
||||
defaultSelectedRange?: PresetKey;
|
||||
};
|
||||
|
||||
const applyTimeToDate = (timeDate: Date, targetDate: Date): Date => {
|
||||
const d = new Date(targetDate);
|
||||
d.setHours(
|
||||
timeDate.getHours(),
|
||||
timeDate.getMinutes(),
|
||||
timeDate.getSeconds(),
|
||||
timeDate.getMilliseconds(),
|
||||
);
|
||||
return d;
|
||||
};
|
||||
|
||||
const getDayBoundaries = () => {
|
||||
const now = new Date();
|
||||
return {
|
||||
from: startOfDay(now),
|
||||
to: endOfDay(now),
|
||||
};
|
||||
};
|
||||
|
||||
const PRESETS: Record<PresetKey, () => { from: Date; to: Date }> = {
|
||||
'7days': () => ({ from: subDays(new Date(), 7), to: new Date() }),
|
||||
'14days': () => ({ from: subDays(new Date(), 14), to: new Date() }),
|
||||
'30days': () => ({ from: subDays(new Date(), 30), to: new Date() }),
|
||||
'90days': () => ({ from: subDays(new Date(), 90), to: new Date() }),
|
||||
'7': () => ({ from: new Date(), to: addDays(new Date(), 7) }),
|
||||
'14': () => ({ from: new Date(), to: addDays(new Date(), 14) }),
|
||||
'30': () => ({ from: new Date(), to: addDays(new Date(), 30) }),
|
||||
'90': () => ({ from: new Date(), to: addDays(new Date(), 90) }),
|
||||
};
|
||||
|
||||
const getPresetLabel = (value: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
'7days': t('Last 7 Days'),
|
||||
'14days': t('Last 14 Days'),
|
||||
'30days': t('Last 30 Days'),
|
||||
'90days': t('Last 90 Days'),
|
||||
'7': t('Next 7 days'),
|
||||
'14': t('Next 14 days'),
|
||||
'30': t('Next 30 days'),
|
||||
'90': t('Next 90 days'),
|
||||
};
|
||||
return labels[value] || '';
|
||||
};
|
||||
|
||||
const detectPreset = (
|
||||
from?: Date,
|
||||
to?: Date,
|
||||
presetType?: 'past' | 'future',
|
||||
): string | null => {
|
||||
if (!from || !to) return null;
|
||||
|
||||
const candidates =
|
||||
presetType === 'past'
|
||||
? (['7days', '14days', '30days', '90days'] as PresetKey[])
|
||||
: (['7', '14', '30', '90'] as PresetKey[]);
|
||||
|
||||
for (const key of candidates) {
|
||||
const { from: pf, to: pt } = PRESETS[key]();
|
||||
if (
|
||||
startOfDay(pf).getTime() === startOfDay(from).getTime() &&
|
||||
endOfDay(pt).getTime() === endOfDay(to).getTime()
|
||||
) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getDefaultRange = (presetKey: PresetKey) => {
|
||||
const preset = PRESETS[presetKey]();
|
||||
preset.from!.setHours(0, 0, 0, 0);
|
||||
preset.to!.setHours(23, 59, 59, 999);
|
||||
return preset;
|
||||
};
|
||||
|
||||
const getInitialDateAndPreset = (
|
||||
fromProp?: string,
|
||||
toProp?: string,
|
||||
presetType: 'past' | 'future' = 'past',
|
||||
defaultPresetKey?: PresetKey,
|
||||
): { initialDate: DateRange | undefined; initialPreset: string | null } => {
|
||||
let initialDate: DateRange | undefined;
|
||||
let initialPreset: string | null = null;
|
||||
|
||||
if (fromProp && toProp) {
|
||||
initialDate = {
|
||||
from: new Date(fromProp),
|
||||
to: new Date(toProp),
|
||||
};
|
||||
initialPreset = detectPreset(initialDate.from, initialDate.to, presetType);
|
||||
} else if (defaultPresetKey) {
|
||||
initialDate = getDefaultRange(defaultPresetKey);
|
||||
initialPreset = defaultPresetKey;
|
||||
}
|
||||
|
||||
return { initialDate, initialPreset };
|
||||
};
|
||||
|
||||
export function DateTimePickerWithRange({
|
||||
className,
|
||||
onChange,
|
||||
from,
|
||||
to,
|
||||
maxDate = new Date(),
|
||||
minDate,
|
||||
presetType = 'past',
|
||||
defaultSelectedRange,
|
||||
}: DateTimePickerWithRangeProps) {
|
||||
const { initialDate, initialPreset } = React.useMemo(() => {
|
||||
return getInitialDateAndPreset(from, to, presetType, defaultSelectedRange);
|
||||
}, [from, to, presetType, defaultSelectedRange]);
|
||||
|
||||
const [date, setDate] = React.useState<DateRange | undefined>(initialDate);
|
||||
const [timeDate, setTimeDate] = React.useState<DateRange>({
|
||||
from: initialDate?.from,
|
||||
to: initialDate?.to,
|
||||
});
|
||||
const [selectedPreset, setSelectedPreset] = React.useState<string | null>(
|
||||
initialPreset,
|
||||
);
|
||||
|
||||
const isDefaultApplied = React.useRef(!!initialPreset && !from && !to);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isDefaultApplied.current && date) {
|
||||
onChange(date);
|
||||
isDefaultApplied.current = false;
|
||||
}
|
||||
}, [date, onChange]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (from && to) {
|
||||
const newDate: DateRange = { from: new Date(from), to: new Date(to) };
|
||||
setDate(newDate);
|
||||
setTimeDate({ from: newDate.from, to: newDate.to });
|
||||
const preset = detectPreset(newDate.from, newDate.to, presetType);
|
||||
setSelectedPreset(preset);
|
||||
} else if (!from && !to) {
|
||||
setDate(initialDate);
|
||||
setTimeDate({ from: initialDate?.from, to: initialDate?.to });
|
||||
setSelectedPreset(initialPreset);
|
||||
}
|
||||
}, [from, to, presetType, initialDate, initialPreset]);
|
||||
|
||||
const handleSelect = (selectedDate: DateRange | undefined) => {
|
||||
setSelectedPreset(null);
|
||||
if (!selectedDate) {
|
||||
setDate(undefined);
|
||||
onChange(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const newDate = {
|
||||
from: selectedDate.from
|
||||
? applyTimeToDate(
|
||||
timeDate.from || getDayBoundaries().from,
|
||||
selectedDate.from,
|
||||
)
|
||||
: undefined,
|
||||
to: selectedDate.to
|
||||
? applyTimeToDate(timeDate.to || getDayBoundaries().to, selectedDate.to)
|
||||
: undefined,
|
||||
};
|
||||
setDate(newDate);
|
||||
onChange(newDate);
|
||||
};
|
||||
|
||||
const handlePresetChange = (value: string) => {
|
||||
const newRange = PRESETS[value as PresetKey]();
|
||||
newRange.from!.setHours(0, 0, 0, 0);
|
||||
newRange.to!.setHours(23, 59, 59, 999);
|
||||
|
||||
setDate(newRange);
|
||||
setTimeDate({ from: newRange.from, to: newRange.to });
|
||||
setSelectedPreset(value);
|
||||
onChange(newRange);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('grid gap-2', className)}>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'min-w-[90px] border-dashed justify-start text-left font-normal',
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{selectedPreset ? (
|
||||
<span>{getPresetLabel(selectedPreset)}</span>
|
||||
) : date?.from ? (
|
||||
date.to ? (
|
||||
<div className="flex gap-2 items-center">
|
||||
<div>{format(date.from, 'LLL dd, y, hh:mm a')}</div>
|
||||
<div>{t('to')}</div>
|
||||
<div>{format(date.to, 'LLL dd, y, hh:mm a')}</div>
|
||||
</div>
|
||||
) : (
|
||||
format(date.from, 'LLL dd, y, hh:mm a')
|
||||
)
|
||||
) : (
|
||||
<span>{t('Pick a date range')}</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-2" align="start">
|
||||
<div className="flex space-x-2 mb-2">
|
||||
<Select
|
||||
onValueChange={handlePresetChange}
|
||||
value={selectedPreset || undefined}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('Select preset')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{presetType === 'past' ? (
|
||||
<>
|
||||
<SelectItem value="7days">{t('Last 7 Days')}</SelectItem>
|
||||
<SelectItem value="14days">{t('Last 14 Days')}</SelectItem>
|
||||
<SelectItem value="30days">{t('Last 30 Days')}</SelectItem>
|
||||
<SelectItem value="90days">{t('Last 90 Days')}</SelectItem>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SelectItem value="7">{t('Next 7 days')}</SelectItem>
|
||||
<SelectItem value="14">{t('Next 14 days')}</SelectItem>
|
||||
<SelectItem value="30">{t('Next 30 days')}</SelectItem>
|
||||
<SelectItem value="90">{t('Next 90 days')}</SelectItem>
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={date?.from}
|
||||
selected={date}
|
||||
onSelect={handleSelect}
|
||||
numberOfMonths={2}
|
||||
weekStartsOn={1}
|
||||
toDate={maxDate}
|
||||
fromDate={minDate}
|
||||
/>
|
||||
|
||||
<Separator className="mb-4" />
|
||||
|
||||
<div className="flex gap-1.5 px-2 items-center text-sm mb-3">
|
||||
<Clock className="w-4 h-4 text-muted-foreground" />
|
||||
{t('Select Time Range')}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 items-center px-2 mb-2">
|
||||
<TimePicker
|
||||
date={timeDate.from}
|
||||
name="from"
|
||||
setDate={(fromTime) => {
|
||||
const fromWithTime = applyTimeToDate(
|
||||
fromTime,
|
||||
date?.from ?? new Date(),
|
||||
);
|
||||
const updated = { from: fromWithTime, to: date?.to };
|
||||
setDate(updated);
|
||||
setTimeDate({ ...timeDate, from: fromTime });
|
||||
setSelectedPreset(null);
|
||||
onChange(updated);
|
||||
}}
|
||||
/>
|
||||
{t('to')}
|
||||
<TimePicker
|
||||
date={timeDate.to}
|
||||
name="to"
|
||||
setDate={(toTime) => {
|
||||
const toWithTime = applyTimeToDate(
|
||||
toTime,
|
||||
date?.to ?? date?.from ?? new Date(),
|
||||
);
|
||||
const updated = { from: date?.from, to: toWithTime };
|
||||
setDate(updated);
|
||||
setTimeDate({ ...timeDate, to: toTime });
|
||||
setSelectedPreset(null);
|
||||
onChange(updated);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mt-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-primary hover:text-primary! w-full"
|
||||
onClick={() => {
|
||||
setDate(undefined);
|
||||
setTimeDate({ from: undefined, to: undefined });
|
||||
setSelectedPreset(null);
|
||||
onChange(undefined);
|
||||
}}
|
||||
>
|
||||
{t('Clear')}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
137
activepieces-fork/packages/react-ui/src/components/ui/dialog.tsx
Normal file
137
activepieces-fork/packages/react-ui/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { X } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
tabIndex={-1}
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 outline-hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||
withCloseButton?: boolean;
|
||||
showOverlay?: boolean;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
withCloseButton = true,
|
||||
showOverlay = true,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => (
|
||||
<DialogPortal>
|
||||
{showOverlay && <DialogOverlay />}
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed left-[50%] top-[50%] outline-hidden z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] g4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{withCloseButton && (
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
|
||||
<X className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
),
|
||||
);
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-1.5 gap-3 text-center sm:text-left mb-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = 'DialogHeader';
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 mt-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = 'DialogFooter';
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-lg font-semibold leading-none tracking-tight',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const dotVariants = cva('size-2 rounded-full', {
|
||||
variants: {
|
||||
variant: {
|
||||
destructive: 'bg-destructive',
|
||||
primary: 'bg-primary',
|
||||
},
|
||||
},
|
||||
defaultVariants: {},
|
||||
});
|
||||
|
||||
interface DotProps
|
||||
extends VariantProps<typeof dotVariants>,
|
||||
React.HTMLAttributes<HTMLDivElement> {
|
||||
animation?: boolean;
|
||||
}
|
||||
|
||||
const Dot = React.forwardRef<HTMLDivElement, DotProps>(
|
||||
({ className, animation = false, variant, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
dotVariants({ variant }),
|
||||
animation && 'animate-pulse',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Dot.displayName = 'Dot';
|
||||
|
||||
export { Dot };
|
||||
157
activepieces-fork/packages/react-ui/src/components/ui/drawer.tsx
Normal file
157
activepieces-fork/packages/react-ui/src/components/ui/drawer.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Drawer as DrawerPrimitive } from 'vaul';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface DrawerContentProps
|
||||
extends React.ComponentProps<typeof DrawerPrimitive.Content> {
|
||||
fullscreen?: boolean;
|
||||
}
|
||||
|
||||
function Drawer({
|
||||
onOpenChange,
|
||||
open,
|
||||
closeOnEscape = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root> & {
|
||||
closeOnEscape?: boolean;
|
||||
}) {
|
||||
React.useEffect(() => {
|
||||
if (!open || !onOpenChange || !closeOnEscape) return;
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && closeOnEscape) {
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [open, onOpenChange, closeOnEscape]);
|
||||
return (
|
||||
<DrawerPrimitive.Root
|
||||
data-slot="drawer"
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
className,
|
||||
children,
|
||||
fullscreen = false,
|
||||
...props
|
||||
}: DrawerContentProps) {
|
||||
return (
|
||||
<DrawerPortal data-slot="drawer-portal">
|
||||
{fullscreen && (
|
||||
<style>
|
||||
{`
|
||||
[data-vaul-drawer][data-vaul-drawer-direction="right"]::after {
|
||||
display: none !important;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
)}
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
className={cn(
|
||||
'group/drawer-content bg-background fixed z-50 flex h-auto flex-col shadow-lg outline-hidden select-text',
|
||||
'data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b',
|
||||
'data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t',
|
||||
'data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:shadow-[-10px_0_10px_-3px_rgba(0,0,0,0.1)]',
|
||||
'data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:shadow-[10px_0_10px_-3px_rgba(0,0,0,0.1)]',
|
||||
fullscreen && 'w-screen max-w-none',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{!fullscreen && (
|
||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
)}
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn(
|
||||
'flex flex-col border border-b gap-0.5 p-0 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn('text-foreground font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
};
|
||||
@@ -0,0 +1,240 @@
|
||||
'use client';
|
||||
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import { Check, ChevronRight, Circle } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:outline-hidden focus-visible:outline-hidden focus:bg-accent data-[state=open]:bg-accent',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'z-50 min-w-32 overflow-hidden rounded-md border bg-background p-1 text-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> & {
|
||||
/**This is needed because animation out changes the focus after the animation is done leading into race conditions
|
||||
i.e when an item is clicked, the menu closes and the item is focused, but the animation is not complete yet
|
||||
so the item is not focused and the menu is open
|
||||
so we need to disable the animation on out
|
||||
*/
|
||||
noAnimationOnOut?: boolean;
|
||||
}
|
||||
>(({ className, sideOffset = 4, noAnimationOnOut = false, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 min-w-32 overflow-hidden rounded-md border bg-background p-1 text-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
{ 'data-[state=closed]:animate-out ': !noAnimationOnOut },
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:outline-hidden focus-visible:outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:outline-hidden focus-visible:outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:outline-hidden focus-visible:outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'px-2 py-1.5 text-sm font-semibold',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
type DropdownMenuShortcutProps = React.HTMLAttributes<HTMLSpanElement> & {
|
||||
keyboardShortcut: string;
|
||||
onKeyboardShortcut: () => void;
|
||||
};
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
keyboardShortcut,
|
||||
onKeyboardShortcut,
|
||||
...props
|
||||
}: DropdownMenuShortcutProps) => {
|
||||
React.useEffect(() => {
|
||||
if (keyboardShortcut) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [keyboardShortcut]);
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === keyboardShortcut?.toLocaleLowerCase() &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (onKeyboardShortcut) {
|
||||
onKeyboardShortcut();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
|
||||
{...props}
|
||||
>
|
||||
⌘{keyboardShortcut.toString().toLocaleUpperCase()}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
|
||||
import { isNil } from '@activepieces/shared';
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip';
|
||||
|
||||
type EditableTextProps = {
|
||||
value: string | undefined;
|
||||
className?: string;
|
||||
readonly: boolean;
|
||||
onValueChange: (value: string) => void;
|
||||
tooltipContent?: string;
|
||||
disallowEditingOnClick?: boolean;
|
||||
isEditing: boolean;
|
||||
setIsEditing: (isEditing: boolean) => void;
|
||||
};
|
||||
|
||||
const EditableText = ({
|
||||
value: initialValue,
|
||||
className = '',
|
||||
readonly = false,
|
||||
onValueChange,
|
||||
tooltipContent,
|
||||
disallowEditingOnClick,
|
||||
isEditing,
|
||||
setIsEditing,
|
||||
}: EditableTextProps) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const isEditingPreviousRef = useRef(false);
|
||||
const valueOnEditingStartedRef = useRef(initialValue);
|
||||
|
||||
if (value !== initialValue) {
|
||||
setValue(initialValue);
|
||||
}
|
||||
const editableTextRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const emitChangedValue = useCallback(() => {
|
||||
const nodeValue = (editableTextRef.current?.textContent ?? '').trim();
|
||||
const shouldUpdateValue =
|
||||
nodeValue.length > 0 && nodeValue !== valueOnEditingStartedRef.current;
|
||||
|
||||
setValue(shouldUpdateValue ? nodeValue : valueOnEditingStartedRef.current);
|
||||
if (shouldUpdateValue) {
|
||||
onValueChange(nodeValue);
|
||||
}
|
||||
}, [onValueChange, valueOnEditingStartedRef.current]);
|
||||
|
||||
const setSelectionToValue = () => {
|
||||
requestAnimationFrame(() => {
|
||||
if (
|
||||
editableTextRef.current &&
|
||||
window.getSelection &&
|
||||
document.createRange
|
||||
) {
|
||||
const range = document.createRange();
|
||||
const sel = window.getSelection();
|
||||
range.selectNodeContents(editableTextRef.current);
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (isEditing && !isEditingPreviousRef.current) {
|
||||
valueOnEditingStartedRef.current = value ? value.trim() : '';
|
||||
|
||||
setSelectionToValue();
|
||||
}
|
||||
isEditingPreviousRef.current = isEditing;
|
||||
|
||||
return !isEditing ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
disabled={
|
||||
readonly ||
|
||||
isEditing ||
|
||||
disallowEditingOnClick ||
|
||||
isNil(tooltipContent)
|
||||
}
|
||||
asChild
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!isEditing && !readonly && !disallowEditingOnClick) {
|
||||
setIsEditing(true);
|
||||
}
|
||||
}}
|
||||
ref={editableTextRef}
|
||||
key={'viewed'}
|
||||
className={`${className} truncate `}
|
||||
title={
|
||||
editableTextRef.current &&
|
||||
editableTextRef.current.scrollWidth >
|
||||
editableTextRef.current.clientWidth &&
|
||||
value
|
||||
? value
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{tooltipContent && (
|
||||
<TooltipContent className="font-normal z-50" side="bottom">
|
||||
{tooltipContent}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div
|
||||
key={'editable'}
|
||||
ref={editableTextRef}
|
||||
contentEditable
|
||||
suppressContentEditableWarning={true}
|
||||
className={`${className} focus:outline-hidden break-all`}
|
||||
onBlur={() => {
|
||||
emitChangedValue();
|
||||
setIsEditing(false);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setValue(valueOnEditingStartedRef.current);
|
||||
setIsEditing(false);
|
||||
} else if (event.key === 'Enter') {
|
||||
emitChangedValue();
|
||||
setIsEditing(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
EditableText.displayName = 'EditableText';
|
||||
export default EditableText;
|
||||
104
activepieces-fork/packages/react-ui/src/components/ui/empty.tsx
Normal file
104
activepieces-fork/packages/react-ui/src/components/ui/empty.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Empty({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty"
|
||||
className={cn(
|
||||
'flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-header"
|
||||
className={cn(
|
||||
'flex max-w-sm flex-col items-center gap-2 text-center',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMediaVariants = cva(
|
||||
'flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-transparent',
|
||||
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function EmptyMedia({
|
||||
className,
|
||||
variant = 'default',
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & VariantProps<typeof emptyMediaVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-icon"
|
||||
data-variant={variant}
|
||||
className={cn(emptyMediaVariants({ variant, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-title"
|
||||
className={cn('text-lg font-medium tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-description"
|
||||
className={cn(
|
||||
'text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-content"
|
||||
className={cn(
|
||||
'flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Empty,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
EmptyDescription,
|
||||
EmptyContent,
|
||||
EmptyMedia,
|
||||
};
|
||||
248
activepieces-fork/packages/react-ui/src/components/ui/field.tsx
Normal file
248
activepieces-fork/packages/react-ui/src/components/ui/field.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
'use client';
|
||||
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
|
||||
return (
|
||||
<fieldset
|
||||
data-slot="field-set"
|
||||
className={cn(
|
||||
'flex flex-col gap-6',
|
||||
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldLegend({
|
||||
className,
|
||||
variant = 'legend',
|
||||
...props
|
||||
}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
|
||||
return (
|
||||
<legend
|
||||
data-slot="field-legend"
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
'mb-3 font-medium',
|
||||
'data-[variant=legend]:text-base',
|
||||
'data-[variant=label]:text-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-group"
|
||||
className={cn(
|
||||
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const fieldVariants = cva(
|
||||
'group/field flex w-full gap-3 data-[invalid=true]:text-destructive',
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: ['flex-col *:w-full [&>.sr-only]:w-auto'],
|
||||
horizontal: [
|
||||
'flex-row items-center',
|
||||
'*:data-[slot=field-label]:flex-auto',
|
||||
'has-[>[data-slot=field-content]]:items-start [&>[role=checkbox],[role=radio]]:has-[>[data-slot=field-content]]:mt-px',
|
||||
],
|
||||
responsive: [
|
||||
'flex-col *:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto',
|
||||
'@md/field-group:*:data-[slot=field-label]:flex-auto',
|
||||
'@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:[&>[role=checkbox],[role=radio]]:has-[>[data-slot=field-content]]:mt-px',
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: 'vertical',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Field({
|
||||
className,
|
||||
orientation = 'vertical',
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="field"
|
||||
data-orientation={orientation}
|
||||
className={cn(fieldVariants({ orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-content"
|
||||
className={cn(
|
||||
'group/field-content flex flex-1 flex-col gap-1.5 leading-snug',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Label>) {
|
||||
return (
|
||||
<Label
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
|
||||
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border data-[slot=field]:*:p-4',
|
||||
'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
||||
return (
|
||||
<p
|
||||
data-slot="field-description"
|
||||
className={cn(
|
||||
'text-muted-foreground text-sm leading-normal font-normal group-has-data-[orientation=horizontal]/field:text-balance',
|
||||
'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
|
||||
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & {
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-separator"
|
||||
data-content={!!children}
|
||||
className={cn(
|
||||
'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Separator className="absolute inset-0 top-1/2" />
|
||||
{children && (
|
||||
<span
|
||||
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
||||
data-slot="field-separator-content"
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldError({
|
||||
className,
|
||||
children,
|
||||
errors,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & {
|
||||
errors?: Array<{ message?: string } | undefined>;
|
||||
}) {
|
||||
const content = useMemo(() => {
|
||||
if (children) {
|
||||
return children;
|
||||
}
|
||||
|
||||
if (!errors?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uniqueErrors = [
|
||||
...new Map(errors.map((error) => [error?.message, error])).values(),
|
||||
];
|
||||
|
||||
if (uniqueErrors?.length == 1) {
|
||||
return uniqueErrors[0]?.message;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
||||
{uniqueErrors.map(
|
||||
(error, index) =>
|
||||
error?.message && <li key={index}>{error.message}</li>,
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
}, [children, errors]);
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
data-slot="field-error"
|
||||
className={cn('text-destructive text-sm font-normal', className)}
|
||||
{...props}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldContent,
|
||||
FieldTitle,
|
||||
};
|
||||
196
activepieces-fork/packages/react-ui/src/components/ui/form.tsx
Normal file
196
activepieces-fork/packages/react-ui/src/components/ui/form.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
'use client';
|
||||
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue,
|
||||
);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState, formState } = useFormContext();
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error('useFormField should be used within <FormField>');
|
||||
}
|
||||
|
||||
const { id } = itemContext;
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
};
|
||||
};
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue,
|
||||
);
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn('space-y-1', className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
});
|
||||
FormItem.displayName = 'FormItem';
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && 'text-destructive', className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = 'FormLabel';
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormControl.displayName = 'FormControl';
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormDescription.displayName = 'FormDescription';
|
||||
const FormError = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement> & {
|
||||
formMessageId: string;
|
||||
}
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={props.formMessageId}
|
||||
className={cn(
|
||||
'text-sm font-medium text-destructive wrap-break-word',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
});
|
||||
FormError.displayName = 'FormError';
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message) : children;
|
||||
|
||||
if (!body) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormError formMessageId={formMessageId} {...props} ref={ref}>
|
||||
{body}
|
||||
</FormError>
|
||||
);
|
||||
});
|
||||
FormMessage.displayName = 'FormMessage';
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
FormError,
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { formatUtils } from '@/lib/utils';
|
||||
|
||||
type FormattedDateProps = {
|
||||
date: Date;
|
||||
includeTime?: boolean | undefined;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const FormattedDate = ({
|
||||
date,
|
||||
includeTime,
|
||||
className,
|
||||
}: FormattedDateProps) => {
|
||||
const formattedDate = formatUtils.formatDate(date);
|
||||
const formattedDateWithTime = formatUtils.formatDateWithTime(date, false);
|
||||
const fullDateTimeTooltip = formatUtils.formatDateWithTime(date, true);
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className={className}>
|
||||
{includeTime ? formattedDateWithTime : formattedDate}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{fullDateTimeTooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { t } from 'i18next';
|
||||
|
||||
import { flagsHooks } from '@/hooks/flags-hooks';
|
||||
|
||||
const FullLogo = () => {
|
||||
const branding = flagsHooks.useWebsiteBranding();
|
||||
|
||||
return (
|
||||
<div className="h-[60px]">
|
||||
<img
|
||||
className="h-full"
|
||||
src={branding.logos.fullLogoUrl}
|
||||
alt={t('logo')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
FullLogo.displayName = 'FullLogo';
|
||||
export { FullLogo };
|
||||
@@ -0,0 +1,75 @@
|
||||
import { t } from 'i18next';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { flagsHooks } from '@/hooks/flags-hooks';
|
||||
import { authenticationSession } from '@/lib/authentication-session';
|
||||
import { ActivepiecesClientEventName } from 'ee-embed-sdk';
|
||||
|
||||
import { useEmbedding } from '../embed-provider';
|
||||
|
||||
import { Button } from './button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip';
|
||||
|
||||
const HomeButtonWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const { embedState } = useEmbedding();
|
||||
if (embedState.emitHomeButtonClickedEvent) {
|
||||
const handleClick = () => {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: ActivepiecesClientEventName.CLIENT_BUILDER_HOME_BUTTON_CLICKED,
|
||||
data: {
|
||||
route: '/flows',
|
||||
},
|
||||
},
|
||||
'*',
|
||||
);
|
||||
};
|
||||
return <div onClick={handleClick}>{children}</div>;
|
||||
}
|
||||
return (
|
||||
<Link to={authenticationSession.appendProjectRoutePrefix('/flows')}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
const HomeButton = () => {
|
||||
const { embedState } = useEmbedding();
|
||||
const branding = flagsHooks.useWebsiteBranding();
|
||||
const showBackButton = embedState.homeButtonIcon === 'back';
|
||||
return (
|
||||
<>
|
||||
{!embedState.hideHomeButtonInBuilder && (
|
||||
<Tooltip>
|
||||
<HomeButtonWrapper>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size={'icon'}
|
||||
className={showBackButton ? 'size-8' : 'size-10'}
|
||||
>
|
||||
{!showBackButton && (
|
||||
<img
|
||||
className="h-5 w-5 object-contain"
|
||||
src={branding.logos.logoIconUrl}
|
||||
alt={branding.websiteName}
|
||||
/>
|
||||
)}
|
||||
{showBackButton && <ChevronLeft className="h-4 w-4" />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</HomeButtonWrapper>
|
||||
{!showBackButton && (
|
||||
<TooltipContent side="bottom">
|
||||
{t('Go to Dashboard')}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
HomeButton.displayName = 'HomeButton';
|
||||
|
||||
export { HomeButton };
|
||||
@@ -0,0 +1,131 @@
|
||||
import { FastAverageColor } from 'fast-average-color';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ImageWithColorBackgroundProps
|
||||
extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
fallback?: React.ReactNode;
|
||||
border?: boolean;
|
||||
}
|
||||
|
||||
const isGrayColor = (r: number, g: number, b: number): boolean => {
|
||||
const threshold = 15;
|
||||
const darkThreshold = 150;
|
||||
const lightThreshold = 225;
|
||||
|
||||
const isDark = r <= darkThreshold && g <= darkThreshold && b <= darkThreshold;
|
||||
const isLight =
|
||||
r >= lightThreshold && g >= lightThreshold && b >= lightThreshold;
|
||||
const diffRG = Math.abs(r - g);
|
||||
const diffRB = Math.abs(r - b);
|
||||
const diffGB = Math.abs(g - b);
|
||||
const isGray =
|
||||
diffRG <= threshold && diffRB <= threshold && diffGB <= threshold;
|
||||
return isDark || isLight || isGray;
|
||||
};
|
||||
|
||||
const ImageWithColorBackground = ({
|
||||
src,
|
||||
alt,
|
||||
fallback,
|
||||
...props
|
||||
}: ImageWithColorBackgroundProps) => {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [backgroundColor, setBackgroundColor] = useState<string | null>(null);
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!src) return;
|
||||
|
||||
const fac = new FastAverageColor();
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.src = src;
|
||||
|
||||
img.onload = () => {
|
||||
fac
|
||||
.getColorAsync(img, { algorithm: 'simple' })
|
||||
.then((color) => {
|
||||
const [r, g, b] = color.value;
|
||||
if (isGrayColor(r, g, b)) {
|
||||
setBackgroundColor(null);
|
||||
} else {
|
||||
setBackgroundColor(
|
||||
`color-mix(in srgb, rgb(${r},${g},${b}) 10%, #fff 92%)`,
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setBackgroundColor(null);
|
||||
});
|
||||
};
|
||||
|
||||
return () => {
|
||||
fac.destroy();
|
||||
};
|
||||
}, [src]);
|
||||
|
||||
const handleLoad = () => {
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
setHasError(true);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const { className, ...rest } = props;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'relative inline-block h-full w-full rounded-lg',
|
||||
className,
|
||||
{
|
||||
'bg-background': backgroundColor === null,
|
||||
'border border-border/50 dark:bg-foreground/10':
|
||||
backgroundColor === null && props.border,
|
||||
},
|
||||
)}
|
||||
style={
|
||||
backgroundColor
|
||||
? {
|
||||
backgroundColor: backgroundColor,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{isLoading && !hasError && (
|
||||
<span className="absolute inset-0 flex items-center justify-center">
|
||||
{fallback ?? <Skeleton className="w-full h-full" />}
|
||||
</span>
|
||||
)}
|
||||
{!hasError ? (
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={src}
|
||||
alt={alt}
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
className={cn(
|
||||
`transition-opacity duration-500 w-full h-full object-contain`,
|
||||
{
|
||||
'opacity-0': isLoading,
|
||||
'opacity-100': !isLoading,
|
||||
},
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
) : (
|
||||
<span className="absolute inset-0 flex items-center justify-center">
|
||||
{fallback ?? <Skeleton className="w-full h-full" />}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export { ImageWithColorBackground };
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ImageWithFallbackProps
|
||||
extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
fallback?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ImageWithFallback = ({
|
||||
src,
|
||||
alt,
|
||||
fallback,
|
||||
...props
|
||||
}: ImageWithFallbackProps) => {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const handleLoad = () => {
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
setHasError(true);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const { className, ...rest } = props;
|
||||
|
||||
return (
|
||||
<span className={cn('relative inline-block h-full w-full', className)}>
|
||||
{isLoading && !hasError && (
|
||||
<span className="absolute inset-0 flex items-center justify-center">
|
||||
{fallback ?? <Skeleton className="w-full h-full" />}
|
||||
</span>
|
||||
)}
|
||||
{!hasError ? (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
className={cn(
|
||||
`transition-opacity duration-500 w-full h-full object-cover`,
|
||||
{
|
||||
'opacity-0': isLoading,
|
||||
'opacity-100': !isLoading,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
) : (
|
||||
<span className="absolute inset-0 flex items-center justify-center">
|
||||
{fallback ?? <Skeleton className="w-full h-full" />}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageWithFallback;
|
||||
@@ -0,0 +1,75 @@
|
||||
import { t } from 'i18next';
|
||||
import { Paperclip } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import { useImperativeHandle } from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { SelectUtilButton } from '../custom/select-util-button';
|
||||
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
thin?: boolean;
|
||||
defaultFileName?: string;
|
||||
};
|
||||
|
||||
export const inputClass =
|
||||
'flex h-9 w-full rounded-md border border-input-border bg-background px-3 py-1 text-base shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm';
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, thin = false, defaultFileName, ...props }, ref) => {
|
||||
const [fileName, setFileName] = React.useState<string | null>(null);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
setFileName(file ? file.name : null);
|
||||
props.onChange?.(event);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => inputRef.current!);
|
||||
const handleDivClick = () => {
|
||||
inputRef.current?.click();
|
||||
};
|
||||
|
||||
return type === 'file' ? (
|
||||
<>
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
ref={inputRef}
|
||||
{...props}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<div
|
||||
onClick={handleDivClick}
|
||||
className={cn(inputClass, 'cursor-pointer items-center ', className)}
|
||||
>
|
||||
<input
|
||||
className={cn('grow cursor-pointer outline-hidden bg-transparent', {
|
||||
'text-muted-foreground': !fileName,
|
||||
})}
|
||||
value={fileName || defaultFileName || t('Select a file')}
|
||||
readOnly
|
||||
/>
|
||||
<div className="basis-1">
|
||||
<SelectUtilButton
|
||||
onClick={(e) => e.preventDefault()}
|
||||
tooltipText={fileName ? fileName : t('Select a file')}
|
||||
Icon={Paperclip}
|
||||
></SelectUtilButton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(inputClass, className, {
|
||||
'h-8 p-2': thin,
|
||||
})}
|
||||
ref={inputRef}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export { Input };
|
||||
193
activepieces-fork/packages/react-ui/src/components/ui/item.tsx
Normal file
193
activepieces-fork/packages/react-ui/src/components/ui/item.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
role="list"
|
||||
data-slot="item-group"
|
||||
className={cn('group/item-group flex flex-col', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="item-separator"
|
||||
orientation="horizontal"
|
||||
className={cn('my-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const itemVariants = cva(
|
||||
'group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-transparent',
|
||||
outline: 'border-border',
|
||||
muted: 'bg-muted/50',
|
||||
},
|
||||
size: {
|
||||
default: 'p-4 gap-4 ',
|
||||
sm: 'py-3 px-4 gap-2.5',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Item({
|
||||
className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> &
|
||||
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : 'div';
|
||||
return (
|
||||
<Comp
|
||||
data-slot="item"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(itemVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const itemMediaVariants = cva(
|
||||
'flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-transparent',
|
||||
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
|
||||
image:
|
||||
'size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function ItemMedia({
|
||||
className,
|
||||
variant = 'default',
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-media"
|
||||
data-variant={variant}
|
||||
className={cn(itemMediaVariants({ variant, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-content"
|
||||
className={cn(
|
||||
'flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-title"
|
||||
className={cn(
|
||||
'flex w-fit items-center gap-2 text-sm leading-snug font-medium',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
||||
return (
|
||||
<p
|
||||
data-slot="item-description"
|
||||
className={cn(
|
||||
'text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance',
|
||||
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemActions({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-actions"
|
||||
className={cn('flex items-center gap-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-header"
|
||||
className={cn(
|
||||
'flex basis-full items-center justify-between gap-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-footer"
|
||||
className={cn(
|
||||
'flex basis-full items-center justify-between gap-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Item,
|
||||
ItemMedia,
|
||||
ItemContent,
|
||||
ItemActions,
|
||||
ItemGroup,
|
||||
ItemSeparator,
|
||||
ItemTitle,
|
||||
ItemDescription,
|
||||
ItemHeader,
|
||||
ItemFooter,
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
@@ -0,0 +1,27 @@
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
import { LoadingSpinner } from './spinner';
|
||||
|
||||
type LoadingScreenProps = {
|
||||
brightSpinner?: boolean;
|
||||
mode?: 'fullscreen' | 'container';
|
||||
};
|
||||
export const LoadingScreen = ({
|
||||
brightSpinner = false,
|
||||
mode = 'fullscreen',
|
||||
}: LoadingScreenProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn('flex h-screen w-screen items-center justify-center', {
|
||||
'h-full w-full': mode === 'container',
|
||||
})}
|
||||
>
|
||||
<LoadingSpinner
|
||||
className={cn({
|
||||
'stroke-background!': brightSpinner,
|
||||
})}
|
||||
isLarge={true}
|
||||
></LoadingSpinner>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Lock } from 'lucide-react';
|
||||
|
||||
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert';
|
||||
|
||||
interface LockedAlertProps {
|
||||
title: string;
|
||||
description: string;
|
||||
button: React.ReactNode;
|
||||
}
|
||||
|
||||
export const LockedAlert = ({
|
||||
title,
|
||||
description,
|
||||
button,
|
||||
}: LockedAlertProps) => {
|
||||
return (
|
||||
<Alert className="flex items-center gap-4 mb-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Lock className="h-5 w-5 text-primary-600 mt-1" />
|
||||
<div>
|
||||
<AlertTitle className="font-semibold text-lg">{title}</AlertTitle>
|
||||
<AlertDescription className="text-sm text-muted-foreground">
|
||||
{description}
|
||||
</AlertDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto">{button}</div>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { t } from 'i18next';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
export const MessageTooltip = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
{ children: React.ReactNode; isDisabled: boolean; message: string }
|
||||
>(({ children, isDisabled, message }, ref) => {
|
||||
return (
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger ref={ref} asChild>
|
||||
<div>{children}</div>
|
||||
</TooltipTrigger>
|
||||
{isDisabled && (
|
||||
<TooltipContent side="bottom">{t(message)}</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
MessageTooltip.displayName = 'MessageTooltip';
|
||||
@@ -0,0 +1,76 @@
|
||||
import { MinusIcon, PlusIcon } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
||||
interface NumberInputWithButtonsProps {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const NumberInputWithButtons: React.FC<NumberInputWithButtonsProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
min = 0,
|
||||
max = Infinity,
|
||||
step = 1,
|
||||
disabled = false,
|
||||
className = '',
|
||||
}) => {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = parseInt(e.target.value) || 0;
|
||||
onChange(Math.min(Math.max(newValue, min), max));
|
||||
};
|
||||
|
||||
const increment = () => {
|
||||
if (disabled) return;
|
||||
onChange(Math.min(value + step, max));
|
||||
};
|
||||
|
||||
const decrement = () => {
|
||||
if (disabled) return;
|
||||
onChange(Math.max(value - step, min));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex items-center ${className}`}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-r-none"
|
||||
onClick={decrement}
|
||||
disabled={disabled || value <= min}
|
||||
type="button"
|
||||
>
|
||||
<MinusIcon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
className="h-8 w-16 text-center rounded-none border-x-0"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-l-none"
|
||||
onClick={increment}
|
||||
disabled={disabled || value >= max}
|
||||
type="button"
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NumberInputWithButtons;
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverAnchor = PopoverPrimitive.Anchor;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 w-72 rounded-md border bg-background p-4 text-foreground shadow-md outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative h-4 w-full overflow-hidden rounded-full bg-accent',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
));
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
|
||||
export { Progress };
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type ProgressProps = React.ComponentProps<typeof ProgressPrimitive.Root> & {
|
||||
indicatorClassName?: string;
|
||||
};
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
value,
|
||||
indicatorClassName,
|
||||
...props
|
||||
}: ProgressProps) {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
'bg-muted relative h-2 w-full overflow-hidden rounded-full',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className={cn(
|
||||
'bg-primary h-full w-full flex-1 transition-all',
|
||||
indicatorClassName,
|
||||
)}
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Progress };
|
||||
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
||||
import { CircleIcon } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function RadioGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
data-slot="radio-group"
|
||||
className={cn('grid gap-3', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RadioGroupItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
data-slot="radio-group-item"
|
||||
className={cn(
|
||||
'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-2xs transition-[color,box-shadow] outline-hidden focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator
|
||||
data-slot="radio-group-indicator"
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export { RadioGroup, RadioGroupItem };
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface ReadMoreProps {
|
||||
text: string;
|
||||
amountOfCharacters?: number;
|
||||
}
|
||||
|
||||
export const ReadMoreDescription = ({
|
||||
text,
|
||||
amountOfCharacters = 80,
|
||||
}: ReadMoreProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const itCanOverflow = text.length > amountOfCharacters;
|
||||
const beginText = itCanOverflow ? text.slice(0, amountOfCharacters) : text;
|
||||
const endText = text.slice(amountOfCharacters);
|
||||
|
||||
const handleKeyboard = (e: { code: string }) => {
|
||||
if (e.code === 'Space' || e.code === 'Enter') {
|
||||
setIsExpanded(!isExpanded);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<p className="text-muted-foreground text-xs whitespace-pre-wrap">
|
||||
{beginText}
|
||||
{itCanOverflow && (
|
||||
<>
|
||||
{!isExpanded && <span>... </span>}
|
||||
<span
|
||||
className={`${!isExpanded && 'hidden'} whitespace-pre-wrap`}
|
||||
aria-hidden={!isExpanded}
|
||||
>
|
||||
{endText}
|
||||
</span>
|
||||
<span
|
||||
className="text-primary ml-2 cursor-pointer"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-expanded={isExpanded}
|
||||
onKeyDown={handleKeyboard}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? 'show less' : 'show more'}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { GripVertical } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import * as ResizablePrimitive from 'react-resizable-panels';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const ResizablePanelGroup = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
||||
<ResizablePrimitive.PanelGroup
|
||||
className={cn(
|
||||
'flex h-full w-full data-[panel-group-direction=vertical]:flex-col',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const ResizablePanel = ResizablePrimitive.Panel;
|
||||
|
||||
const ResizableHandle = ({
|
||||
withHandle,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||
withHandle?: boolean;
|
||||
}) => (
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
className={cn(
|
||||
'bg-border z-40 focus-visible:ring-ring relative flex w-[1px] items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-[2px] data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
||||
<GripVertical className="size-2.5 hover:fill-primary" />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.PanelResizeHandle>
|
||||
);
|
||||
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
|
||||
@@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {
|
||||
viewPortClassName?: string;
|
||||
orientation?: 'vertical' | 'horizontal';
|
||||
viewPortRef?: React.RefObject<HTMLDivElement>;
|
||||
showGradient?: boolean;
|
||||
gradientClassName?: string;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
viewPortClassName,
|
||||
viewPortRef,
|
||||
orientation = 'vertical',
|
||||
showGradient = false,
|
||||
gradientClassName,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [showBottomGradient, setShowBottomGradient] = React.useState(false);
|
||||
const internalViewPortRef = React.useRef<HTMLDivElement>(null);
|
||||
const viewportRef = viewPortRef || internalViewPortRef;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!showGradient || !viewportRef.current) return;
|
||||
|
||||
const viewport = viewportRef.current;
|
||||
const checkScroll = () => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = viewport;
|
||||
const hasScrollableContent = scrollHeight > clientHeight;
|
||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
|
||||
|
||||
setShowBottomGradient(hasScrollableContent && !isAtBottom);
|
||||
};
|
||||
|
||||
checkScroll();
|
||||
viewport.addEventListener('scroll', checkScroll);
|
||||
|
||||
const resizeObserver = new ResizeObserver(checkScroll);
|
||||
if (viewport.firstElementChild) {
|
||||
resizeObserver.observe(viewport.firstElementChild);
|
||||
}
|
||||
|
||||
return () => {
|
||||
viewport.removeEventListener('scroll', checkScroll);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [showGradient, viewportRef]);
|
||||
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
className={cn(
|
||||
'size-full rounded-[inherit] [&>div]:block!',
|
||||
viewPortClassName,
|
||||
)}
|
||||
ref={viewportRef}
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar orientation={orientation} />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
|
||||
{showGradient && showBottomGradient && (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute bottom-0 left-0 right-0 h-1/5 bg-linear-to-t from-sidebar to-transparent',
|
||||
gradientClassName,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</ScrollAreaPrimitive.Root>
|
||||
);
|
||||
},
|
||||
);
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'flex touch-none select-none transition-colors',
|
||||
orientation === 'vertical' &&
|
||||
'h-full w-2.5 border-l border-l-transparent p-px',
|
||||
orientation === 'horizontal' &&
|
||||
'h-2.5 flex-col border-t border-t-transparent p-px',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
));
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||
|
||||
export { ScrollArea, ScrollBar };
|
||||
@@ -0,0 +1,44 @@
|
||||
import { t } from 'i18next';
|
||||
import { Search, X } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { SelectUtilButton } from '../custom/select-util-button';
|
||||
|
||||
export type SearchInputProps = Omit<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
'onChange'
|
||||
> & {
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(
|
||||
({ type, ...props }, ref) => {
|
||||
return (
|
||||
<div className="grow flex items-center gap-2 w-full bg-background px-3 focus-within:outline-hidden first:disabled:cursor-not-allowed first:disabled:opacity-50 box-border">
|
||||
<Search className="size-4 shrink-0 opacity-50"></Search>
|
||||
<input
|
||||
className="rounded-md bg-transparent h-9 grow text-sm outline-hidden placeholder:text-muted-foreground"
|
||||
type={type}
|
||||
ref={ref}
|
||||
{...props}
|
||||
onChange={(e) => props.onChange(e.target.value)}
|
||||
data-testid="pieces-search-input"
|
||||
/>
|
||||
{props.value !== '' && (
|
||||
<SelectUtilButton
|
||||
tooltipText={t('Clear')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
props.onChange('');
|
||||
}}
|
||||
Icon={X}
|
||||
></SelectUtilButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
SearchInput.displayName = 'SearchInput';
|
||||
|
||||
export { SearchInput };
|
||||
193
activepieces-fork/packages/react-ui/src/components/ui/select.tsx
Normal file
193
activepieces-fork/packages/react-ui/src/components/ui/select.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
CaretSortIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||
import { Check } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs ring-offset-background placeholder:text-muted-foreground focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<CaretSortIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = 'popper', ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-background text-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
'p-1',
|
||||
position === 'popper' &&
|
||||
'h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn('px-2 py-1.5 text-sm font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectAction = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & {
|
||||
children: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
>(({ className, children, disabled, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-hidden hover:bg-accent hover:text-accent-foreground',
|
||||
className,
|
||||
{ 'text-muted-foreground cursor-not-allowed': disabled },
|
||||
)}
|
||||
{...props}
|
||||
onClick={(e) => {
|
||||
if (disabled) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
} else {
|
||||
props.onClick?.(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
));
|
||||
|
||||
SelectAction.displayName = 'SelectAction';
|
||||
export { SelectAction };
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = 'horizontal', decorative = true, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'shrink-0 bg-border',
|
||||
orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator };
|
||||
|
||||
type HorizontalSeparatorWithTextProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
const HorizontalSeparatorWithText = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
HorizontalSeparatorWithTextProps
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className={cn('flex w-full flex-row items-center', className)}>
|
||||
<div className="w-1/2 border" />
|
||||
<span className="mx-2 text-sm">{props.children}</span>
|
||||
<div className="w-1/2 border" />
|
||||
</div>
|
||||
));
|
||||
|
||||
HorizontalSeparatorWithText.displayName = 'HorizontalSeparatorWithText';
|
||||
export { HorizontalSeparatorWithText };
|
||||
149
activepieces-fork/packages/react-ui/src/components/ui/sheet.tsx
Normal file
149
activepieces-fork/packages/react-ui/src/components/ui/sheet.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
'use client';
|
||||
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { X } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||
bottom:
|
||||
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||
right:
|
||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: 'right',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {
|
||||
hideCloseButton?: boolean;
|
||||
}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(
|
||||
(
|
||||
{ side = 'right', className, children, hideCloseButton = false, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
{!hideCloseButton && (
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
),
|
||||
);
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-2 text-center sm:text-left',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetHeader.displayName = 'SheetHeader';
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetFooter.displayName = 'SheetFooter';
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const toTitleCase = (str: string) => {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.split(' ')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
};
|
||||
export type ShortcutProps = {
|
||||
//can't name it key because it conflicts with the key prop of the component
|
||||
shortcutKey: string;
|
||||
withCtrl?: boolean;
|
||||
withShift?: boolean;
|
||||
shouldNotPreventDefault?: boolean;
|
||||
};
|
||||
|
||||
export const Shortcut = ({
|
||||
shortcutKey,
|
||||
withCtrl,
|
||||
withShift,
|
||||
className,
|
||||
}: ShortcutProps & { className?: string }) => {
|
||||
const isMac = /(Mac)/i.test(navigator.userAgent);
|
||||
const isEscape = shortcutKey?.toLocaleLowerCase() === 'esc';
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'grow text-xs tracking-widest text-muted-foreground',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{!isEscape && withCtrl && (isMac ? '⌘' : 'Ctrl')}
|
||||
{!isEscape && withShift && 'Shift'}
|
||||
{!isEscape && (withCtrl || withShift) && ' + '}
|
||||
{shortcutKey && toTitleCase(shortcutKey)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,831 @@
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { VariantProps, cva } from 'class-variance-authority';
|
||||
import { t } from 'i18next';
|
||||
import { PanelLeft } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const SIDEBAR_WIDTH = '18rem';
|
||||
const SIDEBAR_WIDTH_MOBILE = '18rem';
|
||||
const SIDEBAR_WIDTH_ICON = '3rem';
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||
const SIDEBAR_STATE_STORAGE_KEY = 'sidebar-state';
|
||||
|
||||
function getSidebarStateFromLocalStorage() {
|
||||
const stored = localStorage.getItem(SIDEBAR_STATE_STORAGE_KEY);
|
||||
return stored ? stored === 'true' : true;
|
||||
}
|
||||
|
||||
function setSidebarStateToLocalStorage(isOpen: boolean) {
|
||||
localStorage.setItem(SIDEBAR_STATE_STORAGE_KEY, isOpen.toString());
|
||||
}
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: 'expanded' | 'collapsed';
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
openMobile: boolean;
|
||||
setOpenMobile: (open: boolean) => void;
|
||||
isMobile: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext);
|
||||
if (!context) {
|
||||
throw new Error('useSidebar must be used within a SidebarProvider.');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const SidebarProvider = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'> & {
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
defaultOpen,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const isMobile = useIsMobile();
|
||||
const [openMobile, setOpenMobile] = React.useState(false);
|
||||
|
||||
const [_open, _setOpen] = React.useState(
|
||||
defaultOpen ?? getSidebarStateFromLocalStorage(),
|
||||
);
|
||||
const open = openProp ?? _open;
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === 'function' ? value(open) : value;
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState);
|
||||
} else {
|
||||
_setOpen(openState);
|
||||
}
|
||||
|
||||
setSidebarStateToLocalStorage(openState);
|
||||
},
|
||||
[setOpenProp, open],
|
||||
);
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile
|
||||
? setOpenMobile((open) => !open)
|
||||
: setOpen((open) => !open);
|
||||
}, [isMobile, setOpen, setOpenMobile]);
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [toggleSidebar]);
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? 'expanded' : 'collapsed';
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
'--sidebar-width': SIDEBAR_WIDTH,
|
||||
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
'group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarProvider.displayName = 'SidebarProvider';
|
||||
|
||||
const Sidebar = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'> & {
|
||||
side?: 'left' | 'right';
|
||||
variant?: 'sidebar' | 'floating' | 'inset';
|
||||
collapsible?: 'offcanvas' | 'icon' | 'none';
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
side = 'left',
|
||||
variant = 'sidebar',
|
||||
collapsible = 'offcanvas',
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { isMobile, state, openMobile, setOpenMobile, setOpen } =
|
||||
useSidebar();
|
||||
|
||||
if (collapsible === 'none') {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-mobile="true"
|
||||
className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="group peer hidden text-sidebar-foreground md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === 'collapsed' ? collapsible : ''}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
|
||||
'group-data-[collapsible=offcanvas]:w-0',
|
||||
'group-data-[side=right]:rotate-180',
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
|
||||
side === 'left'
|
||||
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
||||
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]'
|
||||
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]',
|
||||
state === 'collapsed' &&
|
||||
collapsible === 'icon' &&
|
||||
'[&_*]:!cursor-nesw-resize [&_button]:!cursor-pointer [&_button]:relative [&_button]:z-20 [&_button_*]:!cursor-pointer [&_a]:!cursor-pointer [&_a]:relative [&_a]:z-20 [&_a_*]:!cursor-pointer [&_[role=button]]:!cursor-pointer [&_[role=button]]:relative [&_[role=button]]:z-20 [&_[role=button]_*]:!cursor-pointer [&_[data-sidebar=menu-button]]:!cursor-pointer [&_[data-sidebar=menu-button]]:relative [&_[data-sidebar=menu-button]]:z-20 [&_[data-sidebar=menu-button]_*]:!cursor-pointer cursor-nesw-resize',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
className={cn(
|
||||
'flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm',
|
||||
state === 'collapsed' && collapsible === 'icon' && 'relative',
|
||||
)}
|
||||
>
|
||||
{/* Clickable overlay to expand collapsed sidebar */}
|
||||
{state === 'collapsed' && collapsible === 'icon' && (
|
||||
<div
|
||||
className="absolute inset-0 !cursor-nesw-resize z-10"
|
||||
onClick={() => setOpen(true)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Sidebar.displayName = 'Sidebar';
|
||||
|
||||
const SidebarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof Button>,
|
||||
React.ComponentProps<typeof Button> & { iconClassName?: string }
|
||||
>(({ className, onClick, iconClassName, ...props }, ref) => {
|
||||
const { toggleSidebar, state } = useSidebar();
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
ref={ref}
|
||||
data-sidebar="trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeft></PanelLeft>
|
||||
<span className="sr-only">{t('Toggle Sidebar')}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{state === 'expanded' ? t('Close Sidebar') : t('Open Sidebar')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
SidebarTrigger.displayName = 'SidebarTrigger';
|
||||
|
||||
const SidebarRail = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<'button'>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
data-sidebar="rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
|
||||
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
|
||||
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
||||
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar',
|
||||
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
|
||||
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarRail.displayName = 'SidebarRail';
|
||||
|
||||
const SidebarInset = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'main'>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<main
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex w-full flex-1 flex-col bg-background max-h-[calc(100vh)]',
|
||||
'md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:border-l',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarInset.displayName = 'SidebarInset';
|
||||
|
||||
const SidebarInput = React.forwardRef<
|
||||
React.ElementRef<typeof Input>,
|
||||
React.ComponentProps<typeof Input>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Input
|
||||
ref={ref}
|
||||
data-sidebar="input"
|
||||
className={cn(
|
||||
'h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarInput.displayName = 'SidebarInput';
|
||||
|
||||
const SidebarHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="header"
|
||||
className={cn('flex flex-col gap-2 p-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarHeader.displayName = 'SidebarHeader';
|
||||
|
||||
const SidebarFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="footer"
|
||||
className={cn('flex flex-col gap-2 p-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarFooter.displayName = 'SidebarFooter';
|
||||
|
||||
const SidebarSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof Separator>,
|
||||
React.ComponentProps<typeof Separator>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Separator
|
||||
ref={ref}
|
||||
data-sidebar="separator"
|
||||
className={cn('mx-2 w-auto bg-sidebar-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarSeparator.displayName = 'SidebarSeparator';
|
||||
|
||||
const SidebarContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarContent.displayName = 'SidebarContent';
|
||||
|
||||
const SidebarGroup = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="group"
|
||||
className={cn(
|
||||
'relative flex w-full min-w-0 flex-col px-2 py-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarGroup.displayName = 'SidebarGroup';
|
||||
|
||||
const SidebarGroupLabel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'div';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
'flex h-8 shrink-0 items-center rounded-sm px-2 text-xs font-medium text-sidebar-foreground/70 outline-hidden ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarGroupLabel.displayName = 'SidebarGroupLabel';
|
||||
|
||||
const SidebarGroupAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<'button'> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-sm p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
// Increases the hit area of the button on mobile.
|
||||
'after:absolute after:-inset-2 md:after:hidden',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarGroupAction.displayName = 'SidebarGroupAction';
|
||||
|
||||
const SidebarGroupContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="group-content"
|
||||
className={cn('w-full text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarGroupContent.displayName = 'SidebarGroupContent';
|
||||
|
||||
const SidebarMenu = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<'ul'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
data-sidebar="menu"
|
||||
className={cn('flex w-full min-w-0 flex-col gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenu.displayName = 'SidebarMenu';
|
||||
|
||||
const SidebarMenuItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<'li'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
data-sidebar="menu-item"
|
||||
className={cn('group/menu-item relative', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenuItem.displayName = 'SidebarMenuItem';
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-sm text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'hover:bg-sidebar-accent active:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||
outline:
|
||||
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
|
||||
},
|
||||
size: {
|
||||
default: 'h-8 text-sm',
|
||||
sm: 'h-7 text-xs',
|
||||
lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const SidebarMenuButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<'button'> & {
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>
|
||||
>(
|
||||
(
|
||||
{
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button;
|
||||
}
|
||||
|
||||
if (typeof tooltip === 'string') {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== 'collapsed' || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarMenuButton.displayName = 'SidebarMenuButton';
|
||||
|
||||
const SidebarMenuAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<'button'> & {
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}
|
||||
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
'absolute right-1 top-2 flex aspect-square w-5 items-center justify-center rounded-sm p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
// Increases the hit area of the button on mobile.
|
||||
'after:absolute after:-inset-2 md:after:hidden',
|
||||
'peer-data-[size=sm]/menu-button:top-1',
|
||||
'peer-data-[size=default]/menu-button:top-1.5',
|
||||
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
showOnHover &&
|
||||
'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarMenuAction.displayName = 'SidebarMenuAction';
|
||||
|
||||
const SidebarMenuBadge = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
'pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-sm px-1 text-xs font-medium tabular-nums text-sidebar-foreground',
|
||||
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
|
||||
'peer-data-[size=sm]/menu-button:top-1',
|
||||
'peer-data-[size=default]/menu-button:top-1.5',
|
||||
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenuBadge.displayName = 'SidebarMenuBadge';
|
||||
|
||||
const SidebarMenuSkeleton = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'> & {
|
||||
showIcon?: boolean;
|
||||
}
|
||||
>(({ className, showIcon = false, ...props }, ref) => {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn('flex h-8 items-center gap-2 rounded-sm px-2', className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className="size-4 rounded-sm"
|
||||
data-sidebar="menu-skeleton-icon"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 max-w-(--skeleton-width) flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
'--skeleton-width': width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
SidebarMenuSkeleton.displayName = 'SidebarMenuSkeleton';
|
||||
|
||||
const SidebarMenuSub = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<'ul'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
'ml-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border pl-2.5',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenuSub.displayName = 'SidebarMenuSub';
|
||||
|
||||
const SidebarMenuSubItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<'li'>
|
||||
>(({ ...props }, ref) => <li ref={ref} {...props} />);
|
||||
SidebarMenuSubItem.displayName = 'SidebarMenuSubItem';
|
||||
|
||||
const SidebarMenuSubButton = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentProps<'a'> & {
|
||||
asChild?: boolean;
|
||||
size?: 'sm' | 'md';
|
||||
isActive?: boolean;
|
||||
}
|
||||
>(({ asChild = false, size = 'md', isActive, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'a';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-sm px-2 text-sidebar-foreground outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
|
||||
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
|
||||
size === 'sm' && 'text-xs',
|
||||
size === 'md' && 'text-sm',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarMenuSubButton.displayName = 'SidebarMenuSubButton';
|
||||
|
||||
const SidebarSkeleton = ({ numOfItems }: { numOfItems: number }) => {
|
||||
return (
|
||||
<SidebarMenu>
|
||||
{[...Array(numOfItems)].map((_, index) => (
|
||||
<SidebarMenuItem key={index}>
|
||||
<SidebarMenuButton disabled className="px-2">
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-4 h-4 mr-2 bg-muted rounded animate-pulse" />
|
||||
<div className="bg-muted w-full rounded animate-pulse h-4" />
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarSkeleton,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn('animate-pulse rounded-md bg-muted/50', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SkeletonList({
|
||||
className,
|
||||
numberOfItems = 3,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & {
|
||||
numberOfItems?: number;
|
||||
}) {
|
||||
const array = Array(numberOfItems).fill(null);
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{array.map((_, index) => (
|
||||
<Skeleton
|
||||
key={index}
|
||||
className={cn('h-4 w-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
SkeletonList.displayName = 'SkeletonList';
|
||||
Skeleton.displayName = 'Skeleton';
|
||||
export { Skeleton, SkeletonList };
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as SliderPrimitive from '@radix-ui/react-slider';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex w-full touch-none select-none items-center',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow-sm transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
));
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
export { Slider };
|
||||
@@ -0,0 +1,74 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
CircleCheckIcon,
|
||||
InfoIcon,
|
||||
Loader2Icon,
|
||||
OctagonXIcon,
|
||||
TriangleAlertIcon,
|
||||
} from 'lucide-react';
|
||||
import { Toaster as Sonner, toast, type ToasterProps } from 'sonner';
|
||||
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
|
||||
export const INTERNAL_ERROR_MESSAGE =
|
||||
'An unexpected error occurred. Please try again in a moment.';
|
||||
|
||||
export function internalErrorToast() {
|
||||
toast.error('Something went wrong', {
|
||||
description: INTERNAL_ERROR_MESSAGE,
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
|
||||
export const UNSAVED_CHANGES_TOAST = {
|
||||
id: 'unsaved-changes',
|
||||
title: 'Unsaved Changes',
|
||||
description:
|
||||
'Something went wrong and there are unsaved changes, please refresh and contact support if the problem persists.',
|
||||
variant: 'destructive',
|
||||
duration: Infinity,
|
||||
};
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps['theme']}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast: `
|
||||
data-[type=error]:text-destructive-300!
|
||||
data-[type=warning]:text-warning-300!
|
||||
data-[type=success]:text-success-300!
|
||||
`,
|
||||
description: `
|
||||
data-[type=error]:text-destructive-300!
|
||||
data-[type=warning]:text-warning-300!
|
||||
data-[type=success]:text-success-300!
|
||||
`,
|
||||
},
|
||||
descriptionClassName: 'text-inherit!',
|
||||
}}
|
||||
icons={{
|
||||
success: <CircleCheckIcon className="size-4" />,
|
||||
info: <InfoIcon className="size-4" />,
|
||||
warning: <TriangleAlertIcon className="size-4" />,
|
||||
error: <OctagonXIcon className="size-4" />,
|
||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||
}}
|
||||
style={
|
||||
{
|
||||
'--normal-text': 'var(--foreground)',
|
||||
'--normal-bg': 'var(--background)',
|
||||
'--normal-border': 'var(--border)',
|
||||
'--border-radius': 'var(--radius)',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
||||
@@ -0,0 +1,338 @@
|
||||
'use client';
|
||||
|
||||
import type {
|
||||
DndContextProps,
|
||||
DraggableSyntheticListeners,
|
||||
DropAnimation,
|
||||
UniqueIdentifier,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
closestCenter,
|
||||
defaultDropAnimationSideEffects,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
KeyboardSensor,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
restrictToHorizontalAxis,
|
||||
restrictToParentElement,
|
||||
restrictToVerticalAxis,
|
||||
} from '@dnd-kit/modifiers';
|
||||
import {
|
||||
arrayMove,
|
||||
horizontalListSortingStrategy,
|
||||
SortableContext,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
type SortableContextProps,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Slot, type SlotProps } from '@radix-ui/react-slot';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button, type ButtonProps } from '@/components/ui/button';
|
||||
import { composeRefs } from '@/lib/compose-refs';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const orientationConfig = {
|
||||
vertical: {
|
||||
modifiers: [restrictToVerticalAxis, restrictToParentElement],
|
||||
strategy: verticalListSortingStrategy,
|
||||
},
|
||||
horizontal: {
|
||||
modifiers: [restrictToHorizontalAxis, restrictToParentElement],
|
||||
strategy: horizontalListSortingStrategy,
|
||||
},
|
||||
mixed: {
|
||||
modifiers: [restrictToParentElement],
|
||||
strategy: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
interface SortableProps<TData extends { id: UniqueIdentifier }>
|
||||
extends DndContextProps {
|
||||
/**
|
||||
* An array of data items that the sortable component will render.
|
||||
* @example
|
||||
* value={[
|
||||
* { id: 1, name: 'Item 1' },
|
||||
* { id: 2, name: 'Item 2' },
|
||||
* ]}
|
||||
*/
|
||||
value: TData[];
|
||||
|
||||
/**
|
||||
* An optional callback function that is called when the order of the data items changes.
|
||||
* It receives the new array of items as its argument.
|
||||
* @example
|
||||
* onValueChange={(items) => console.log(items)}
|
||||
*/
|
||||
onValueChange?: (items: TData[]) => void;
|
||||
|
||||
/**
|
||||
* An optional callback function that is called when an item is moved.
|
||||
* It receives an event object with `activeIndex` and `overIndex` properties, representing the original and new positions of the moved item.
|
||||
* This will override the default behavior of updating the order of the data items.
|
||||
* @type (event: { activeIndex: number; overIndex: number }) => void
|
||||
* @example
|
||||
* onMove={(event) => console.log(`Item moved from index ${event.activeIndex} to index ${event.overIndex}`)}
|
||||
*/
|
||||
onMove?: (event: { activeIndex: number; overIndex: number }) => void;
|
||||
|
||||
/**
|
||||
* A collision detection strategy that will be used to determine the closest sortable item.
|
||||
* @default closestCenter
|
||||
* @type DndContextProps["collisionDetection"]
|
||||
*/
|
||||
collisionDetection?: DndContextProps['collisionDetection'];
|
||||
|
||||
/**
|
||||
* An array of modifiers that will be used to modify the behavior of the sortable component.
|
||||
* @default
|
||||
* [restrictToVerticalAxis, restrictToParentElement]
|
||||
* @type Modifier[]
|
||||
*/
|
||||
modifiers?: DndContextProps['modifiers'];
|
||||
|
||||
/**
|
||||
* A sorting strategy that will be used to determine the new order of the data items.
|
||||
* @default verticalListSortingStrategy
|
||||
* @type SortableContextProps["strategy"]
|
||||
*/
|
||||
strategy?: SortableContextProps['strategy'];
|
||||
|
||||
/**
|
||||
* Specifies the axis for the drag-and-drop operation. It can be "vertical", "horizontal", or "both".
|
||||
* @default "vertical"
|
||||
* @type "vertical" | "horizontal" | "mixed"
|
||||
*/
|
||||
orientation?: 'vertical' | 'horizontal' | 'mixed';
|
||||
|
||||
/**
|
||||
* An optional React node that is rendered on top of the sortable component.
|
||||
* It can be used to display additional information or controls.
|
||||
* @default null
|
||||
* @type React.ReactNode | null
|
||||
* @example
|
||||
* overlay={<Skeleton className="w-full h-8" />}
|
||||
*/
|
||||
overlay?: React.ReactNode | null;
|
||||
}
|
||||
|
||||
function Sortable<TData extends { id: UniqueIdentifier }>({
|
||||
value,
|
||||
onValueChange,
|
||||
collisionDetection = closestCenter,
|
||||
modifiers,
|
||||
strategy,
|
||||
onMove,
|
||||
orientation = 'vertical',
|
||||
overlay,
|
||||
children,
|
||||
...props
|
||||
}: SortableProps<TData>) {
|
||||
const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null);
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor),
|
||||
useSensor(TouchSensor),
|
||||
useSensor(KeyboardSensor),
|
||||
);
|
||||
|
||||
const config = orientationConfig[orientation];
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
modifiers={modifiers ?? config.modifiers}
|
||||
sensors={sensors}
|
||||
onDragStart={({ active }) => setActiveId(active.id)}
|
||||
onDragEnd={({ active, over }) => {
|
||||
if (over && active.id !== over?.id) {
|
||||
const activeIndex = value.findIndex((item) => item.id === active.id);
|
||||
const overIndex = value.findIndex((item) => item.id === over.id);
|
||||
|
||||
if (onMove) {
|
||||
onMove({ activeIndex, overIndex });
|
||||
} else {
|
||||
onValueChange?.(arrayMove(value, activeIndex, overIndex));
|
||||
}
|
||||
}
|
||||
setActiveId(null);
|
||||
}}
|
||||
onDragCancel={() => setActiveId(null)}
|
||||
collisionDetection={collisionDetection}
|
||||
{...props}
|
||||
>
|
||||
<SortableContext items={value} strategy={strategy ?? config.strategy}>
|
||||
{children}
|
||||
</SortableContext>
|
||||
{overlay ? (
|
||||
<SortableOverlay activeId={activeId}>{overlay}</SortableOverlay>
|
||||
) : null}
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
const dropAnimationOpts: DropAnimation = {
|
||||
sideEffects: defaultDropAnimationSideEffects({
|
||||
styles: {
|
||||
active: {
|
||||
opacity: '0.4',
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
interface SortableOverlayProps
|
||||
extends React.ComponentPropsWithRef<typeof DragOverlay> {
|
||||
activeId?: UniqueIdentifier | null;
|
||||
}
|
||||
|
||||
const SortableOverlay = React.forwardRef<HTMLDivElement, SortableOverlayProps>(
|
||||
(
|
||||
{ activeId, dropAnimation = dropAnimationOpts, children, ...props },
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<DragOverlay dropAnimation={dropAnimation} {...props}>
|
||||
{activeId ? (
|
||||
<SortableItem
|
||||
ref={ref}
|
||||
value={activeId}
|
||||
className="cursor-grabbing"
|
||||
asChild
|
||||
>
|
||||
{children}
|
||||
</SortableItem>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
);
|
||||
},
|
||||
);
|
||||
SortableOverlay.displayName = 'SortableOverlay';
|
||||
|
||||
interface SortableItemContextProps {
|
||||
attributes: React.HTMLAttributes<HTMLElement>;
|
||||
listeners: DraggableSyntheticListeners | undefined;
|
||||
isDragging?: boolean;
|
||||
}
|
||||
|
||||
const SortableItemContext = React.createContext<SortableItemContextProps>({
|
||||
attributes: {},
|
||||
listeners: undefined,
|
||||
isDragging: false,
|
||||
});
|
||||
|
||||
function useSortableItem() {
|
||||
const context = React.useContext(SortableItemContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useSortableItem must be used within a SortableItem');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
interface SortableItemProps extends SlotProps {
|
||||
/**
|
||||
* The unique identifier of the item.
|
||||
* @example "item-1"
|
||||
* @type UniqueIdentifier
|
||||
*/
|
||||
value: UniqueIdentifier;
|
||||
|
||||
/**
|
||||
* Specifies whether the item should act as a trigger for the drag-and-drop action.
|
||||
* @default false
|
||||
* @type boolean | undefined
|
||||
*/
|
||||
asTrigger?: boolean;
|
||||
|
||||
/**
|
||||
* Merges the item's props into its immediate child.
|
||||
* @default false
|
||||
* @type boolean | undefined
|
||||
*/
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
/** Child must be a div */
|
||||
const SortableItem = React.forwardRef<HTMLDivElement, SortableItemProps>(
|
||||
({ value, asTrigger, asChild, className, ...props }, ref) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: value });
|
||||
|
||||
const context = React.useMemo<SortableItemContextProps>(
|
||||
() => ({
|
||||
attributes,
|
||||
listeners,
|
||||
isDragging,
|
||||
}),
|
||||
[attributes, listeners, isDragging],
|
||||
);
|
||||
const style: React.CSSProperties = {
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
const Comp = asChild ? Slot : 'div';
|
||||
|
||||
return (
|
||||
<SortableItemContext.Provider value={context}>
|
||||
<Comp
|
||||
data-state={isDragging ? 'dragging' : undefined}
|
||||
className={cn(
|
||||
'data-[state=dragging]:cursor-grabbing',
|
||||
{ 'cursor-grab': !isDragging && asTrigger },
|
||||
className,
|
||||
)}
|
||||
ref={composeRefs(ref, setNodeRef as React.Ref<HTMLDivElement>)}
|
||||
style={style}
|
||||
{...(asTrigger ? attributes : {})}
|
||||
{...(asTrigger ? listeners : {})}
|
||||
{...props}
|
||||
/>
|
||||
</SortableItemContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
SortableItem.displayName = 'SortableItem';
|
||||
|
||||
interface SortableDragHandleProps extends ButtonProps {
|
||||
withHandle?: boolean;
|
||||
}
|
||||
|
||||
const SortableDragHandle = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
SortableDragHandleProps
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { attributes, listeners, isDragging } = useSortableItem();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={composeRefs(ref)}
|
||||
data-state={isDragging ? 'dragging' : undefined}
|
||||
className={cn(
|
||||
'cursor-grab data-[state=dragging]:cursor-grabbing',
|
||||
className,
|
||||
)}
|
||||
type="button"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SortableDragHandle.displayName = 'SortableDragHandle';
|
||||
|
||||
export { Sortable, SortableDragHandle, SortableItem, SortableOverlay };
|
||||
@@ -0,0 +1,29 @@
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface ISVGProps extends React.SVGProps<SVGSVGElement> {
|
||||
className?: string;
|
||||
isLarge?: boolean;
|
||||
}
|
||||
/**When editing the size of the spinner use size class */
|
||||
const LoadingSpinner = React.memo(
|
||||
({ className, isLarge = false }: ISVGProps) => {
|
||||
return (
|
||||
<LoaderCircle
|
||||
className={cn(
|
||||
'animate-spin duration-1500 stroke-foreground size-5',
|
||||
{
|
||||
'size-[24px]': !isLarge,
|
||||
'size-[50px]': isLarge,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
LoadingSpinner.displayName = 'LoadingSpinner';
|
||||
export { LoadingSpinner };
|
||||
@@ -0,0 +1,60 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import React from 'react';
|
||||
|
||||
import { isNil } from '@activepieces/shared';
|
||||
|
||||
const statusCodeVariants = cva(
|
||||
'inline-flex gap-1 rounded px-2.5 py-1 text-xs font-semibold',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
success: 'bg-success-100 text-success-300',
|
||||
error: 'bg-destructive-100 text-destructive-300',
|
||||
default: 'bg-accent text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface StatusIconWithTextProps
|
||||
extends VariantProps<typeof statusCodeVariants> {
|
||||
icon: any;
|
||||
text: string;
|
||||
color?: string;
|
||||
textColor?: string;
|
||||
}
|
||||
|
||||
const StatusIconWithText = React.memo(
|
||||
({
|
||||
icon: Icon,
|
||||
text,
|
||||
variant,
|
||||
color,
|
||||
textColor,
|
||||
}: StatusIconWithTextProps) => {
|
||||
if (isNil(color) || isNil(textColor)) {
|
||||
return (
|
||||
<span className={statusCodeVariants({ variant })}>
|
||||
<Icon className="size-4" />
|
||||
<span>{text}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span
|
||||
className={statusCodeVariants({ variant })}
|
||||
style={{ backgroundColor: color || undefined }}
|
||||
>
|
||||
<Icon className="size-4" style={{ color: textColor }} />
|
||||
<span style={{ color: textColor }}>{text}</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
StatusIconWithText.displayName = 'StatusIconWithText';
|
||||
export { StatusIconWithText };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user