mirror of
https://github.com/umami-software/umami.git
synced 2026-02-15 10:05:36 +01:00
Move board component metadata editing into modal
This commit is contained in:
parent
0123f7069b
commit
db637864f6
5 changed files with 182 additions and 80 deletions
|
|
@ -1,5 +1,14 @@
|
|||
import { Button, Column, Focusable, ListItem, Row, Select, Text } from '@umami/react-zen';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Column,
|
||||
Focusable,
|
||||
ListItem,
|
||||
Row,
|
||||
Select,
|
||||
Text,
|
||||
TextField,
|
||||
} from '@umami/react-zen';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import type { BoardComponentConfig } from '@/lib/types';
|
||||
|
|
@ -13,29 +22,66 @@ import { BoardComponentRenderer } from './BoardComponentRenderer';
|
|||
|
||||
export function BoardComponentSelect({
|
||||
websiteId,
|
||||
initialConfig,
|
||||
onSelect,
|
||||
onClose,
|
||||
}: {
|
||||
websiteId: string;
|
||||
initialConfig?: BoardComponentConfig;
|
||||
onSelect: (config: BoardComponentConfig) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t, labels, messages } = useMessages();
|
||||
const [selectedDef, setSelectedDef] = useState<ComponentDefinition | null>(null);
|
||||
const [configValues, setConfigValues] = useState<Record<string, any>>({});
|
||||
const [title, setTitle] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
|
||||
const handleSelectComponent = (def: ComponentDefinition) => {
|
||||
setSelectedDef(def);
|
||||
const allDefinitions = useMemo(
|
||||
() => CATEGORIES.flatMap(category => getComponentsByCategory(category.key)),
|
||||
[],
|
||||
);
|
||||
|
||||
const getDefaultConfigValues = (def: ComponentDefinition, config?: BoardComponentConfig) => {
|
||||
const defaults: Record<string, any> = {};
|
||||
if (def.configFields) {
|
||||
for (const field of def.configFields) {
|
||||
defaults[field.name] = field.defaultValue;
|
||||
}
|
||||
|
||||
for (const field of def.configFields ?? []) {
|
||||
defaults[field.name] = field.defaultValue;
|
||||
}
|
||||
|
||||
if (def.defaultProps) {
|
||||
Object.assign(defaults, def.defaultProps);
|
||||
}
|
||||
setConfigValues(defaults);
|
||||
|
||||
if (config?.props) {
|
||||
Object.assign(defaults, config.props);
|
||||
}
|
||||
|
||||
return defaults;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
const definition = allDefinitions.find(def => def.type === initialConfig.type);
|
||||
|
||||
if (!definition) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedDef(definition);
|
||||
setConfigValues(getDefaultConfigValues(definition, initialConfig));
|
||||
setTitle(initialConfig.title || definition.name);
|
||||
setDescription(initialConfig.description || '');
|
||||
}, [initialConfig, allDefinitions]);
|
||||
|
||||
const handleSelectComponent = (def: ComponentDefinition) => {
|
||||
setSelectedDef(def);
|
||||
setConfigValues(getDefaultConfigValues(def));
|
||||
setTitle(def.name);
|
||||
setDescription('');
|
||||
};
|
||||
|
||||
const handleConfigChange = (name: string, value: any) => {
|
||||
|
|
@ -46,16 +92,25 @@ export function BoardComponentSelect({
|
|||
if (!selectedDef) return;
|
||||
|
||||
const props: Record<string, any> = {};
|
||||
|
||||
if (selectedDef.defaultProps) {
|
||||
Object.assign(props, selectedDef.defaultProps);
|
||||
}
|
||||
|
||||
Object.assign(props, configValues);
|
||||
|
||||
if (props.limit) {
|
||||
props.limit = Number(props.limit);
|
||||
for (const field of selectedDef.configFields ?? []) {
|
||||
if (field.type === 'number' && props[field.name] != null && props[field.name] !== '') {
|
||||
props[field.name] = Number(props[field.name]);
|
||||
}
|
||||
}
|
||||
|
||||
const config: BoardComponentConfig = { type: selectedDef.type };
|
||||
const config: BoardComponentConfig = {
|
||||
type: selectedDef.type,
|
||||
title: title || selectedDef.name,
|
||||
description,
|
||||
};
|
||||
|
||||
if (Object.keys(props).length > 0) {
|
||||
config.props = props;
|
||||
}
|
||||
|
|
@ -66,6 +121,8 @@ export function BoardComponentSelect({
|
|||
const previewConfig: BoardComponentConfig | null = selectedDef
|
||||
? {
|
||||
type: selectedDef.type,
|
||||
title,
|
||||
description,
|
||||
props: { ...selectedDef.defaultProps, ...configValues },
|
||||
}
|
||||
: null;
|
||||
|
|
@ -73,12 +130,13 @@ export function BoardComponentSelect({
|
|||
return (
|
||||
<Column gap="4">
|
||||
<Row gap="4" style={{ height: 600 }}>
|
||||
<Column gap="1" style={{ width: 200, flexShrink: 0, overflowY: 'auto' }}>
|
||||
{CATEGORIES.map(cat => {
|
||||
const components = getComponentsByCategory(cat.key);
|
||||
<Column gap="1" style={{ width: 280, flexShrink: 0, overflowY: 'auto' }}>
|
||||
{CATEGORIES.map(category => {
|
||||
const components = getComponentsByCategory(category.key);
|
||||
|
||||
return (
|
||||
<Column key={cat.key} gap="1" marginBottom="2">
|
||||
<Text weight="bold">{cat.name}</Text>
|
||||
<Column key={category.key} gap="1" marginBottom="2">
|
||||
<Text weight="bold">{category.name}</Text>
|
||||
{components.map(def => (
|
||||
<Focusable key={def.type}>
|
||||
<Row
|
||||
|
|
@ -111,30 +169,8 @@ export function BoardComponentSelect({
|
|||
);
|
||||
})}
|
||||
</Column>
|
||||
|
||||
<Column gap="3" flexGrow={1} style={{ minWidth: 0 }}>
|
||||
{selectedDef?.configFields && selectedDef.configFields.length > 0 && (
|
||||
<Row gap="3" alignItems="center" wrap="wrap">
|
||||
{selectedDef.configFields.map((field: ConfigField) => (
|
||||
<Row key={field.name} gap="2" alignItems="center">
|
||||
<Text size="sm" color="muted">
|
||||
{field.label}
|
||||
</Text>
|
||||
{field.type === 'select' && (
|
||||
<Select
|
||||
value={String(configValues[field.name] ?? field.defaultValue ?? '')}
|
||||
onChange={(value: string) => handleConfigChange(field.name, value)}
|
||||
>
|
||||
{field.options?.map(opt => (
|
||||
<ListItem key={opt.value} id={opt.value}>
|
||||
{opt.label}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</Row>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
<Panel maxHeight="100%">
|
||||
{previewConfig && websiteId ? (
|
||||
<BoardComponentRenderer config={previewConfig} websiteId={websiteId} />
|
||||
|
|
@ -147,7 +183,66 @@ export function BoardComponentSelect({
|
|||
)}
|
||||
</Panel>
|
||||
</Column>
|
||||
|
||||
<Column gap="3" style={{ width: 320, flexShrink: 0, overflowY: 'auto' }}>
|
||||
<Text weight="bold">{t(labels.properties)}</Text>
|
||||
|
||||
<Column gap="2">
|
||||
<Text size="sm" color="muted">
|
||||
{t(labels.title)}
|
||||
</Text>
|
||||
<TextField value={title} onChange={setTitle} autoComplete="off" />
|
||||
</Column>
|
||||
|
||||
<Column gap="2">
|
||||
<Text size="sm" color="muted">
|
||||
{t(labels.description)}
|
||||
</Text>
|
||||
<TextField value={description} onChange={setDescription} autoComplete="off" />
|
||||
</Column>
|
||||
|
||||
{selectedDef?.configFields && selectedDef.configFields.length > 0 && (
|
||||
<Column gap="3">
|
||||
{selectedDef.configFields.map((field: ConfigField) => (
|
||||
<Column key={field.name} gap="2">
|
||||
<Text size="sm" color="muted">
|
||||
{field.label}
|
||||
</Text>
|
||||
|
||||
{field.type === 'select' && (
|
||||
<Select
|
||||
value={String(configValues[field.name] ?? field.defaultValue ?? '')}
|
||||
onChange={(value: string) => handleConfigChange(field.name, value)}
|
||||
>
|
||||
{field.options?.map(option => (
|
||||
<ListItem key={option.value} id={option.value}>
|
||||
{option.label}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{field.type === 'text' && (
|
||||
<TextField
|
||||
value={String(configValues[field.name] ?? field.defaultValue ?? '')}
|
||||
onChange={(value: string) => handleConfigChange(field.name, value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{field.type === 'number' && (
|
||||
<TextField
|
||||
type="number"
|
||||
value={String(configValues[field.name] ?? field.defaultValue ?? '')}
|
||||
onChange={(value: string) => handleConfigChange(field.name, value)}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
))}
|
||||
</Column>
|
||||
)}
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row justifyContent="flex-end" gap="2" paddingTop="4">
|
||||
<Button variant="quiet" onPress={onClose}>
|
||||
{t(labels.cancel)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Box, Button, Dialog, Icon, Modal, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
||||
import { Box, Button, Dialog, Icon, Modal, Row, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { useBoard, useMessages } from '@/components/hooks';
|
||||
|
|
@ -43,7 +43,9 @@ export function BoardEditColumn({
|
|||
|
||||
const hasComponent = !!component;
|
||||
const canRemoveAction = hasComponent || canRemove;
|
||||
const title = component ? getComponentDefinition(component.type)?.name : undefined;
|
||||
const defaultTitle = component ? getComponentDefinition(component.type)?.name : undefined;
|
||||
const title = component?.title || defaultTitle;
|
||||
const description = component?.description;
|
||||
|
||||
const handleRemove = () => {
|
||||
if (hasComponent) {
|
||||
|
|
@ -56,10 +58,9 @@ export function BoardEditColumn({
|
|||
return (
|
||||
<Panel
|
||||
title={title}
|
||||
description={description}
|
||||
width="100%"
|
||||
height="100%"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
position="relative"
|
||||
className={styles.column}
|
||||
>
|
||||
|
|
@ -67,51 +68,49 @@ export function BoardEditColumn({
|
|||
<Box
|
||||
className={styles.columnAction}
|
||||
position="absolute"
|
||||
top="10px"
|
||||
right="20px"
|
||||
top="22px"
|
||||
right="24px"
|
||||
zIndex={100}
|
||||
>
|
||||
<TooltipTrigger delay={0}>
|
||||
<Button variant="outline" onPress={handleRemove} isDisabled={!canRemoveAction}>
|
||||
<Icon size="sm">
|
||||
<X />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Tooltip>{hasComponent ? 'Remove component' : 'Remove column'}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
</Box>
|
||||
)}
|
||||
{renderedComponent ? (
|
||||
<>
|
||||
<Box width="100%" height="100%" overflow="auto">
|
||||
{renderedComponent}
|
||||
</Box>
|
||||
{canEdit && (
|
||||
<Box
|
||||
className={styles.columnAction}
|
||||
position="absolute"
|
||||
bottom="10px"
|
||||
right="20px"
|
||||
zIndex={100}
|
||||
>
|
||||
<Row gap="2">
|
||||
{hasComponent && (
|
||||
<TooltipTrigger delay={0}>
|
||||
<Button variant="outline" onPress={() => setShowSelect(true)}>
|
||||
<Icon size="sm">
|
||||
<Pencil />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Tooltip>Change component</Tooltip>
|
||||
<Tooltip>{t(labels.edit)}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<TooltipTrigger delay={0}>
|
||||
<Button variant="outline" onPress={handleRemove} isDisabled={!canRemoveAction}>
|
||||
<Icon size="sm">
|
||||
<X />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Tooltip>{t(labels.remove)}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
</Row>
|
||||
</Box>
|
||||
)}
|
||||
{renderedComponent ? (
|
||||
<Box width="100%" height="100%" overflow="auto">
|
||||
{renderedComponent}
|
||||
</Box>
|
||||
) : (
|
||||
canEdit && (
|
||||
<Button variant="outline" onPress={() => setShowSelect(true)}>
|
||||
<Icon>
|
||||
<Plus />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
<Button variant="outline" onPress={() => setShowSelect(true)}>
|
||||
<Icon>
|
||||
<Plus />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
<Modal isOpen={showSelect} onOpenChange={setShowSelect}>
|
||||
|
|
@ -127,6 +126,7 @@ export function BoardEditColumn({
|
|||
{() => (
|
||||
<BoardComponentSelect
|
||||
websiteId={websiteId}
|
||||
initialConfig={component}
|
||||
onSelect={handleSelect}
|
||||
onClose={() => setShowSelect(false)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@ export function BoardViewColumn({ component }: { component?: BoardComponentConfi
|
|||
return null;
|
||||
}
|
||||
|
||||
const title = getComponentDefinition(component.type)?.name;
|
||||
const title = component.title || getComponentDefinition(component.type)?.name;
|
||||
const description = component.description;
|
||||
|
||||
return (
|
||||
<Panel title={title} height="100%">
|
||||
<Panel title={title} description={description} height="100%">
|
||||
<Column width="100%" height="100%">
|
||||
<Box width="100%" overflow="auto">
|
||||
<BoardComponentRenderer config={component} websiteId={websiteId} />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
Heading,
|
||||
Icon,
|
||||
Row,
|
||||
Text,
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
} from '@umami/react-zen';
|
||||
|
|
@ -14,6 +15,7 @@ import { Maximize, X } from '@/components/icons';
|
|||
|
||||
export interface PanelProps extends ColumnProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
allowFullscreen?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -29,6 +31,7 @@ const fullscreenStyles = {
|
|||
|
||||
export function Panel({
|
||||
title,
|
||||
description,
|
||||
allowFullscreen,
|
||||
style,
|
||||
children,
|
||||
|
|
@ -56,6 +59,7 @@ export function Panel({
|
|||
style={{ ...style, ...(isFullscreen ? fullscreenStyles : { height, width }) }}
|
||||
>
|
||||
{title && <Heading>{title}</Heading>}
|
||||
{description && <Text color="muted">{description}</Text>}
|
||||
{allowFullscreen && (
|
||||
<Row justifyContent="flex-end" alignItems="center">
|
||||
<TooltipTrigger delay={0} isDisabled={isFullscreen}>
|
||||
|
|
|
|||
|
|
@ -146,6 +146,8 @@ export interface ApiError extends Error {
|
|||
|
||||
export interface BoardComponentConfig {
|
||||
type: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
props?: Record<string, any>;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue