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