mirror of
https://github.com/umami-software/umami.git
synced 2026-02-15 10:05:36 +01:00
Migrate board layout UI to react-zen and preserve empty component titles
This commit is contained in:
parent
db637864f6
commit
cda9c684c3
9 changed files with 168 additions and 177 deletions
|
|
@ -1,17 +0,0 @@
|
|||
.column {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.columnAction {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity 120ms ease;
|
||||
}
|
||||
|
||||
.column:hover .columnAction,
|
||||
.column:focus-within .columnAction {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
|
@ -73,14 +73,14 @@ export function BoardComponentSelect({
|
|||
|
||||
setSelectedDef(definition);
|
||||
setConfigValues(getDefaultConfigValues(definition, initialConfig));
|
||||
setTitle(initialConfig.title || definition.name);
|
||||
setTitle(initialConfig.title ?? '');
|
||||
setDescription(initialConfig.description || '');
|
||||
}, [initialConfig, allDefinitions]);
|
||||
|
||||
const handleSelectComponent = (def: ComponentDefinition) => {
|
||||
setSelectedDef(def);
|
||||
setConfigValues(getDefaultConfigValues(def));
|
||||
setTitle(def.name);
|
||||
setTitle('');
|
||||
setDescription('');
|
||||
};
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ export function BoardComponentSelect({
|
|||
|
||||
const config: BoardComponentConfig = {
|
||||
type: selectedDef.type,
|
||||
title: title || selectedDef.name,
|
||||
title,
|
||||
description,
|
||||
};
|
||||
|
||||
|
|
@ -248,7 +248,7 @@ export function BoardComponentSelect({
|
|||
{t(labels.cancel)}
|
||||
</Button>
|
||||
<Button variant="primary" onPress={handleAdd} isDisabled={!selectedDef}>
|
||||
{t(labels.add)}
|
||||
{t(labels.save)}
|
||||
</Button>
|
||||
</Row>
|
||||
</Column>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Button, Icon, Row, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
||||
import { Box, Button, Icon, Row, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
||||
import { produce } from 'immer';
|
||||
import { Fragment, useEffect, useRef } from 'react';
|
||||
import { Group, type GroupImperativeHandle, Panel, Separator } from 'react-resizable-panels';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useBoard } from '@/components/hooks';
|
||||
import { GripHorizontal, Plus } from '@/components/icons';
|
||||
import styles from './BoardEditLayout.module.css';
|
||||
import { BoardEditRow } from './BoardEditRow';
|
||||
import { BUTTON_ROW_HEIGHT, MAX_ROW_HEIGHT, MIN_ROW_HEIGHT } from './boardConstants';
|
||||
|
||||
|
|
@ -108,52 +107,71 @@ export function BoardEditBody() {
|
|||
const minHeight = (rows.length || 1) * MAX_ROW_HEIGHT + BUTTON_ROW_HEIGHT;
|
||||
|
||||
return (
|
||||
<Group groupRef={rowGroupRef} orientation="vertical" style={{ minHeight }}>
|
||||
{rows.map((row, index) => (
|
||||
<Fragment key={`${row.id}:${row.size ?? 'auto'}`}>
|
||||
<Panel
|
||||
id={row.id}
|
||||
minSize={MIN_ROW_HEIGHT}
|
||||
maxSize={MAX_ROW_HEIGHT}
|
||||
defaultSize={row.size != null ? `${row.size}%` : undefined}
|
||||
>
|
||||
<BoardEditRow
|
||||
{...row}
|
||||
rowId={row.id}
|
||||
rowIndex={index}
|
||||
rowCount={rows.length}
|
||||
canEdit={!!websiteId}
|
||||
onRemove={handleRemoveRow}
|
||||
onMoveUp={handleMoveRowUp}
|
||||
onMoveDown={handleMoveRowDown}
|
||||
onRegisterRef={registerColumnGroupRef}
|
||||
/>
|
||||
<Box minHeight={`${minHeight}px`}>
|
||||
<Group groupRef={rowGroupRef} orientation="vertical">
|
||||
{rows.map((row, index) => (
|
||||
<Fragment key={`${row.id}:${row.size ?? 'auto'}`}>
|
||||
<Panel
|
||||
id={row.id}
|
||||
minSize={MIN_ROW_HEIGHT}
|
||||
maxSize={MAX_ROW_HEIGHT}
|
||||
defaultSize={row.size != null ? `${row.size}%` : undefined}
|
||||
>
|
||||
<BoardEditRow
|
||||
{...row}
|
||||
rowId={row.id}
|
||||
rowIndex={index}
|
||||
rowCount={rows.length}
|
||||
canEdit={!!websiteId}
|
||||
onRemove={handleRemoveRow}
|
||||
onMoveUp={handleMoveRowUp}
|
||||
onMoveDown={handleMoveRowDown}
|
||||
onRegisterRef={registerColumnGroupRef}
|
||||
/>
|
||||
</Panel>
|
||||
{index < rows.length - 1 && (
|
||||
<Separator
|
||||
style={{
|
||||
height: '12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
boxShadow: 'none',
|
||||
background: 'transparent',
|
||||
}}
|
||||
>
|
||||
<Row
|
||||
width="100%"
|
||||
height="100%"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
style={{ cursor: 'row-resize' }}
|
||||
>
|
||||
<Icon size="sm">
|
||||
<GripHorizontal />
|
||||
</Icon>
|
||||
</Row>
|
||||
</Separator>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
{!!websiteId && (
|
||||
<Panel minSize={BUTTON_ROW_HEIGHT}>
|
||||
<Row padding="3">
|
||||
<TooltipTrigger delay={0}>
|
||||
<Button variant="outline" onPress={handleAddRow}>
|
||||
<Icon>
|
||||
<Plus />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Tooltip placement="bottom">Add row</Tooltip>
|
||||
</TooltipTrigger>
|
||||
</Row>
|
||||
</Panel>
|
||||
{index < rows.length - 1 && (
|
||||
<Separator className={styles.rowSeparator}>
|
||||
<span className={styles.separatorHandle}>
|
||||
<Icon size="sm">
|
||||
<GripHorizontal />
|
||||
</Icon>
|
||||
</span>
|
||||
</Separator>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
{!!websiteId && (
|
||||
<Panel minSize={BUTTON_ROW_HEIGHT}>
|
||||
<Row padding="3">
|
||||
<TooltipTrigger delay={0}>
|
||||
<Button variant="outline" onPress={handleAddRow}>
|
||||
<Icon>
|
||||
<Plus />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Tooltip placement="bottom">Add row</Tooltip>
|
||||
</TooltipTrigger>
|
||||
</Row>
|
||||
</Panel>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,20 @@
|
|||
import { Box, Button, Dialog, Icon, Modal, Row, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Column,
|
||||
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';
|
||||
import { Pencil, Plus, X } from '@/components/icons';
|
||||
import type { BoardComponentConfig } from '@/lib/types';
|
||||
import { getComponentDefinition } from '../boardComponentRegistry';
|
||||
import styles from './BoardColumn.module.css';
|
||||
import { BoardComponentRenderer } from './BoardComponentRenderer';
|
||||
import { BoardComponentSelect } from './BoardComponentSelect';
|
||||
|
||||
|
|
@ -25,6 +34,7 @@ export function BoardEditColumn({
|
|||
canRemove?: boolean;
|
||||
}) {
|
||||
const [showSelect, setShowSelect] = useState(false);
|
||||
const [showActions, setShowActions] = useState(false);
|
||||
const { board } = useBoard();
|
||||
const { t, labels } = useMessages();
|
||||
const websiteId = board?.parameters?.websiteId;
|
||||
|
|
@ -44,7 +54,7 @@ export function BoardEditColumn({
|
|||
const hasComponent = !!component;
|
||||
const canRemoveAction = hasComponent || canRemove;
|
||||
const defaultTitle = component ? getComponentDefinition(component.type)?.name : undefined;
|
||||
const title = component?.title || defaultTitle;
|
||||
const title = component?.title ?? defaultTitle;
|
||||
const description = component?.description;
|
||||
|
||||
const handleRemove = () => {
|
||||
|
|
@ -62,16 +72,11 @@ export function BoardEditColumn({
|
|||
width="100%"
|
||||
height="100%"
|
||||
position="relative"
|
||||
className={styles.column}
|
||||
onMouseEnter={() => setShowActions(true)}
|
||||
onMouseLeave={() => setShowActions(false)}
|
||||
>
|
||||
{canEdit && canRemoveAction && (
|
||||
<Box
|
||||
className={styles.columnAction}
|
||||
position="absolute"
|
||||
top="22px"
|
||||
right="24px"
|
||||
zIndex={100}
|
||||
>
|
||||
{canEdit && canRemoveAction && showActions && (
|
||||
<Box position="absolute" top="22px" right="24px" zIndex={100}>
|
||||
<Row gap="2">
|
||||
{hasComponent && (
|
||||
<TooltipTrigger delay={0}>
|
||||
|
|
@ -100,17 +105,13 @@ export function BoardEditColumn({
|
|||
</Box>
|
||||
) : (
|
||||
canEdit && (
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
<Column width="100%" height="100%" alignItems="center" justifyContent="center">
|
||||
<Button variant="outline" onPress={() => setShowSelect(true)}>
|
||||
<Icon>
|
||||
<Plus />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Box>
|
||||
</Column>
|
||||
)
|
||||
)}
|
||||
<Modal isOpen={showSelect} onOpenChange={setShowSelect}>
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
.columnSeparator {
|
||||
width: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.rowSeparator {
|
||||
height: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.separatorHandle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--gray-9);
|
||||
}
|
||||
|
||||
.rowGroup {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.rowActions {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 12px;
|
||||
transform: translateY(-50%);
|
||||
z-index: 20;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity 120ms ease;
|
||||
}
|
||||
|
||||
.rowGroup:hover .rowActions,
|
||||
.rowGroup:focus-within .rowActions {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Button, Column, Icon, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
||||
import { Box, Button, Column, Icon, Row, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
||||
import { produce } from 'immer';
|
||||
import { Fragment } from 'react';
|
||||
import { Fragment, useState } from 'react';
|
||||
import {
|
||||
Group,
|
||||
type GroupImperativeHandle,
|
||||
|
|
@ -12,7 +12,6 @@ import { useBoard } from '@/components/hooks';
|
|||
import { ChevronDown, GripVertical, Minus, Plus } from '@/components/icons';
|
||||
import type { BoardColumn as BoardColumnType, BoardComponentConfig } from '@/lib/types';
|
||||
import { BoardEditColumn } from './BoardEditColumn';
|
||||
import styles from './BoardEditLayout.module.css';
|
||||
import { MAX_COLUMNS, MIN_COLUMN_WIDTH } from './boardConstants';
|
||||
|
||||
export function BoardEditRow({
|
||||
|
|
@ -37,6 +36,7 @@ export function BoardEditRow({
|
|||
onRegisterRef: (rowId: string, ref: GroupImperativeHandle | null) => void;
|
||||
}) {
|
||||
const { board, updateBoard } = useBoard();
|
||||
const [showActions, setShowActions] = useState(false);
|
||||
|
||||
const handleGroupRef = (ref: GroupImperativeHandle | null) => {
|
||||
onRegisterRef(rowId, ref);
|
||||
|
|
@ -82,35 +82,68 @@ export function BoardEditRow({
|
|||
};
|
||||
|
||||
return (
|
||||
<Group groupRef={handleGroupRef} className={styles.rowGroup}>
|
||||
{columns?.map((column, index) => (
|
||||
<Fragment key={`${column.id}:${column.size ?? 'auto'}`}>
|
||||
<ResizablePanel
|
||||
id={column.id}
|
||||
minSize={MIN_COLUMN_WIDTH}
|
||||
defaultSize={column.size != null ? `${column.size}%` : undefined}
|
||||
>
|
||||
<BoardEditColumn
|
||||
{...column}
|
||||
canEdit={canEdit}
|
||||
onRemove={handleRemoveColumn}
|
||||
onSetComponent={handleSetComponent}
|
||||
canRemove={columns.length > 1}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
{index < columns.length - 1 && (
|
||||
<Separator className={styles.columnSeparator}>
|
||||
<span className={styles.separatorHandle}>
|
||||
<Icon size="sm">
|
||||
<GripVertical />
|
||||
</Icon>
|
||||
</span>
|
||||
</Separator>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
{canEdit && (
|
||||
<Column className={styles.rowActions} padding="3" gap="1">
|
||||
<Box
|
||||
position="relative"
|
||||
height="100%"
|
||||
onMouseEnter={() => setShowActions(true)}
|
||||
onMouseLeave={() => setShowActions(false)}
|
||||
>
|
||||
<Group groupRef={handleGroupRef}>
|
||||
{columns?.map((column, index) => (
|
||||
<Fragment key={`${column.id}:${column.size ?? 'auto'}`}>
|
||||
<ResizablePanel
|
||||
id={column.id}
|
||||
minSize={MIN_COLUMN_WIDTH}
|
||||
defaultSize={column.size != null ? `${column.size}%` : undefined}
|
||||
>
|
||||
<BoardEditColumn
|
||||
{...column}
|
||||
canEdit={canEdit}
|
||||
onRemove={handleRemoveColumn}
|
||||
onSetComponent={handleSetComponent}
|
||||
canRemove={columns.length > 1}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
{index < columns.length - 1 && (
|
||||
<Separator
|
||||
style={{
|
||||
width: '12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
boxShadow: 'none',
|
||||
background: 'transparent',
|
||||
}}
|
||||
>
|
||||
<Row
|
||||
width="100%"
|
||||
height="100%"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
style={{ cursor: 'col-resize' }}
|
||||
>
|
||||
<Icon size="sm">
|
||||
<GripVertical />
|
||||
</Icon>
|
||||
</Row>
|
||||
</Separator>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</Group>
|
||||
{canEdit && showActions && (
|
||||
<Column
|
||||
padding="3"
|
||||
gap="1"
|
||||
position="absolute"
|
||||
top="0"
|
||||
bottom="0"
|
||||
right="12px"
|
||||
zIndex={20}
|
||||
justifyContent="center"
|
||||
>
|
||||
<TooltipTrigger delay={0}>
|
||||
<Button variant="outline" onPress={() => onMoveUp(rowId)} isDisabled={rowIndex === 0}>
|
||||
<Icon rotate={180}>
|
||||
|
|
@ -153,6 +186,6 @@ export function BoardEditRow({
|
|||
</TooltipTrigger>
|
||||
</Column>
|
||||
)}
|
||||
</Group>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Column } from '@umami/react-zen';
|
||||
import { useBoard } from '@/components/hooks';
|
||||
import { BoardViewRow } from './BoardViewRow';
|
||||
|
||||
|
|
@ -6,10 +7,10 @@ export function BoardViewBody() {
|
|||
const rows = board?.parameters?.rows ?? [];
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<Column gap="3">
|
||||
{rows.map(row => (
|
||||
<BoardViewRow key={row.id} columns={row.columns} />
|
||||
))}
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function BoardViewColumn({ component }: { component?: BoardComponentConfi
|
|||
return null;
|
||||
}
|
||||
|
||||
const title = component.title || getComponentDefinition(component.type)?.name;
|
||||
const title = component.title ?? getComponentDefinition(component.type)?.name;
|
||||
const description = component.description;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
import { Box, Row } from '@umami/react-zen';
|
||||
import type { BoardColumn } from '@/lib/types';
|
||||
import { BoardViewColumn } from './BoardViewColumn';
|
||||
import { MIN_COLUMN_WIDTH } from './boardConstants';
|
||||
|
||||
export function BoardViewRow({ columns }: { columns: BoardColumn[] }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 12, width: '100%', overflowX: 'auto' }}>
|
||||
<Row gap="3" width="100%" overflowX="auto">
|
||||
{columns.map(column => (
|
||||
<div
|
||||
<Box
|
||||
key={column.id}
|
||||
style={{
|
||||
flex: `${column.size ?? 1} 1 0%`,
|
||||
minWidth: MIN_COLUMN_WIDTH,
|
||||
}}
|
||||
flexGrow={column.size ?? 1}
|
||||
flexShrink={1}
|
||||
flexBasis="0%"
|
||||
minWidth={`${MIN_COLUMN_WIDTH}px`}
|
||||
>
|
||||
<BoardViewColumn component={column.component} />
|
||||
</div>
|
||||
</Box>
|
||||
))}
|
||||
</div>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue