Skip to content

Commit 8563fe1

Browse files
committed
Add TextBlock board component for free-form text content
New component that doesn't require a website, with textarea config field. Added requiresWebsite flag to ComponentDefinition so board components can opt out of the websiteId requirement.
1 parent f097732 commit 8563fe1

File tree

5 files changed

+66
-19
lines changed

5 files changed

+66
-19
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use client';
2+
import { Text } from '@umami/react-zen';
3+
4+
export function TextBlock({ text }: { text?: string }) {
5+
if (!text) {
6+
return null;
7+
}
8+
9+
return <Text style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{text}</Text>;
10+
}

src/app/(main)/boards/[boardId]/BoardComponentRenderer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ function BoardComponentRendererComponent({
2222

2323
const Component = definition.component;
2424

25-
if (!websiteId) {
25+
if (!websiteId && definition.requiresWebsite !== false) {
2626
return (
2727
<Column alignItems="center" justifyContent="center" width="100%" height="100%">
2828
<Text color="muted">Select a website</Text>
2929
</Column>
3030
);
3131
}
3232

33-
return <Component websiteId={websiteId} {...config.props} />;
33+
return <Component {...(websiteId ? { websiteId } : {})} {...config.props} />;
3434
}
3535

3636
export const BoardComponentRenderer = memo(

src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,10 @@ export function BoardComponentSelect({
9797
setConfigValues(prev => ({ ...prev, [name]: value }));
9898
};
9999

100+
const needsWebsite = selectedDef?.requiresWebsite !== false;
101+
100102
const handleAdd = () => {
101-
if (!selectedDef || !selectedWebsiteId) return;
103+
if (!selectedDef || (needsWebsite && !selectedWebsiteId)) return;
102104

103105
const props: Record<string, any> = {};
104106

@@ -116,7 +118,7 @@ export function BoardComponentSelect({
116118

117119
const config: BoardComponentConfig = {
118120
type: selectedDef.type,
119-
websiteId: selectedWebsiteId,
121+
...(needsWebsite ? { websiteId: selectedWebsiteId } : {}),
120122
title,
121123
description,
122124
};
@@ -137,7 +139,7 @@ export function BoardComponentSelect({
137139
}
138140
: null;
139141

140-
const canSave = !!selectedDef && !!selectedWebsiteId;
142+
const canSave = !!selectedDef && (!needsWebsite || !!selectedWebsiteId);
141143

142144
return (
143145
<Column gap="4">
@@ -184,7 +186,7 @@ export function BoardComponentSelect({
184186

185187
<Column gap="3" flexGrow={1} style={{ minWidth: 0 }}>
186188
<Panel maxHeight="100%">
187-
{previewConfig && selectedWebsiteId ? (
189+
{previewConfig && (!needsWebsite || selectedWebsiteId) ? (
188190
<BoardComponentRenderer config={previewConfig} websiteId={selectedWebsiteId} />
189191
) : (
190192
<Column alignItems="center" justifyContent="center" height="100%">
@@ -201,17 +203,19 @@ export function BoardComponentSelect({
201203
<Column gap="3" style={{ width: 320, flexShrink: 0, overflowY: 'auto' }}>
202204
<Text weight="bold">{t(labels.properties)}</Text>
203205

204-
<Column gap="2">
205-
<Text size="sm" color="muted">
206-
{t(labels.website)}
207-
</Text>
208-
<WebsiteSelect
209-
websiteId={selectedWebsiteId}
210-
teamId={teamId}
211-
placeholder={t(labels.selectWebsite)}
212-
onChange={setSelectedWebsiteId}
213-
/>
214-
</Column>
206+
{needsWebsite && (
207+
<Column gap="2">
208+
<Text size="sm" color="muted">
209+
{t(labels.website)}
210+
</Text>
211+
<WebsiteSelect
212+
websiteId={selectedWebsiteId}
213+
teamId={teamId}
214+
placeholder={t(labels.selectWebsite)}
215+
onChange={setSelectedWebsiteId}
216+
/>
217+
</Column>
218+
)}
215219

216220
<Column gap="2">
217221
<Text size="sm" color="muted">
@@ -262,6 +266,15 @@ export function BoardComponentSelect({
262266
onChange={(value: string) => handleConfigChange(field.name, value)}
263267
/>
264268
)}
269+
270+
{field.type === 'textarea' && (
271+
<TextField
272+
asTextArea
273+
value={String(configValues[field.name] ?? field.defaultValue ?? '')}
274+
onChange={(value: string) => handleConfigChange(field.name, value)}
275+
style={{ minHeight: 200 }}
276+
/>
277+
)}
265278
</Column>
266279
))}
267280
</Column>

src/app/(main)/boards/[boardId]/BoardViewColumn.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import { Box, Column } from '@umami/react-zen';
22
import { Panel } from '@/components/common/Panel';
33
import { useBoard } from '@/components/hooks';
44
import type { BoardComponentConfig } from '@/lib/types';
5+
import { getComponentDefinition } from '../boardComponentRegistry';
56
import { BoardComponentRenderer } from './BoardComponentRenderer';
67

78
export function BoardViewColumn({ component }: { component?: BoardComponentConfig }) {
89
const { board } = useBoard();
10+
const definition = component ? getComponentDefinition(component.type) : undefined;
911
const websiteId = component?.websiteId || board?.parameters?.websiteId;
1012

11-
if (!component || !websiteId) {
13+
if (!component || (!websiteId && definition?.requiresWebsite !== false)) {
1214
return null;
1315
}
1416

src/app/(main)/boards/boardComponentRegistry.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ComponentType } from 'react';
2+
import { TextBlock } from '@/app/(main)/boards/TextBlock';
23
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
34
import { WebsiteMetricsBar } from '@/app/(main)/websites/[websiteId]/WebsiteMetricsBar';
45
import { EventsChart } from '@/components/metrics/EventsChart';
@@ -9,7 +10,7 @@ import { WorldMap } from '@/components/metrics/WorldMap';
910
export interface ConfigField {
1011
name: string;
1112
label: string;
12-
type: 'select' | 'number' | 'text';
13+
type: 'select' | 'number' | 'text' | 'textarea';
1314
options?: { label: string; value: string }[];
1415
defaultValue?: any;
1516
}
@@ -22,12 +23,14 @@ export interface ComponentDefinition {
2223
component: ComponentType<any>;
2324
defaultProps?: Record<string, any>;
2425
configFields?: ConfigField[];
26+
requiresWebsite?: boolean;
2527
}
2628

2729
export const CATEGORIES = [
2830
{ key: 'overview', name: 'Overview' },
2931
{ key: 'tables', name: 'Tables' },
3032
{ key: 'visualization', name: 'Visualization' },
33+
{ key: 'content', name: 'Content' },
3134
] as const;
3235

3336
const METRIC_TYPES = [
@@ -121,6 +124,25 @@ const componentDefinitions: ComponentDefinition[] = [
121124
category: 'visualization',
122125
component: EventsChart,
123126
},
127+
128+
// Content
129+
{
130+
type: 'TextBlock',
131+
name: 'Text',
132+
description: 'Free-form text content',
133+
category: 'content',
134+
component: TextBlock,
135+
requiresWebsite: false,
136+
defaultProps: { text: '' },
137+
configFields: [
138+
{
139+
name: 'text',
140+
label: 'Text',
141+
type: 'textarea',
142+
defaultValue: '',
143+
},
144+
],
145+
},
124146
];
125147

126148
const definitionMap = new Map(componentDefinitions.map(def => [def.type, def]));

0 commit comments

Comments
 (0)