mirror of
https://github.com/umami-software/umami.git
synced 2026-02-15 18:15:35 +01:00
Split board view/edit rendering and isolate edit interactions
This commit is contained in:
parent
b09694ddb6
commit
d8c41ac8a6
15 changed files with 249 additions and 111 deletions
|
|
@ -99,7 +99,7 @@ export function BoardProvider({
|
||||||
const saveBoard = useCallback(async () => {
|
const saveBoard = useCallback(async () => {
|
||||||
const defaultName = t(labels.untitled);
|
const defaultName = t(labels.untitled);
|
||||||
|
|
||||||
// Get current layout sizes from BoardBody if registered
|
// Get current layout sizes from BoardEditBody if registered
|
||||||
const layoutData = layoutGetterRef.current?.();
|
const layoutData = layoutGetterRef.current?.();
|
||||||
const parameters = sanitizeBoardParameters(
|
const parameters = sanitizeBoardParameters(
|
||||||
layoutData ? { ...board.parameters, ...layoutData } : board.parameters,
|
layoutData ? { ...board.parameters, ...layoutData } : board.parameters,
|
||||||
|
|
|
||||||
18
src/app/(main)/boards/[boardId]/BoardControls.tsx
Normal file
18
src/app/(main)/boards/[boardId]/BoardControls.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Box } from '@umami/react-zen';
|
||||||
|
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||||
|
import { useBoard } from '@/components/hooks';
|
||||||
|
|
||||||
|
export function BoardControls() {
|
||||||
|
const { board } = useBoard();
|
||||||
|
const websiteId = board?.parameters?.websiteId;
|
||||||
|
|
||||||
|
if (!websiteId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box marginBottom="4">
|
||||||
|
<WebsiteControls websiteId={websiteId} allowCompare={true} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,16 +4,16 @@ import { Fragment, useEffect, useRef } from 'react';
|
||||||
import { Group, type GroupImperativeHandle, Panel, Separator } from 'react-resizable-panels';
|
import { Group, type GroupImperativeHandle, Panel, Separator } from 'react-resizable-panels';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { useBoard } from '@/components/hooks';
|
import { useBoard } from '@/components/hooks';
|
||||||
import { Plus } from '@/components/icons';
|
import { GripHorizontal, Plus } from '@/components/icons';
|
||||||
import { BoardRow } from './BoardRow';
|
import styles from './BoardEditLayout.module.css';
|
||||||
|
import { BoardEditRow } from './BoardEditRow';
|
||||||
import { BUTTON_ROW_HEIGHT, MAX_ROW_HEIGHT, MIN_ROW_HEIGHT } from './boardConstants';
|
import { BUTTON_ROW_HEIGHT, MAX_ROW_HEIGHT, MIN_ROW_HEIGHT } from './boardConstants';
|
||||||
|
|
||||||
export function BoardBody() {
|
export function BoardEditBody() {
|
||||||
const { board, editing, updateBoard, saveBoard, isPending, registerLayoutGetter } = useBoard();
|
const { board, updateBoard, registerLayoutGetter } = useBoard();
|
||||||
const rowGroupRef = useRef<GroupImperativeHandle>(null);
|
const rowGroupRef = useRef<GroupImperativeHandle>(null);
|
||||||
const columnGroupRefs = useRef<Map<string, GroupImperativeHandle>>(new Map());
|
const columnGroupRefs = useRef<Map<string, GroupImperativeHandle>>(new Map());
|
||||||
|
|
||||||
// Register a function to get current layout sizes on save
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
registerLayoutGetter(() => {
|
registerLayoutGetter(() => {
|
||||||
const rows = board?.parameters?.rows;
|
const rows = board?.parameters?.rows;
|
||||||
|
|
@ -104,9 +104,8 @@ export function BoardBody() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const websiteId = board?.parameters?.websiteId;
|
const websiteId = board?.parameters?.websiteId;
|
||||||
const canEdit = editing && !!websiteId;
|
|
||||||
const rows = board?.parameters?.rows ?? [];
|
const rows = board?.parameters?.rows ?? [];
|
||||||
const minHeight = (rows?.length || 1) * MAX_ROW_HEIGHT + BUTTON_ROW_HEIGHT;
|
const minHeight = (rows.length || 1) * MAX_ROW_HEIGHT + BUTTON_ROW_HEIGHT;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group groupRef={rowGroupRef} orientation="vertical" style={{ minHeight }}>
|
<Group groupRef={rowGroupRef} orientation="vertical" style={{ minHeight }}>
|
||||||
|
|
@ -118,22 +117,30 @@ export function BoardBody() {
|
||||||
maxSize={MAX_ROW_HEIGHT}
|
maxSize={MAX_ROW_HEIGHT}
|
||||||
defaultSize={row.size}
|
defaultSize={row.size}
|
||||||
>
|
>
|
||||||
<BoardRow
|
<BoardEditRow
|
||||||
{...row}
|
{...row}
|
||||||
rowId={row.id}
|
rowId={row.id}
|
||||||
rowIndex={index}
|
rowIndex={index}
|
||||||
rowCount={rows?.length}
|
rowCount={rows.length}
|
||||||
editing={canEdit}
|
canEdit={!!websiteId}
|
||||||
onRemove={handleRemoveRow}
|
onRemove={handleRemoveRow}
|
||||||
onMoveUp={handleMoveRowUp}
|
onMoveUp={handleMoveRowUp}
|
||||||
onMoveDown={handleMoveRowDown}
|
onMoveDown={handleMoveRowDown}
|
||||||
onRegisterRef={registerColumnGroupRef}
|
onRegisterRef={registerColumnGroupRef}
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
{index < rows?.length - 1 && <Separator />}
|
{index < rows.length - 1 && (
|
||||||
|
<Separator className={styles.rowSeparator}>
|
||||||
|
<span className={styles.separatorHandle}>
|
||||||
|
<Icon size="sm">
|
||||||
|
<GripHorizontal />
|
||||||
|
</Icon>
|
||||||
|
</span>
|
||||||
|
</Separator>
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
{canEdit && (
|
{!!websiteId && (
|
||||||
<Panel minSize={BUTTON_ROW_HEIGHT}>
|
<Panel minSize={BUTTON_ROW_HEIGHT}>
|
||||||
<Row padding="3">
|
<Row padding="3">
|
||||||
<TooltipTrigger delay={0}>
|
<TooltipTrigger delay={0}>
|
||||||
|
|
@ -1,34 +1,27 @@
|
||||||
import {
|
import { Box, Button, Dialog, Icon, Modal, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Column,
|
|
||||||
Dialog,
|
|
||||||
Icon,
|
|
||||||
Modal,
|
|
||||||
Tooltip,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@umami/react-zen';
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { useBoard, useMessages } from '@/components/hooks';
|
import { useBoard, useMessages } from '@/components/hooks';
|
||||||
import { Pencil, Plus, X } from '@/components/icons';
|
import { Pencil, Plus, X } from '@/components/icons';
|
||||||
import type { BoardComponentConfig } from '@/lib/types';
|
import type { BoardComponentConfig } from '@/lib/types';
|
||||||
|
import { getComponentDefinition } from '../boardComponentRegistry';
|
||||||
import styles from './BoardColumn.module.css';
|
import styles from './BoardColumn.module.css';
|
||||||
import { BoardComponentRenderer } from './BoardComponentRenderer';
|
import { BoardComponentRenderer } from './BoardComponentRenderer';
|
||||||
import { BoardComponentSelect } from './BoardComponentSelect';
|
import { BoardComponentSelect } from './BoardComponentSelect';
|
||||||
|
|
||||||
export function BoardColumn({
|
export function BoardEditColumn({
|
||||||
id,
|
id,
|
||||||
component,
|
component,
|
||||||
editing = false,
|
canEdit,
|
||||||
onRemove,
|
onRemove,
|
||||||
onSetComponent,
|
onSetComponent,
|
||||||
canRemove = true,
|
canRemove = true,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
component?: BoardComponentConfig;
|
component?: BoardComponentConfig;
|
||||||
editing?: boolean;
|
canEdit: boolean;
|
||||||
onRemove?: (id: string) => void;
|
onRemove: (id: string) => void;
|
||||||
onSetComponent?: (id: string, config: BoardComponentConfig | null) => void;
|
onSetComponent: (id: string, config: BoardComponentConfig | null) => void;
|
||||||
canRemove?: boolean;
|
canRemove?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [showSelect, setShowSelect] = useState(false);
|
const [showSelect, setShowSelect] = useState(false);
|
||||||
|
|
@ -44,32 +37,33 @@ export function BoardColumn({
|
||||||
}, [component, websiteId]);
|
}, [component, websiteId]);
|
||||||
|
|
||||||
const handleSelect = (config: BoardComponentConfig) => {
|
const handleSelect = (config: BoardComponentConfig) => {
|
||||||
onSetComponent?.(id, config);
|
onSetComponent(id, config);
|
||||||
setShowSelect(false);
|
setShowSelect(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasComponent = !!component;
|
const hasComponent = !!component;
|
||||||
const canRemoveAction = hasComponent || canRemove;
|
const canRemoveAction = hasComponent || canRemove;
|
||||||
|
const title = component ? getComponentDefinition(component.type)?.name : undefined;
|
||||||
|
|
||||||
const handleRemove = () => {
|
const handleRemove = () => {
|
||||||
if (hasComponent) {
|
if (hasComponent) {
|
||||||
onSetComponent?.(id, null);
|
onSetComponent(id, null);
|
||||||
} else {
|
} else {
|
||||||
onRemove?.(id);
|
onRemove(id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column
|
<Panel
|
||||||
|
title={title}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
backgroundColor="surface-sunken"
|
|
||||||
position="relative"
|
position="relative"
|
||||||
className={styles.column}
|
className={styles.column}
|
||||||
>
|
>
|
||||||
{editing && canRemoveAction && (
|
{canEdit && canRemoveAction && (
|
||||||
<Box
|
<Box
|
||||||
className={styles.columnAction}
|
className={styles.columnAction}
|
||||||
position="absolute"
|
position="absolute"
|
||||||
|
|
@ -92,7 +86,7 @@ export function BoardColumn({
|
||||||
<Box width="100%" height="100%" overflow="auto">
|
<Box width="100%" height="100%" overflow="auto">
|
||||||
{renderedComponent}
|
{renderedComponent}
|
||||||
</Box>
|
</Box>
|
||||||
{editing && (
|
{canEdit && (
|
||||||
<Box
|
<Box
|
||||||
className={styles.columnAction}
|
className={styles.columnAction}
|
||||||
position="absolute"
|
position="absolute"
|
||||||
|
|
@ -112,7 +106,7 @@ export function BoardColumn({
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
editing && (
|
canEdit && (
|
||||||
<Button variant="outline" onPress={() => setShowSelect(true)}>
|
<Button variant="outline" onPress={() => setShowSelect(true)}>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Plus />
|
<Plus />
|
||||||
|
|
@ -139,6 +133,6 @@ export function BoardColumn({
|
||||||
)}
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Column>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
46
src/app/(main)/boards/[boardId]/BoardEditLayout.module.css
Normal file
46
src/app/(main)/boards/[boardId]/BoardEditLayout.module.css
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
21
src/app/(main)/boards/[boardId]/BoardEditPage.tsx
Normal file
21
src/app/(main)/boards/[boardId]/BoardEditPage.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
'use client';
|
||||||
|
import { Column } from '@umami/react-zen';
|
||||||
|
import { BoardProvider } from '@/app/(main)/boards/BoardProvider';
|
||||||
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
|
import { BoardControls } from './BoardControls';
|
||||||
|
import { BoardEditBody } from './BoardEditBody';
|
||||||
|
import { BoardEditHeader } from './BoardEditHeader';
|
||||||
|
|
||||||
|
export function BoardEditPage({ boardId }: { boardId?: string }) {
|
||||||
|
return (
|
||||||
|
<BoardProvider boardId={boardId} editing>
|
||||||
|
<PageBody>
|
||||||
|
<Column>
|
||||||
|
<BoardEditHeader />
|
||||||
|
<BoardControls />
|
||||||
|
<BoardEditBody />
|
||||||
|
</Column>
|
||||||
|
</PageBody>
|
||||||
|
</BoardProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,22 +4,23 @@ import { Fragment } from 'react';
|
||||||
import {
|
import {
|
||||||
Group,
|
Group,
|
||||||
type GroupImperativeHandle,
|
type GroupImperativeHandle,
|
||||||
Panel as ResizeablePanel,
|
Panel as ResizablePanel,
|
||||||
Separator,
|
Separator,
|
||||||
} from 'react-resizable-panels';
|
} from 'react-resizable-panels';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { useBoard } from '@/components/hooks';
|
import { useBoard } from '@/components/hooks';
|
||||||
import { ChevronDown, Minus, Plus } from '@/components/icons';
|
import { ChevronDown, GripVertical, Minus, Plus } from '@/components/icons';
|
||||||
import type { BoardColumn as BoardColumnType, BoardComponentConfig } from '@/lib/types';
|
import type { BoardColumn as BoardColumnType, BoardComponentConfig } from '@/lib/types';
|
||||||
import { BoardColumn } from './BoardColumn';
|
import { BoardEditColumn } from './BoardEditColumn';
|
||||||
|
import styles from './BoardEditLayout.module.css';
|
||||||
import { MAX_COLUMNS, MIN_COLUMN_WIDTH } from './boardConstants';
|
import { MAX_COLUMNS, MIN_COLUMN_WIDTH } from './boardConstants';
|
||||||
|
|
||||||
export function BoardRow({
|
export function BoardEditRow({
|
||||||
rowId,
|
rowId,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
rowCount,
|
rowCount,
|
||||||
columns,
|
columns,
|
||||||
editing = false,
|
canEdit,
|
||||||
onRemove,
|
onRemove,
|
||||||
onMoveUp,
|
onMoveUp,
|
||||||
onMoveDown,
|
onMoveDown,
|
||||||
|
|
@ -29,16 +30,16 @@ export function BoardRow({
|
||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
rowCount: number;
|
rowCount: number;
|
||||||
columns: BoardColumnType[];
|
columns: BoardColumnType[];
|
||||||
editing?: boolean;
|
canEdit: boolean;
|
||||||
onRemove?: (id: string) => void;
|
onRemove: (id: string) => void;
|
||||||
onMoveUp?: (id: string) => void;
|
onMoveUp: (id: string) => void;
|
||||||
onMoveDown?: (id: string) => void;
|
onMoveDown: (id: string) => void;
|
||||||
onRegisterRef?: (rowId: string, ref: GroupImperativeHandle | null) => void;
|
onRegisterRef: (rowId: string, ref: GroupImperativeHandle | null) => void;
|
||||||
}) {
|
}) {
|
||||||
const { board, updateBoard } = useBoard();
|
const { board, updateBoard } = useBoard();
|
||||||
|
|
||||||
const handleGroupRef = (ref: GroupImperativeHandle | null) => {
|
const handleGroupRef = (ref: GroupImperativeHandle | null) => {
|
||||||
onRegisterRef?.(rowId, ref);
|
onRegisterRef(rowId, ref);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddColumn = () => {
|
const handleAddColumn = () => {
|
||||||
|
|
@ -81,25 +82,33 @@ export function BoardRow({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group groupRef={handleGroupRef} style={{ height: '100%' }}>
|
<Group groupRef={handleGroupRef} className={styles.rowGroup}>
|
||||||
{columns?.map((column, index) => (
|
{columns?.map((column, index) => (
|
||||||
<Fragment key={column.id}>
|
<Fragment key={column.id}>
|
||||||
<ResizeablePanel id={column.id} minSize={MIN_COLUMN_WIDTH} defaultSize={column.size}>
|
<ResizablePanel id={column.id} minSize={MIN_COLUMN_WIDTH} defaultSize={column.size}>
|
||||||
<BoardColumn
|
<BoardEditColumn
|
||||||
{...column}
|
{...column}
|
||||||
editing={editing}
|
canEdit={canEdit}
|
||||||
onRemove={handleRemoveColumn}
|
onRemove={handleRemoveColumn}
|
||||||
onSetComponent={handleSetComponent}
|
onSetComponent={handleSetComponent}
|
||||||
canRemove={columns?.length > 1}
|
canRemove={columns.length > 1}
|
||||||
/>
|
/>
|
||||||
</ResizeablePanel>
|
</ResizablePanel>
|
||||||
{index < columns?.length - 1 && <Separator />}
|
{index < columns.length - 1 && (
|
||||||
|
<Separator className={styles.columnSeparator}>
|
||||||
|
<span className={styles.separatorHandle}>
|
||||||
|
<Icon size="sm">
|
||||||
|
<GripVertical />
|
||||||
|
</Icon>
|
||||||
|
</span>
|
||||||
|
</Separator>
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
{editing && (
|
{canEdit && (
|
||||||
<Column alignSelf="center" padding="3" gap="1">
|
<Column className={styles.rowActions} padding="3" gap="1">
|
||||||
<TooltipTrigger delay={0}>
|
<TooltipTrigger delay={0}>
|
||||||
<Button variant="outline" onPress={() => onMoveUp?.(rowId)} isDisabled={rowIndex === 0}>
|
<Button variant="outline" onPress={() => onMoveUp(rowId)} isDisabled={rowIndex === 0}>
|
||||||
<Icon rotate={180}>
|
<Icon rotate={180}>
|
||||||
<ChevronDown />
|
<ChevronDown />
|
||||||
</Icon>
|
</Icon>
|
||||||
|
|
@ -110,7 +119,7 @@ export function BoardRow({
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onPress={handleAddColumn}
|
onPress={handleAddColumn}
|
||||||
isDisabled={columns?.length >= MAX_COLUMNS}
|
isDisabled={columns.length >= MAX_COLUMNS}
|
||||||
>
|
>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Plus />
|
<Plus />
|
||||||
|
|
@ -119,7 +128,7 @@ export function BoardRow({
|
||||||
<Tooltip placement="left">Add column</Tooltip>
|
<Tooltip placement="left">Add column</Tooltip>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipTrigger delay={0}>
|
<TooltipTrigger delay={0}>
|
||||||
<Button variant="outline" onPress={() => onRemove?.(rowId)}>
|
<Button variant="outline" onPress={() => onRemove(rowId)}>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Minus />
|
<Minus />
|
||||||
</Icon>
|
</Icon>
|
||||||
|
|
@ -129,7 +138,7 @@ export function BoardRow({
|
||||||
<TooltipTrigger delay={0}>
|
<TooltipTrigger delay={0}>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onPress={() => onMoveDown?.(rowId)}
|
onPress={() => onMoveDown(rowId)}
|
||||||
isDisabled={rowIndex === rowCount - 1}
|
isDisabled={rowIndex === rowCount - 1}
|
||||||
>
|
>
|
||||||
<Icon>
|
<Icon>
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { useBoard } from '@/components/hooks';
|
|
||||||
import { BoardEditHeader } from './BoardEditHeader';
|
|
||||||
import { BoardViewHeader } from './BoardViewHeader';
|
|
||||||
|
|
||||||
export function BoardHeader() {
|
|
||||||
const { board, editing } = useBoard();
|
|
||||||
|
|
||||||
if (editing) {
|
|
||||||
return <BoardEditHeader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <BoardViewHeader />;
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
'use client';
|
|
||||||
import { Column } from '@umami/react-zen';
|
|
||||||
import { BoardBody } from '@/app/(main)/boards/[boardId]/BoardBody';
|
|
||||||
import { BoardHeader } from '@/app/(main)/boards/[boardId]/BoardHeader';
|
|
||||||
import { BoardProvider } from '@/app/(main)/boards/BoardProvider';
|
|
||||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
|
||||||
import { PageBody } from '@/components/common/PageBody';
|
|
||||||
import { useBoard } from '@/components/hooks';
|
|
||||||
|
|
||||||
export function BoardPage({ boardId, editing = false }: { boardId?: string; editing?: boolean }) {
|
|
||||||
return (
|
|
||||||
<BoardProvider boardId={boardId} editing={editing}>
|
|
||||||
<PageBody>
|
|
||||||
<Column>
|
|
||||||
<BoardHeader />
|
|
||||||
<BoardControls />
|
|
||||||
<BoardBody />
|
|
||||||
</Column>
|
|
||||||
</PageBody>
|
|
||||||
</BoardProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BoardControls() {
|
|
||||||
const { board } = useBoard();
|
|
||||||
const websiteId = board?.parameters?.websiteId;
|
|
||||||
|
|
||||||
if (!websiteId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <WebsiteControls websiteId={websiteId} allowCompare={true} />;
|
|
||||||
}
|
|
||||||
15
src/app/(main)/boards/[boardId]/BoardViewBody.tsx
Normal file
15
src/app/(main)/boards/[boardId]/BoardViewBody.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { useBoard } from '@/components/hooks';
|
||||||
|
import { BoardViewRow } from './BoardViewRow';
|
||||||
|
|
||||||
|
export function BoardViewBody() {
|
||||||
|
const { board } = useBoard();
|
||||||
|
const rows = board?.parameters?.rows ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
|
{rows.map(row => (
|
||||||
|
<BoardViewRow key={row.id} columns={row.columns} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/app/(main)/boards/[boardId]/BoardViewColumn.tsx
Normal file
27
src/app/(main)/boards/[boardId]/BoardViewColumn.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Box, Column } from '@umami/react-zen';
|
||||||
|
import { Panel } from '@/components/common/Panel';
|
||||||
|
import { useBoard } from '@/components/hooks';
|
||||||
|
import type { BoardComponentConfig } from '@/lib/types';
|
||||||
|
import { getComponentDefinition } from '../boardComponentRegistry';
|
||||||
|
import { BoardComponentRenderer } from './BoardComponentRenderer';
|
||||||
|
|
||||||
|
export function BoardViewColumn({ component }: { component?: BoardComponentConfig }) {
|
||||||
|
const { board } = useBoard();
|
||||||
|
const websiteId = board?.parameters?.websiteId;
|
||||||
|
|
||||||
|
if (!component || !websiteId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = getComponentDefinition(component.type)?.name;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel title={title} height="100%">
|
||||||
|
<Column width="100%" height="100%">
|
||||||
|
<Box width="100%" overflow="auto">
|
||||||
|
<BoardComponentRenderer config={component} websiteId={websiteId} />
|
||||||
|
</Box>
|
||||||
|
</Column>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/app/(main)/boards/[boardId]/BoardViewPage.tsx
Normal file
21
src/app/(main)/boards/[boardId]/BoardViewPage.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
'use client';
|
||||||
|
import { Column } from '@umami/react-zen';
|
||||||
|
import { BoardProvider } from '@/app/(main)/boards/BoardProvider';
|
||||||
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
|
import { BoardControls } from './BoardControls';
|
||||||
|
import { BoardViewBody } from './BoardViewBody';
|
||||||
|
import { BoardViewHeader } from './BoardViewHeader';
|
||||||
|
|
||||||
|
export function BoardViewPage({ boardId }: { boardId: string }) {
|
||||||
|
return (
|
||||||
|
<BoardProvider boardId={boardId}>
|
||||||
|
<PageBody>
|
||||||
|
<Column>
|
||||||
|
<BoardViewHeader />
|
||||||
|
<BoardControls />
|
||||||
|
<BoardViewBody />
|
||||||
|
</Column>
|
||||||
|
</PageBody>
|
||||||
|
</BoardProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/app/(main)/boards/[boardId]/BoardViewRow.tsx
Normal file
21
src/app/(main)/boards/[boardId]/BoardViewRow.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
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' }}>
|
||||||
|
{columns.map(column => (
|
||||||
|
<div
|
||||||
|
key={column.id}
|
||||||
|
style={{
|
||||||
|
flex: `${column.size ?? 1} 1 0%`,
|
||||||
|
minWidth: MIN_COLUMN_WIDTH,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BoardViewColumn component={column.component} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { BoardPage } from '../BoardPage';
|
import { BoardEditPage } from '../BoardEditPage';
|
||||||
|
|
||||||
export default async function ({ params }: { params: Promise<{ boardId: string }> }) {
|
export default async function ({ params }: { params: Promise<{ boardId: string }> }) {
|
||||||
const { boardId } = await params;
|
const { boardId } = await params;
|
||||||
|
|
||||||
return <BoardPage boardId={boardId} editing />;
|
return <BoardEditPage boardId={boardId} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { BoardPage } from './BoardPage';
|
import { BoardEditPage } from './BoardEditPage';
|
||||||
|
import { BoardViewPage } from './BoardViewPage';
|
||||||
|
|
||||||
export default async function ({ params }: { params: Promise<{ boardId: string }> }) {
|
export default async function ({ params }: { params: Promise<{ boardId: string }> }) {
|
||||||
const { boardId } = await params;
|
const { boardId } = await params;
|
||||||
const isCreate = boardId === 'create';
|
const isCreate = boardId === 'create';
|
||||||
|
|
||||||
return <BoardPage boardId={isCreate ? undefined : boardId} editing={isCreate} />;
|
if (isCreate) {
|
||||||
|
return <BoardEditPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BoardViewPage boardId={boardId} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue