@columenlabs/lumen-ui 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lumen-layout.js +6 -3
- package/dist/lumen-widget.d.ts +107 -1
- package/dist/lumen-widget.js +27 -12
- package/dist/sourced-card.d.ts +10 -0
- package/dist/sourced-card.js +69 -0
- package/dist/view-editor.d.ts +1 -1
- package/dist/view-editor.js +187 -46
- package/dist/widget-data.d.ts +4 -0
- package/dist/widget-data.js +32 -0
- package/dist/widget-settings.d.ts +80 -0
- package/package.json +2 -2
package/dist/lumen-layout.js
CHANGED
|
@@ -32,6 +32,7 @@ import { LumenViewCard } from './lumen-view-card.js';
|
|
|
32
32
|
import { AddWidgetDialog, LayoutActions, LayoutChip, LumenSavedWidget, LumenWidget, cardFilterFields, cardTitle, cardsAfter, } from './lumen-widget.js';
|
|
33
33
|
import { OriginLegend } from './origin-legend.js';
|
|
34
34
|
import { IconButton, Page } from './page.js';
|
|
35
|
+
import { SourcedCard } from './sourced-card.js';
|
|
35
36
|
import { LumenCard, SummaryTiles } from './summary-cards.js';
|
|
36
37
|
import { Button } from './ui/button.js';
|
|
37
38
|
import { ViewEditDialog } from './view-editor.js';
|
|
@@ -534,7 +535,7 @@ function ViewLayout({ crumbs, actions, meta, status, intro, cards: extra, lead,
|
|
|
534
535
|
return {
|
|
535
536
|
...place,
|
|
536
537
|
wide: isTableCard(c) && c.w === GRID_COLS && c.h === undefined,
|
|
537
|
-
node: isTableCard(c) && c.id !== viewCardId ? (_jsx(LayoutTable, { card: c, base: joins.domain, config: config, rows: c.filter.length ? filterByTokens(rows, widgetFields, c.filter) : rows, load: scope(c.id, load), state: state, onDetails: onDetails, controls: controls, onLayout: onConfigChange ? (table) => putCard({ ...c, table }) : undefined })) : isTableCard(c) ? (_jsx(LumenView, { domain: domain, config: config, rows: viewRows, total: rows.length, groupBy: group.groupField, pivot: pivot, subgroup: group.sub, load: scope(c.id, load), state: state, title: c.title ?? config.name, selection: selection, onDetails: rowPick.onTap ?? onDetails, tapped: rowPick.tapped, groupActions: groupActions, controls: controls, inLayout: true, tools: viewTools, filter: viewFilter, filterCount: filter.tokens.length, strip: _jsx(ConnectOffer, { domain: domain, rows: rows, config: config, read: joins.read, cardKey: viewId ?? config.name, onConnect: onConfigChange ? connect : undefined }), link: cardLink(c.id, viewFollows, viewColor ? filter.tokens.length : 0), shown: c.controls, onColumns: onConfigChange && columnPicker !== false
|
|
538
|
+
node: c.source ? (_jsx(SourcedCard, { card: c, name: config.name, controls: controls })) : isTableCard(c) && c.id !== viewCardId ? (_jsx(LayoutTable, { card: c, base: joins.domain, config: config, rows: c.filter.length ? filterByTokens(rows, widgetFields, c.filter) : rows, load: scope(c.id, load), state: state, onDetails: onDetails, controls: controls, onLayout: onConfigChange ? (table) => putCard({ ...c, table }) : undefined })) : isTableCard(c) ? (_jsx(LumenView, { domain: domain, config: config, rows: viewRows, total: rows.length, groupBy: group.groupField, pivot: pivot, subgroup: group.sub, load: scope(c.id, load), state: state, title: c.title ?? config.name, selection: selection, onDetails: rowPick.onTap ?? onDetails, tapped: rowPick.tapped, groupActions: groupActions, controls: controls, inLayout: true, tools: viewTools, filter: viewFilter, filterCount: filter.tokens.length, strip: _jsx(ConnectOffer, { domain: domain, rows: rows, config: config, read: joins.read, cardKey: viewId ?? config.name, onConnect: onConfigChange ? connect : undefined }), link: cardLink(c.id, viewFollows, viewColor ? filter.tokens.length : 0), shown: c.controls, onColumns: onConfigChange && columnPicker !== false
|
|
538
539
|
? (columns) => onConfigChange({ ...config, columns })
|
|
539
540
|
: undefined, onAddColumn: addColumn, sort: heldSort, onSort: setHeldSort })) : (_jsx(LumenSavedWidget, { widget: c, title: cardTitle(c, config.name), domain: domain, rows: narrow(rows, followed(c.id)), link: cardLink(c.id, followed(c.id)), fields: widgetFields, merge: mergeFlat, onDetails: onDetails, load: scope(c.id, load), tiles: 'tile' in c
|
|
540
541
|
? cardOf(c)
|
|
@@ -607,10 +608,12 @@ function ViewLayout({ crumbs, actions, meta, status, intro, cards: extra, lead,
|
|
|
607
608
|
} })) : (onConfigChange && (_jsxs(_Fragment, { children: [changed && saveMode === 'document' && (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: Undo2Icon, label: "Revert view", onClick: revert }), _jsx(IconButton, { icon: SaveIcon, label: "Save view (chips, sort, group)", variant: "default", onClick: () => onConfigChange?.(heldConfig) })] })), _jsx(IconButton, { icon: LayoutDashboardIcon, label: "Edit layout", onClick: () => {
|
|
608
609
|
setDraft(saved);
|
|
609
610
|
setLayout(true);
|
|
610
|
-
} })] }))), children: [_jsx(LinksProvider, { links: links, children: _jsx(Scroller, { fit: fit, children: body }) }), _jsx(LumenDrawer, { open: open !== null, title: openTitle, onClose: () => setDetails(null), children: open !== null && (_jsx(DetailsBody, { domain: domain, row: open, columns: columns, chips: config.chips ?? [] })) }), onConfigChange && (_jsxs(_Fragment, { children: [_jsx(AddWidgetDialog, { open: adding, onOpenChange: setAdding, domain: domain, cards: cards, onAdd: (c) => {
|
|
611
|
+
} })] }))), children: [_jsx(LinksProvider, { links: links, children: _jsx(Scroller, { fit: fit, children: body }) }), _jsx(LumenDrawer, { open: open !== null, title: openTitle, onClose: () => setDetails(null), children: open !== null && (_jsx(DetailsBody, { domain: domain, row: open, columns: columns, chips: config.chips ?? [] })) }), onConfigChange && (_jsxs(_Fragment, { children: [_jsx(AddWidgetDialog, { open: adding, onOpenChange: setAdding, domain: domain, cards: cards, name: config.name, onAdd: (c) => {
|
|
611
612
|
setAdding(false);
|
|
612
613
|
// A second table starts as a copy of the view's layout, then goes its own way.
|
|
613
|
-
putCard(isTableCard(c) && mainTable
|
|
614
|
+
putCard(isTableCard(c) && mainTable && !c.source
|
|
615
|
+
? { ...c, table: tableLayoutOf(config) }
|
|
616
|
+
: c);
|
|
614
617
|
}, onBuild: (c) => {
|
|
615
618
|
setAdding(false);
|
|
616
619
|
if (c.kind !== 'page')
|
package/dist/lumen-widget.d.ts
CHANGED
|
@@ -144,11 +144,13 @@ export declare function draftOf<T>(styleId: string, domain: ViewDomain<T>, cards
|
|
|
144
144
|
* of $1 a domain that draws curves. */
|
|
145
145
|
export declare function useOffered<T>(domain: ViewDomain<T>): (styleId: string) => boolean;
|
|
146
146
|
/** Add widget: the domain's pre-built widgets, or a kind to build. */
|
|
147
|
-
export declare function AddWidgetDialog<T>({ open, onOpenChange, domain, cards, onAdd, onBuild, }: {
|
|
147
|
+
export declare function AddWidgetDialog<T>({ open, onOpenChange, domain, cards, onAdd, onBuild, name, }: {
|
|
148
148
|
open: boolean;
|
|
149
149
|
onOpenChange: (o: boolean) => void;
|
|
150
150
|
domain: ViewDomain<T>;
|
|
151
151
|
cards: ViewCard[];
|
|
152
|
+
/** The page's name, for the data picker's "this page" row. */
|
|
153
|
+
name?: string;
|
|
152
154
|
/** Adds a ready card (a pre-built widget, or the view's tiles or the view itself). */
|
|
153
155
|
onAdd: (card: ViewCard) => void;
|
|
154
156
|
/** Opens Edit widget on a new card of a kind. */
|
|
@@ -175,6 +177,14 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
175
177
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
176
178
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
177
179
|
}[];
|
|
180
|
+
source?: string | undefined;
|
|
181
|
+
connections?: {
|
|
182
|
+
to: string;
|
|
183
|
+
on: {
|
|
184
|
+
from: string;
|
|
185
|
+
to: string;
|
|
186
|
+
}[];
|
|
187
|
+
}[] | undefined;
|
|
178
188
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
179
189
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
180
190
|
kind: "table";
|
|
@@ -230,6 +240,14 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
230
240
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
231
241
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
232
242
|
}[];
|
|
243
|
+
source?: string | undefined;
|
|
244
|
+
connections?: {
|
|
245
|
+
to: string;
|
|
246
|
+
on: {
|
|
247
|
+
from: string;
|
|
248
|
+
to: string;
|
|
249
|
+
}[];
|
|
250
|
+
}[] | undefined;
|
|
233
251
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
234
252
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
235
253
|
kind: "table";
|
|
@@ -257,6 +275,14 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
257
275
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
258
276
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
259
277
|
}[];
|
|
278
|
+
source?: string | undefined;
|
|
279
|
+
connections?: {
|
|
280
|
+
to: string;
|
|
281
|
+
on: {
|
|
282
|
+
from: string;
|
|
283
|
+
to: string;
|
|
284
|
+
}[];
|
|
285
|
+
}[] | undefined;
|
|
260
286
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
261
287
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
262
288
|
kind: "metric";
|
|
@@ -325,6 +351,14 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
325
351
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
326
352
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
327
353
|
}[];
|
|
354
|
+
source?: string | undefined;
|
|
355
|
+
connections?: {
|
|
356
|
+
to: string;
|
|
357
|
+
on: {
|
|
358
|
+
from: string;
|
|
359
|
+
to: string;
|
|
360
|
+
}[];
|
|
361
|
+
}[] | undefined;
|
|
328
362
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
329
363
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
330
364
|
kind: "metric";
|
|
@@ -387,6 +421,14 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
387
421
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
388
422
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
389
423
|
}[];
|
|
424
|
+
source?: string | undefined;
|
|
425
|
+
connections?: {
|
|
426
|
+
to: string;
|
|
427
|
+
on: {
|
|
428
|
+
from: string;
|
|
429
|
+
to: string;
|
|
430
|
+
}[];
|
|
431
|
+
}[] | undefined;
|
|
390
432
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
391
433
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
392
434
|
w: number;
|
|
@@ -416,6 +458,14 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
416
458
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
417
459
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
418
460
|
}[];
|
|
461
|
+
source?: string | undefined;
|
|
462
|
+
connections?: {
|
|
463
|
+
to: string;
|
|
464
|
+
on: {
|
|
465
|
+
from: string;
|
|
466
|
+
to: string;
|
|
467
|
+
}[];
|
|
468
|
+
}[] | undefined;
|
|
419
469
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
420
470
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
421
471
|
kind: "allocation";
|
|
@@ -443,6 +493,14 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
443
493
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
444
494
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
445
495
|
}[];
|
|
496
|
+
source?: string | undefined;
|
|
497
|
+
connections?: {
|
|
498
|
+
to: string;
|
|
499
|
+
on: {
|
|
500
|
+
from: string;
|
|
501
|
+
to: string;
|
|
502
|
+
}[];
|
|
503
|
+
}[] | undefined;
|
|
446
504
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
447
505
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
448
506
|
kind: "allocation";
|
|
@@ -470,6 +528,14 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
470
528
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
471
529
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
472
530
|
}[];
|
|
531
|
+
source?: string | undefined;
|
|
532
|
+
connections?: {
|
|
533
|
+
to: string;
|
|
534
|
+
on: {
|
|
535
|
+
from: string;
|
|
536
|
+
to: string;
|
|
537
|
+
}[];
|
|
538
|
+
}[] | undefined;
|
|
473
539
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
474
540
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
475
541
|
kind: "allocation";
|
|
@@ -500,6 +566,14 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
500
566
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
501
567
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
502
568
|
}[];
|
|
569
|
+
source?: string | undefined;
|
|
570
|
+
connections?: {
|
|
571
|
+
to: string;
|
|
572
|
+
on: {
|
|
573
|
+
from: string;
|
|
574
|
+
to: string;
|
|
575
|
+
}[];
|
|
576
|
+
}[] | undefined;
|
|
503
577
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
504
578
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
505
579
|
kind: "chart";
|
|
@@ -527,6 +601,14 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
527
601
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
528
602
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
529
603
|
}[];
|
|
604
|
+
source?: string | undefined;
|
|
605
|
+
connections?: {
|
|
606
|
+
to: string;
|
|
607
|
+
on: {
|
|
608
|
+
from: string;
|
|
609
|
+
to: string;
|
|
610
|
+
}[];
|
|
611
|
+
}[] | undefined;
|
|
530
612
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
531
613
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
532
614
|
kind: "chart";
|
|
@@ -552,6 +634,14 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
552
634
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
553
635
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
554
636
|
}[];
|
|
637
|
+
source?: string | undefined;
|
|
638
|
+
connections?: {
|
|
639
|
+
to: string;
|
|
640
|
+
on: {
|
|
641
|
+
from: string;
|
|
642
|
+
to: string;
|
|
643
|
+
}[];
|
|
644
|
+
}[] | undefined;
|
|
555
645
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
556
646
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
557
647
|
kind: "chart";
|
|
@@ -577,6 +667,14 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
577
667
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
578
668
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
579
669
|
}[];
|
|
670
|
+
source?: string | undefined;
|
|
671
|
+
connections?: {
|
|
672
|
+
to: string;
|
|
673
|
+
on: {
|
|
674
|
+
from: string;
|
|
675
|
+
to: string;
|
|
676
|
+
}[];
|
|
677
|
+
}[] | undefined;
|
|
580
678
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
581
679
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
582
680
|
w: number;
|
|
@@ -606,6 +704,14 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
606
704
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
607
705
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
608
706
|
}[];
|
|
707
|
+
source?: string | undefined;
|
|
708
|
+
connections?: {
|
|
709
|
+
to: string;
|
|
710
|
+
on: {
|
|
711
|
+
from: string;
|
|
712
|
+
to: string;
|
|
713
|
+
}[];
|
|
714
|
+
}[] | undefined;
|
|
609
715
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
610
716
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
611
717
|
kind: "page";
|
package/dist/lumen-widget.js
CHANGED
|
@@ -7,7 +7,7 @@ import { OUTLOOK_INDEXES, marketWindows, } from '@columenlabs/lumen-ui-types/mar
|
|
|
7
7
|
// cards out; this file draws the card frame every card shares (its header, ↻ and ⋯ menu), each
|
|
8
8
|
// widget, the Edit layout bar, Add widget (Pre-built and Build your own) and Edit widget.
|
|
9
9
|
import { ALLOCATION_MAX, VIEW_CARD_KIND_NAMES, WIDGET_STYLES, controlOn, isDollarFormat, isRowCard, isTableCard, trendBuckets, viewCardKinds, viewCardSchema, viewWidgetCatalog, widgetStyleId, widgetStyleOf, } from '@columenlabs/lumen-ui-types/views';
|
|
10
|
-
import { ActivityIcon, ArrowDownIcon, ArrowUpIcon, BarChart3Icon, CandlestickChartIcon, ChartLineIcon, ChartPieIcon, CheckIcon, CodeIcon, CopyIcon, EyeOffIcon, FoldVerticalIcon, FunnelIcon, GripVerticalIcon, InfoIcon, LayoutGridIcon, ListOrderedIcon, MoreHorizontalIcon, PackageIcon, PencilIcon, PlusIcon, RotateCcwIcon, Rows3Icon, SparklesIcon, SquareIcon, TableIcon, Trash2Icon, TrendingUpIcon, WrenchIcon, XIcon, } from 'lucide-react';
|
|
10
|
+
import { ActivityIcon, ArrowDownIcon, ArrowUpIcon, BarChart3Icon, CandlestickChartIcon, ChartLineIcon, ChartPieIcon, CheckIcon, CodeIcon, CopyIcon, DatabaseIcon, EyeOffIcon, FoldVerticalIcon, FunnelIcon, GripVerticalIcon, InfoIcon, LayoutGridIcon, ListOrderedIcon, MoreHorizontalIcon, PackageIcon, PencilIcon, PlusIcon, RotateCcwIcon, Rows3Icon, SparklesIcon, SquareIcon, TableIcon, Trash2Icon, TrendingUpIcon, WrenchIcon, XIcon, } from 'lucide-react';
|
|
11
11
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
12
12
|
import { useAssistant, useAssistantName } from './assistant-context.js';
|
|
13
13
|
import { LumenConfirm } from './confirm.js';
|
|
@@ -15,6 +15,7 @@ import { FoldChevron, useFolded, usePageKey } from './fold.js';
|
|
|
15
15
|
import { GrowthChart } from './growth-chart.js';
|
|
16
16
|
import { useIsMobile } from './hooks/use-mobile.js';
|
|
17
17
|
import { IconButton } from './icon-button.js';
|
|
18
|
+
import { useDomainReads } from './joins.js';
|
|
18
19
|
import { cn } from './lib/utils.js';
|
|
19
20
|
import { LumenBoundary } from './lumen-boundary.js';
|
|
20
21
|
import { LumenTokenizedFilter, filterByTokens, useLumenFilter, } from './lumen-filter.js';
|
|
@@ -48,6 +49,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
|
|
|
48
49
|
import { fieldOptions } from './view-formulas.js';
|
|
49
50
|
import { computedOf, label, summaryCash, } from './view-model.js';
|
|
50
51
|
import { WidgetControlsEditor } from './widget-controls.js';
|
|
52
|
+
import { PAGE_SOURCE, useWidgetDataOptions } from './widget-data.js';
|
|
51
53
|
import { dateFields } from './widget-settings.js';
|
|
52
54
|
/** Each style's icon, by its id in `WIDGET_STYLES`; a style left out takes its kind's. */
|
|
53
55
|
const STYLE_ICONS = {
|
|
@@ -513,13 +515,26 @@ function PickCard({ icon: Icon, name, note, added, disabled, onClick, }) {
|
|
|
513
515
|
return (_jsxs(Button, { variant: "outline", disabled: disabled, onClick: onClick, className: "h-auto justify-start gap-3 p-3 text-left font-normal whitespace-normal", children: [_jsx("span", { className: "grid size-8 shrink-0 place-items-center rounded-md bg-primary/10 text-primary", children: _jsx(Icon, { className: "size-4" }) }), _jsxs("span", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx("span", { className: "font-medium", children: name }), _jsx("span", { className: "text-xs text-muted-foreground", children: note })] }), added && (_jsxs(Badge, { variant: "secondary", className: "ml-auto font-normal", children: [_jsx(CheckIcon, {}), " Added"] }))] }));
|
|
514
516
|
}
|
|
515
517
|
/** Add widget: the domain's pre-built widgets, or a kind to build. */
|
|
516
|
-
export function AddWidgetDialog({ open, onOpenChange, domain, cards, onAdd, onBuild, }) {
|
|
517
|
-
|
|
518
|
+
export function AddWidgetDialog({ open, onOpenChange, domain, cards, onAdd, onBuild, name = 'This page', }) {
|
|
519
|
+
// Step 1: the widget's main data, this page's rows unless another is picked.
|
|
520
|
+
const [data, setData] = useState(PAGE_SOURCE);
|
|
521
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: starts on this page each time it opens.
|
|
522
|
+
useEffect(() => setData(PAGE_SOURCE), [open]);
|
|
523
|
+
const pageLabel = `${name} (this page)`;
|
|
524
|
+
const dataOptions = useWidgetDataOptions(open, pageLabel);
|
|
525
|
+
const own = data !== PAGE_SOURCE;
|
|
526
|
+
const ids = useMemo(() => (own ? [data] : []), [own, data]);
|
|
527
|
+
const read = useDomainReads(ids, ids)[data];
|
|
528
|
+
const shapeDomain = (own ? (read?.domain ?? { id: data, rowKey: String, fields: [] }) : domain);
|
|
529
|
+
const sourced = (c) => (own ? { ...c, source: data } : c);
|
|
530
|
+
const catalog = own ? [] : (viewWidgetCatalog[domain.id] ?? []);
|
|
518
531
|
const keys = new Set(domain.fields.map((f) => f.key));
|
|
519
|
-
const hasRow = cards.some(isRowCard);
|
|
532
|
+
const hasRow = !own && cards.some(isRowCard);
|
|
520
533
|
const full = cards.length >= 12;
|
|
521
|
-
const offered = useOffered(
|
|
522
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Add widget" }), _jsxs(DialogDescription, { children: ["
|
|
534
|
+
const offered = useOffered(shapeDomain);
|
|
535
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Add widget" }), _jsxs(DialogDescription, { children: ["Pick its data, then its shape", full ? '. A page holds 12 cards at most.' : '.'] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "add-widget-data", className: "text-xs font-medium text-muted-foreground", children: "Data" }), _jsx(LumenSelector, { id: "add-widget-data", icon: DatabaseIcon, label: "Widget data", search: true, value: data, options: dataOptions, onChange: setData }), own && read?.state !== 'ok' && (_jsx("p", { className: cn('text-xs', read?.state === 'loading' || !read ? 'text-muted-foreground' : 'text-destructive'), children: read?.state === 'loading' || !read
|
|
536
|
+
? 'Reading its fields…'
|
|
537
|
+
: (read.detail ?? 'Its rows could not be read.') }))] }), _jsxs(Tabs, { defaultValue: catalog.length ? 'prebuilt' : 'own', children: [_jsxs(TabsList, { children: [!own && (_jsxs(TabsTrigger, { value: "prebuilt", children: [_jsx(PackageIcon, {}), "Pre-built"] })), _jsxs(TabsTrigger, { value: "own", children: [_jsx(WrenchIcon, {}), "Build your own"] })] }), _jsx(TabsContent, { value: "prebuilt", className: dialogBody, children: catalog.length ? (_jsx("div", { className: "grid grid-cols-1 gap-2 sm:grid-cols-2", children: catalog.map((it) => {
|
|
523
538
|
const added = cards.some((b) => b.id === it.widget.id);
|
|
524
539
|
const fits = [
|
|
525
540
|
...('by' in it.widget ? [it.widget.by, it.widget.value] : []),
|
|
@@ -536,16 +551,16 @@ export function AddWidgetDialog({ open, onOpenChange, domain, cards, onAdd, onBu
|
|
|
536
551
|
// A table goes on as it is; the page's first row of numbers shows the
|
|
537
552
|
// view's cards, any other starts empty in Edit widget.
|
|
538
553
|
const ready = w.id === 'table:table' || (w.id === 'metric:row' && !hasRow);
|
|
539
|
-
return (_jsx(PickCard, { icon: cardIcon(w), name: w.name, note: w.note, disabled: full, onClick: () => ready
|
|
540
|
-
? onAdd(draftOf(w.id,
|
|
541
|
-
: onBuild(w.id === 'metric:row'
|
|
554
|
+
return (_jsx(PickCard, { icon: cardIcon(w), name: w.name, note: w.note, disabled: full || (own && read?.state !== 'ok'), onClick: () => ready
|
|
555
|
+
? onAdd(sourced(draftOf(w.id, shapeDomain, cards)))
|
|
556
|
+
: onBuild(sourced(w.id === 'metric:row'
|
|
542
557
|
? viewCardSchema.parse({
|
|
543
|
-
...draftOf(w.id,
|
|
558
|
+
...draftOf(w.id, shapeDomain, cards),
|
|
544
559
|
tiles: [],
|
|
545
560
|
})
|
|
546
|
-
: draftOf(w.id,
|
|
561
|
+
: draftOf(w.id, shapeDomain, cards))) }, w.id));
|
|
547
562
|
}) })] }, kind));
|
|
548
|
-
}) })] })] }) }));
|
|
563
|
+
}) })] }, data)] }) }));
|
|
549
564
|
}
|
|
550
565
|
/** A page's cards after `action` on card `i`, or null for Edit (the caller opens a dialog). */
|
|
551
566
|
export function cardsAfter(cards, i, action) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ViewWidget } from '@columenlabs/lumen-ui-types/views';
|
|
2
|
+
import { type CardControls } from './lumen-widget.js';
|
|
3
|
+
export declare function SourcedCard({ card, name, controls, }: {
|
|
4
|
+
card: ViewWidget & {
|
|
5
|
+
source: string;
|
|
6
|
+
};
|
|
7
|
+
/** The page's name, for a widget with no title of its own. */
|
|
8
|
+
name: string;
|
|
9
|
+
controls?: CardControls;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// A widget with its own main data (`source` on the card): it reads that domain or recipe itself
|
|
3
|
+
// (the same reads a link uses), joins its own links, and draws like any widget over those rows. It
|
|
4
|
+
// shares nothing of the page's rows; the page's chips reach it only by a key both carry.
|
|
5
|
+
import { isRowCard, isTableCard, sourceConfigOf, tilesOfCard, } from '@columenlabs/lumen-ui-types/views';
|
|
6
|
+
import { useMemo } from 'react';
|
|
7
|
+
import { useDomainReads, useJoins } from './joins.js';
|
|
8
|
+
import { filterByTokens } from './lumen-filter.js';
|
|
9
|
+
import { followedBy, followsText, keyFilterFields, useLinks } from './lumen-links.js';
|
|
10
|
+
import { mergeRows } from './lumen-view.js';
|
|
11
|
+
import { LumenViewCard } from './lumen-view-card.js';
|
|
12
|
+
import { LumenSavedWidget, cardFilterFields, cardTitle } from './lumen-widget.js';
|
|
13
|
+
import { LumenCard, SummaryTiles } from './summary-cards.js';
|
|
14
|
+
import { summaryCash, withViewFields } from './view-model.js';
|
|
15
|
+
/** A source read's state as the card's loading state. */
|
|
16
|
+
function loadOf(state, detail, label) {
|
|
17
|
+
const failed = state === 'failed' || state === 'off';
|
|
18
|
+
return {
|
|
19
|
+
loading: state === 'loading',
|
|
20
|
+
fetching: state === 'loading',
|
|
21
|
+
refreshing: false,
|
|
22
|
+
background: false,
|
|
23
|
+
error: failed ? new Error(`${label}: ${detail ?? 'could not be read'}`) : null,
|
|
24
|
+
stale: null,
|
|
25
|
+
refresh: async () => { },
|
|
26
|
+
tookMs: null,
|
|
27
|
+
cached: false,
|
|
28
|
+
at: null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function SourcedCard({ card, name, controls, }) {
|
|
32
|
+
const ids = useMemo(() => [card.source], [card.source]);
|
|
33
|
+
const read = useDomainReads(ids, ids)[card.source];
|
|
34
|
+
const base = (read?.domain ?? {
|
|
35
|
+
id: card.source,
|
|
36
|
+
rowKey: String,
|
|
37
|
+
fields: [],
|
|
38
|
+
});
|
|
39
|
+
const rows = (read?.rows ?? []);
|
|
40
|
+
const config = useMemo(() => sourceConfigOf(card, name), [card, name]);
|
|
41
|
+
const load = loadOf(read?.state ?? 'loading', read?.detail, read?.label ?? card.source);
|
|
42
|
+
if (isTableCard(card))
|
|
43
|
+
return (_jsx(LumenViewCard, { id: card.id, domain: base, config: config, rows: card.filter.length
|
|
44
|
+
? filterByTokens(rows, cardFilterFields(base.fields), card.filter)
|
|
45
|
+
: rows, load: load, title: card.title ?? read?.label ?? name, controls: controls, shown: card.controls }));
|
|
46
|
+
return (_jsx(SourcedWidget, { card: card, base: base, rows: rows, load: load, label: read?.label ?? name, controls: controls }));
|
|
47
|
+
}
|
|
48
|
+
function SourcedWidget({ card, base, rows, load, label, controls, }) {
|
|
49
|
+
const config = useMemo(() => sourceConfigOf(card, label), [card, label]);
|
|
50
|
+
const joins = useJoins(base, rows, config);
|
|
51
|
+
const domain = useMemo(() => withViewFields(joins.domain, config), [joins.domain, config]);
|
|
52
|
+
const fields = useMemo(() => [...cardFilterFields(domain.fields), ...keyFilterFields(domain.keys)], [domain.fields, domain.keys]);
|
|
53
|
+
const links = useLinks();
|
|
54
|
+
const follows = followedBy(links?.follows(card.id) ?? [], fields, domain.keys);
|
|
55
|
+
const own = card.filter.length ? filterByTokens(rows, fields, card.filter, domain.text) : rows;
|
|
56
|
+
const drawn = follows.length ? filterByTokens(own, fields, follows, domain.text) : own;
|
|
57
|
+
const narrowed = card.filter.length > 0 || follows.length > 0;
|
|
58
|
+
const counted = domain.cash && !summaryCash(isRowCard(card) ? card : {}, narrowed)
|
|
59
|
+
? drawn.filter((r) => !domain.cash?.(r))
|
|
60
|
+
: drawn;
|
|
61
|
+
const loading = load.loading;
|
|
62
|
+
return (_jsx(LumenSavedWidget, { widget: card, title: card.title ?? cardTitle(card, label), domain: domain, rows: drawn, link: links
|
|
63
|
+
? {
|
|
64
|
+
color: links.colorOf(card.id),
|
|
65
|
+
onPick: (c) => links.setColor(card.id, c),
|
|
66
|
+
follows: followsText(follows),
|
|
67
|
+
}
|
|
68
|
+
: undefined, fields: fields, merge: (rs) => mergeRows(domain, rs, null), load: load, tiles: 'tile' in card ? (_jsx(LumenCard, { domain: domain, tile: card.tile, rows: counted, fields: fields, loading: loading, style: card.style, target: card.target, meter: card.meter, bands: card.bands })) : isRowCard(card) ? (_jsx(SummaryTiles, { domain: domain, tiles: tilesOfCard(config, card), rows: counted, fields: fields, loading: loading })) : null, controls: controls }));
|
|
69
|
+
}
|
package/dist/view-editor.d.ts
CHANGED
|
@@ -49,4 +49,4 @@ export interface ViewEditDialogProps<T> {
|
|
|
49
49
|
* only Fields and Settings; a type tried and left comes back as it was. The view's own table stays
|
|
50
50
|
* a table.
|
|
51
51
|
*/
|
|
52
|
-
export declare function ViewEditDialog<T>({ open, onOpenChange, domain:
|
|
52
|
+
export declare function ViewEditDialog<T>({ open, onOpenChange, domain: pageDomain, config, rows: pageRows, current, onSave, card, onSaveCard, fields: pageFields, viewTiles, read, viewId, editor, onDuplicate, onDelete, }: ViewEditDialogProps<T>): import("react").JSX.Element;
|
package/dist/view-editor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { CATALOG, FRAME_LINK, catalogKeys, linkColumns, readableDomain, } from '@columenlabs/lumen-ui-types/catalog';
|
|
3
|
-
import { CONNECTED_COUNT, VIEW_CARD_KIND_NAMES, WIDGET_STYLES, cardsOf, connectedKey, inferConnection, isOwnTile, isRowCard, isTableCard, mainTableOf, tableConfigOf, tableLayoutOf, viewAggs, viewCardSchema, viewPageSizes, viewPaginationSchema, widgetStyleId, } from '@columenlabs/lumen-ui-types/views';
|
|
3
|
+
import { CONNECTED_COUNT, VIEW_CARD_KIND_NAMES, WIDGET_STYLES, cardsOf, connectedKey, customViewConfigSchema, inferConnection, isOwnTile, isRowCard, isTableCard, mainTableOf, sourceConfigOf, tableConfigOf, tableLayoutOf, viewAggs, viewCardSchema, viewPageSizes, viewPaginationSchema, widgetStyleId, } from '@columenlabs/lumen-ui-types/views';
|
|
4
4
|
// The Edit view dialog (mock 2b): two panes. Left, a tree of every field the
|
|
5
5
|
// view's rows reach, grouped by where it comes from (Holding, Symbol by topic, Account, Formulas),
|
|
6
6
|
// ticked to show. Right, what the view does with them: column order (drag), shape, group, filter,
|
|
@@ -10,7 +10,7 @@ import { CONNECTED_COUNT, VIEW_CARD_KIND_NAMES, WIDGET_STYLES, cardsOf, connecte
|
|
|
10
10
|
import { DndContext, KeyboardSensor, PointerSensor, TouchSensor, closestCenter, useSensor, useSensors, } from '@dnd-kit/core';
|
|
11
11
|
import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable';
|
|
12
12
|
import { CSS } from '@dnd-kit/utilities';
|
|
13
|
-
import { ArrowDownIcon, ArrowLeftIcon, ArrowUpIcon, CheckIcon, ChevronDownIcon, ChevronRightIcon, Columns3Icon, CopyIcon, DatabaseIcon, EllipsisIcon, FilterIcon, GripVerticalIcon, LayoutDashboardIcon, Link2Icon, MoveHorizontalIcon, PencilIcon, PlusIcon, RotateCcwIcon, SearchIcon, ShapesIcon, ShrinkIcon, SigmaIcon, SlidersHorizontalIcon, SmileIcon, SparklesIcon, ToggleRightIcon, Trash2Icon, Unlink2Icon, XIcon, } from 'lucide-react';
|
|
13
|
+
import { ArrowDownIcon, ArrowLeftIcon, ArrowUpIcon, CheckIcon, ChevronDownIcon, ChevronRightIcon, Columns3Icon, CopyIcon, DatabaseIcon, EllipsisIcon, FilterIcon, GripVerticalIcon, LayoutDashboardIcon, Link2Icon, MoveHorizontalIcon, PencilIcon, PlusIcon, ReplaceIcon, RotateCcwIcon, SearchIcon, ShapesIcon, ShrinkIcon, SigmaIcon, SlidersHorizontalIcon, SmileIcon, SparklesIcon, StarIcon, TableIcon, ToggleRightIcon, Trash2Icon, Unlink2Icon, XIcon, } from 'lucide-react';
|
|
14
14
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
15
15
|
import { AddFormulaDialog } from './add-column.js';
|
|
16
16
|
import { useAssistant, useAssistantName } from './assistant-context.js';
|
|
@@ -21,12 +21,13 @@ import { SaveButton } from './form-issues.js';
|
|
|
21
21
|
import { useIsMobile } from './hooks/use-mobile.js';
|
|
22
22
|
import { IconButton } from './icon-button.js';
|
|
23
23
|
import { IconPicker } from './icon-picker.js';
|
|
24
|
-
import { asOf, frameGroupId, useDomainReads, useDraftFrames, useFrameChoices } from './joins.js';
|
|
24
|
+
import { asOf, frameGroupId, rowsDomain, useDomainReads, useDraftFrames, useFrameChoices, } from './joins.js';
|
|
25
25
|
import { cn } from './lib/utils.js';
|
|
26
26
|
import { LumenTokenizedFilter, filterByTokens, tokenText, useLumenFilter, } from './lumen-filter.js';
|
|
27
27
|
import { LinkPicker } from './lumen-links.js';
|
|
28
28
|
import { LumenSelector } from './lumen-selector.js';
|
|
29
|
-
import {
|
|
29
|
+
import { lumenToast } from './lumen-toast.js';
|
|
30
|
+
import { CARD_COLUMNS, CARD_TITLE_MAX, cardFilterFields, cardTitle, draftOf, useOffered, } from './lumen-widget.js';
|
|
30
31
|
import { useApp } from './shell.js';
|
|
31
32
|
import { summaryCardIssues } from './summary-cards.js';
|
|
32
33
|
import { Badge } from './ui/badge.js';
|
|
@@ -48,6 +49,7 @@ import { AGG_LABELS, FORMAT_LABELS, FormatChip, FormulaForm, MAX_FORMULAS, field
|
|
|
48
49
|
import { VIEW_ICONS, lucideIconNames, shapeIcon, viewIcon } from './view-icons.js';
|
|
49
50
|
import { FORMULAS_GROUP, canFilter, chipsOf, formulaBlanks, formulasOf, groupOf, hiddenOf, subgroupLabel, subgroupOf, subgroupsOf, summaryCash, takesFormulas, tilesOf, totalAggOf, withViewFields, } from './view-model.js';
|
|
50
51
|
import { WidgetControlsEditor } from './widget-controls.js';
|
|
52
|
+
import { PAGE_SOURCE, useWidgetDataOptions } from './widget-data.js';
|
|
51
53
|
import { EditorRow as Row, WidgetFields, WidgetSettings, hasWidgetSettings, readsRows, slotKeys, widgetSlots, } from './widget-settings.js';
|
|
52
54
|
import { widgetThumb } from './widget-thumbs.js';
|
|
53
55
|
/** The field's own group and Total rows: a formula over the rolled-up inputs, or a share. */
|
|
@@ -60,6 +62,7 @@ const SHAPES = [
|
|
|
60
62
|
{ value: 'pivot', label: 'Pivot' },
|
|
61
63
|
];
|
|
62
64
|
const NONE = '_none';
|
|
65
|
+
/** The main data picker's value for the page's own rows. */
|
|
63
66
|
const NO_FRAMES = [];
|
|
64
67
|
/** A recipe's (a linked the assistant frame's) group id starts with this (`recipe:fr_…`). */
|
|
65
68
|
const FRAME_PREFIX = frameGroupId('');
|
|
@@ -123,10 +126,17 @@ const tableDraftOf = (config, card) => tableConfigOf(config, isTableCard(card)
|
|
|
123
126
|
* only Fields and Settings; a type tried and left comes back as it was. The view's own table stays
|
|
124
127
|
* a table.
|
|
125
128
|
*/
|
|
126
|
-
export function ViewEditDialog({ open, onOpenChange, domain:
|
|
129
|
+
export function ViewEditDialog({ open, onOpenChange, domain: pageDomain, config, rows: pageRows, current, onSave, card, onSaveCard, fields: pageFields = [], viewTiles, read, viewId, editor, onDuplicate, onDelete, }) {
|
|
127
130
|
const isMobile = useIsMobile();
|
|
128
|
-
|
|
131
|
+
// A widget's main data (`source`): null is the page's rows, '' none yet (all cleared).
|
|
132
|
+
const [source, setSource] = useState(card?.source ?? null);
|
|
133
|
+
const start = () => card ? tableDraftOf(card.source ? sourceConfigOf(card, config.name) : config, card) : config;
|
|
129
134
|
const [draft, setDraft] = useState(start);
|
|
135
|
+
const srcIds = useMemo(() => (open && source ? [source] : NO_FRAMES), [open, source]);
|
|
136
|
+
const srcRead = useDomainReads(srcIds, srcIds);
|
|
137
|
+
const srcNow = source ? srcRead[source] : undefined;
|
|
138
|
+
const joined = (source === null ? pageDomain : (srcNow?.domain ?? rowsDomain(source || 'none', [], [])));
|
|
139
|
+
const rows = (source === null ? pageRows : (srcNow?.rows ?? []));
|
|
130
140
|
// Recipes: the assistant frames the draft attaches, their columns in the tree
|
|
131
141
|
// before Save.
|
|
132
142
|
const frameIds = draft.frames ?? NO_FRAMES;
|
|
@@ -156,6 +166,7 @@ export function ViewEditDialog({ open, onOpenChange, domain: joined, config, row
|
|
|
156
166
|
if (open !== wasOpen) {
|
|
157
167
|
setWasOpen(open);
|
|
158
168
|
if (open) {
|
|
169
|
+
setSource(card?.source ?? null);
|
|
159
170
|
setDraft(start());
|
|
160
171
|
setWidget(card && !isTableCard(card) ? card : null);
|
|
161
172
|
setFrame(card ?? null);
|
|
@@ -273,6 +284,8 @@ export function ViewEditDialog({ open, onOpenChange, domain: joined, config, row
|
|
|
273
284
|
}
|
|
274
285
|
: undefined;
|
|
275
286
|
const domain = withViewFields(withLinks, draft);
|
|
287
|
+
// A widget's own filter reads the page's fields, or its own source's.
|
|
288
|
+
const filterFields = source === null ? pageFields : cardFilterFields(domain.fields);
|
|
276
289
|
const formulas = formulasOf(draft);
|
|
277
290
|
// Each formula's blank rows on the page's rows now, and why.
|
|
278
291
|
const formulaKey = JSON.stringify(formulas);
|
|
@@ -348,6 +361,97 @@ export function ViewEditDialog({ open, onOpenChange, domain: joined, config, row
|
|
|
348
361
|
description: w.note,
|
|
349
362
|
thumb: widgetThumb(w.id),
|
|
350
363
|
}));
|
|
364
|
+
// Main data (a widget only): change it, drop it (the next source moves
|
|
365
|
+
// up and links re-infer against it), make a linked one the main, or clear all. Each has Undo.
|
|
366
|
+
const keysOf = (id) => (id.startsWith(FRAME_LINK) ? [] : catalogKeys(id));
|
|
367
|
+
const labelOf = (id) => id.startsWith(FRAME_LINK)
|
|
368
|
+
? ((choices.data ?? []).find((f) => `${FRAME_LINK}${f.id}` === id)?.name ?? 'Recipe')
|
|
369
|
+
: (allRead[id]?.label ?? CATALOG[id]?.label ?? id);
|
|
370
|
+
const pageLabel = `${config.name} (this page)`;
|
|
371
|
+
const mainLabel = source === null ? pageLabel : source ? (srcNow?.label ?? labelOf(source)) : '';
|
|
372
|
+
// Everything the widget joins besides its main data: its links, and recipes attached by key.
|
|
373
|
+
const others = () => [
|
|
374
|
+
...connections.map((c) => c.to),
|
|
375
|
+
...frameIds.map((id) => `${FRAME_LINK}${id}`),
|
|
376
|
+
];
|
|
377
|
+
const relink = (main, ids) => ids
|
|
378
|
+
.filter((x) => x !== main)
|
|
379
|
+
.slice(0, 4)
|
|
380
|
+
.map((to) => ({ to, on: inferConnection(keysOf(main), keysOf(to)) ?? [] }));
|
|
381
|
+
const rebuild = useRef(false);
|
|
382
|
+
const changeMain = (next, conns, message) => {
|
|
383
|
+
if (!card)
|
|
384
|
+
return;
|
|
385
|
+
const before = { source, draft, widget, tokens };
|
|
386
|
+
setSource(next);
|
|
387
|
+
setDraft(next === null
|
|
388
|
+
? tableDraftOf(config, card)
|
|
389
|
+
: {
|
|
390
|
+
...customViewConfigSchema.parse({ connections: conns }),
|
|
391
|
+
name: draft.name,
|
|
392
|
+
cards: draft.cards,
|
|
393
|
+
});
|
|
394
|
+
setTokens([]);
|
|
395
|
+
rebuild.current = true;
|
|
396
|
+
lumenToast.undo(message, () => {
|
|
397
|
+
setSource(before.source);
|
|
398
|
+
setDraft(before.draft);
|
|
399
|
+
setWidget(before.widget);
|
|
400
|
+
setTokens(before.tokens);
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
const dropMain = () => {
|
|
404
|
+
const rest = others();
|
|
405
|
+
const next = rest[0];
|
|
406
|
+
if (next)
|
|
407
|
+
changeMain(next, relink(next, rest), `${mainLabel} dropped. ${labelOf(next)} is the main data now.`);
|
|
408
|
+
else
|
|
409
|
+
changeMain('', [], `${mainLabel} dropped.`);
|
|
410
|
+
};
|
|
411
|
+
const makeMain = (to) => {
|
|
412
|
+
const old = source ?? (readableDomain(pageDomain.id) ? pageDomain.id : null);
|
|
413
|
+
const rest = [...others().filter((x) => x !== to), ...(old ? [old] : [])];
|
|
414
|
+
changeMain(to, relink(to, rest), `${labelOf(to)} is the main data now.`);
|
|
415
|
+
};
|
|
416
|
+
const pickMain = (to) => to === PAGE_SOURCE
|
|
417
|
+
? changeMain(null, [], `${pageLabel} is the main data now.`)
|
|
418
|
+
: changeMain(to, relink(to, others()), `${labelOf(to)} is the main data now.`);
|
|
419
|
+
const clearAll = () => changeMain('', [], 'All data cleared.');
|
|
420
|
+
const dropLink = (to) => {
|
|
421
|
+
const before = draft;
|
|
422
|
+
unlink(to);
|
|
423
|
+
lumenToast.undo(`${labelOf(to)} dropped.`, () => setDraft(before));
|
|
424
|
+
};
|
|
425
|
+
const dropRecipe = (id) => {
|
|
426
|
+
const before = draft;
|
|
427
|
+
removeFrame(id);
|
|
428
|
+
lumenToast.undo(`${labelOf(`${FRAME_LINK}${id}`)} dropped.`, () => setDraft(before));
|
|
429
|
+
};
|
|
430
|
+
// The main data picker: this page's rows, the app's live domains, the person's recipes.
|
|
431
|
+
const dataOptions = useWidgetDataOptions(open && !!card, pageLabel);
|
|
432
|
+
const mainOptions = card ? dataOptions.filter((o) => o.value !== (source ?? PAGE_SOURCE)) : [];
|
|
433
|
+
// A new main data's fields: a widget whose fields it lacks starts afresh once its fields load.
|
|
434
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: runs when the new main data's fields arrive.
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
if (!rebuild.current)
|
|
437
|
+
return;
|
|
438
|
+
if (source && srcNow?.state !== 'ok')
|
|
439
|
+
return;
|
|
440
|
+
rebuild.current = false;
|
|
441
|
+
if (!widget)
|
|
442
|
+
return;
|
|
443
|
+
const keys = new Set(domain.fields.map((f) => f.key));
|
|
444
|
+
const fits = widgetSlots(widget, domain).every((sl) => sl.keys.every((k) => keys.has(k)));
|
|
445
|
+
if (fits)
|
|
446
|
+
return;
|
|
447
|
+
const kept = Object.fromEntries(KEPT.filter((k) => widget[k] !== undefined).map((k) => [k, widget[k]]));
|
|
448
|
+
// A shape the new data cannot fill (a number with no number field) stays as it is, and Save
|
|
449
|
+
// says which field is missing.
|
|
450
|
+
try {
|
|
451
|
+
setWidget({ ...draftOf(styleId, domain, []), ...kept });
|
|
452
|
+
}
|
|
453
|
+
catch { }
|
|
454
|
+
}, [source, srcNow?.state, domain.fields.length]);
|
|
351
455
|
// A widget's rows: the view's, narrowed by its own filter, with or without cash.
|
|
352
456
|
const widgetRows = tokens.length ? filterByTokens(rows, filterFields, tokens, domain.text) : rows;
|
|
353
457
|
const cashRows = widget && isRowCard(widget) && domain.cash && !summaryCash(widget, tokens.length > 0)
|
|
@@ -368,9 +472,25 @@ export function ViewEditDialog({ open, onOpenChange, domain: joined, config, row
|
|
|
368
472
|
why: (f) => (slot.accepts(f) ? null : slot.why),
|
|
369
473
|
}
|
|
370
474
|
: { on: (k) => shownKeys.includes(k), set: setShown, why: () => null };
|
|
475
|
+
// A widget on its own data keeps it and its links on the card; the page's stay as they were.
|
|
476
|
+
const sourcePart = source
|
|
477
|
+
? { source, connections: draft.connections ?? [] }
|
|
478
|
+
: { source: undefined, connections: undefined };
|
|
479
|
+
const viewPart = (next) => source !== null
|
|
480
|
+
? {
|
|
481
|
+
formulas: config.formulas,
|
|
482
|
+
connections: config.connections,
|
|
483
|
+
...(config.frames ? { frames: config.frames } : {}),
|
|
484
|
+
}
|
|
485
|
+
: {
|
|
486
|
+
formulas: next.formulas,
|
|
487
|
+
connections: next.connections,
|
|
488
|
+
...(next.frames ? { frames: next.frames } : {}),
|
|
489
|
+
};
|
|
371
490
|
const parsed = widget
|
|
372
491
|
? viewCardSchema.safeParse({
|
|
373
492
|
...widget,
|
|
493
|
+
...sourcePart,
|
|
374
494
|
title: title.trim() || null,
|
|
375
495
|
// A widget's own filter is its own: it never shares with the colour group.
|
|
376
496
|
filter: readsRows(widget) ? tokens.map(({ link: _, ...t }) => t) : [],
|
|
@@ -385,6 +505,16 @@ export function ViewEditDialog({ open, onOpenChange, domain: joined, config, row
|
|
|
385
505
|
setSections((s) => new Set(s).add(id));
|
|
386
506
|
};
|
|
387
507
|
const issues = [
|
|
508
|
+
...(source === ''
|
|
509
|
+
? [
|
|
510
|
+
{
|
|
511
|
+
where: 'Data',
|
|
512
|
+
message: 'Pick the main data',
|
|
513
|
+
target: 'view-editor-main-data',
|
|
514
|
+
reveal: () => setTab('fields'),
|
|
515
|
+
},
|
|
516
|
+
]
|
|
517
|
+
: []),
|
|
388
518
|
...(!card && !draft.name.trim()
|
|
389
519
|
? [
|
|
390
520
|
{
|
|
@@ -463,11 +593,7 @@ export function ViewEditDialog({ open, onOpenChange, domain: joined, config, row
|
|
|
463
593
|
}
|
|
464
594
|
if (parsed) {
|
|
465
595
|
if (parsed.success)
|
|
466
|
-
onSaveCard?.(parsed.data,
|
|
467
|
-
formulas: draft.formulas,
|
|
468
|
-
connections: draft.connections,
|
|
469
|
-
...(draft.frames ? { frames: draft.frames } : {}),
|
|
470
|
-
});
|
|
596
|
+
onSaveCard?.(parsed.data, viewPart(draft));
|
|
471
597
|
}
|
|
472
598
|
else if (frame) {
|
|
473
599
|
// A table of its own: the frame, its name and its layout.
|
|
@@ -476,25 +602,32 @@ export function ViewEditDialog({ open, onOpenChange, domain: joined, config, row
|
|
|
476
602
|
onSaveCard?.(viewCardSchema.parse({
|
|
477
603
|
...Object.fromEntries(KEPT.filter((k) => frame[k] !== undefined).map((k) => [k, frame[k]])),
|
|
478
604
|
...(own?.controls ? { controls: own.controls } : {}),
|
|
605
|
+
...sourcePart,
|
|
479
606
|
kind: 'table',
|
|
480
607
|
style: 'table',
|
|
481
608
|
title: title.trim() || null,
|
|
482
609
|
filter: [],
|
|
483
610
|
table: tableLayoutOf(next),
|
|
484
|
-
}),
|
|
485
|
-
formulas: next.formulas,
|
|
486
|
-
connections: next.connections,
|
|
487
|
-
...(next.frames ? { frames: next.frames } : {}),
|
|
488
|
-
});
|
|
611
|
+
}), viewPart(next));
|
|
489
612
|
}
|
|
490
613
|
onOpenChange(false);
|
|
491
614
|
};
|
|
492
|
-
const tree = pick ? (_jsx(FieldTree, { domain: domain, draft: draft, picked: pick.on, onPick: pick.set, why: pick.why, formulaProblems: formulaProblems, onEditFormula: edit, onRemoveFormula: removeFormula, onAddFormula: addFormula, deskDomains: deskDomains, onLink: link, onUnlink: unlink,
|
|
615
|
+
const tree = pick ? (_jsx(FieldTree, { domain: domain, draft: draft, picked: pick.on, onPick: pick.set, why: pick.why, formulaProblems: formulaProblems, onEditFormula: edit, onRemoveFormula: removeFormula, onAddFormula: addFormula, deskDomains: deskDomains, onLink: link, onUnlink: card ? dropLink : unlink, main: card
|
|
616
|
+
? {
|
|
617
|
+
label: mainLabel,
|
|
618
|
+
empty: source === '',
|
|
619
|
+
options: mainOptions,
|
|
620
|
+
onPick: pickMain,
|
|
621
|
+
onDrop: dropMain,
|
|
622
|
+
onClear: clearAll,
|
|
623
|
+
onMakeMain: makeMain,
|
|
624
|
+
}
|
|
625
|
+
: undefined, linkPairs: {
|
|
493
626
|
cardFields: unlinked.fields.filter((f) => f.group !== FORMULAS_GROUP),
|
|
494
627
|
fieldsOf: (to) => allRead[to]?.domain.fields ?? [],
|
|
495
628
|
onOf: (to) => connections.find((c) => c.to === to)?.on ?? [],
|
|
496
629
|
set: setPairs,
|
|
497
|
-
}, deskLabel: app?.label ?? null, recipes: recipes, recipeNote: choices.isError ? `Recipes did not load: ${choices.error.message}` : null, onAddRecipe: addRecipe, recipeByKey: byKey, onRemoveRecipe: removeFrame, onNewRecipe: askRecipe })) : (_jsx("p", { className: "px-1.5 text-sm text-muted-foreground", children: widget?.kind === 'chart' && (widget.style === 'price' || widget.style === 'market')
|
|
630
|
+
}, deskLabel: app?.label ?? null, recipes: recipes, recipeNote: choices.isError ? `Recipes did not load: ${choices.error.message}` : null, onAddRecipe: addRecipe, recipeByKey: byKey, onRemoveRecipe: card ? dropRecipe : removeFrame, onNewRecipe: askRecipe })) : (_jsx("p", { className: "px-1.5 text-sm text-muted-foreground", children: widget?.kind === 'chart' && (widget.style === 'price' || widget.style === 'market')
|
|
498
631
|
? 'This chart reads its symbols, not fields.'
|
|
499
632
|
: 'This widget takes no fields.' }));
|
|
500
633
|
const treeHint = widget ? (slot && (_jsxs("div", { className: "flex flex-col gap-2 px-1.5 pb-2", children: [slots.length > 1 && (_jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, className: "justify-self-start", "aria-label": "Fill", value: slot.id, onValueChange: (v) => v && setSlotId(v), children: slots.map((s) => (_jsx(ToggleGroupItem, { value: s.id, className: "px-2.5", children: s.label }, s.id))) })), _jsx("p", { className: "text-xs text-muted-foreground", children: slot.max === 1
|
|
@@ -577,8 +710,7 @@ function LinkPairs({ label, on, cardFields, fields, onChange, }) {
|
|
|
577
710
|
const row = (p, i) => (_jsxs("div", { className: "flex min-w-0 items-center gap-1", children: [_jsx(LumenSelector, { label: "This card's field", size: "sm", className: "min-w-0 flex-1", placeholder: "This card", search: true, value: p.from || null, options: mine, onChange: (v) => put({ ...p, from: v }, i) }), _jsx("span", { className: "text-xs text-muted-foreground", children: "=" }), _jsx(LumenSelector, { label: `${label}'s field`, size: "sm", className: "min-w-0 flex-1", placeholder: theirs.length ? label : 'Loading…', search: true, disabled: !theirs.length, value: p.to || null, options: theirs, onChange: (v) => put({ ...p, to: v }, i) }), i !== null && (_jsx(IconButton, { icon: XIcon, size: "icon-xs", label: "Remove this pair", onClick: () => onChange(on.filter((_, j) => j !== i)) }))] }, i ?? 'new'));
|
|
578
711
|
return (_jsxs("div", { className: "flex flex-col gap-1 px-1 py-1", children: [on.map((p, i) => row(p, i)), open && row(open, null), !open && on.length < MAX_PAIRS && (_jsxs(Button, { size: "xs", variant: "ghost", className: "self-start text-primary", onClick: () => setPart({ from: '', to: '' }), children: [_jsx(PlusIcon, {}), "Field pair"] }))] }));
|
|
579
712
|
}
|
|
580
|
-
|
|
581
|
-
function FieldTree({ domain, draft, picked, onPick, why, formulaProblems, onEditFormula, onRemoveFormula, onAddFormula, deskDomains = [], onLink, onUnlink, deskLabel, recipes = [], recipeNote, onAddRecipe, recipeByKey = () => false, onRemoveRecipe, onNewRecipe, linkPairs, }) {
|
|
713
|
+
function FieldTree({ domain, draft, picked, onPick, why, formulaProblems, onEditFormula, onRemoveFormula, onAddFormula, deskDomains = [], onLink, onUnlink, deskLabel, recipes = [], recipeNote, onAddRecipe, recipeByKey = () => false, onRemoveRecipe, onNewRecipe, linkPairs, main, }) {
|
|
582
714
|
const assistantName = useAssistantName();
|
|
583
715
|
const [query, setQuery] = useState('');
|
|
584
716
|
// New recipe's box, open while not null.
|
|
@@ -655,32 +787,41 @@ function FieldTree({ domain, draft, picked, onPick, why, formulaProblems, onEdit
|
|
|
655
787
|
const box = (_jsx(Checkbox, { checked: picked(f.key), disabled: !!off, onCheckedChange: (v) => onPick(f.key, v === true), "aria-label": `Use ${f.label}` }));
|
|
656
788
|
return (_jsxs("div", { className: cn('flex min-h-7 items-center gap-2 rounded-md px-1 hover:bg-accent/60', off && 'text-muted-foreground'), children: [_jsxs(Label, { className: "flex min-w-0 flex-1 cursor-pointer items-center gap-2 py-1 text-[0.8125rem] font-normal", children: [off ? _jsx(WhyOff, { note: off, children: box }) : box, _jsx("span", { className: "truncate", children: f.label }), f.note && (_jsx(Badge, { variant: "secondary", className: "font-normal", children: f.note })), fi >= 0 && formulaProblems[fi] && (_jsx(Badge, { variant: "destructive", className: "font-normal", children: "fix" }))] }), _jsx("span", { className: "flex w-8 shrink-0 justify-end", children: _jsx(FormatChip, { format: f.format, icon: f.icon }) }), _jsx("code", { className: "w-28 shrink-0 truncate font-mono text-[0.7rem] text-muted-foreground max-md:hidden", title: f.formula ?? f.key, children: fi >= 0 ? formulas[fi]?.expr || 'no expression yet' : f.key }), fi >= 0 && (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: PencilIcon, size: "icon-xs", label: `Edit formula ${f.label}`, onClick: () => onEditFormula(f.key) }), _jsx(LumenConfirm, { title: `Remove ${f.label}?`, description: "The formula and its column leave this view when you save.", action: "Remove", onConfirm: () => onRemoveFormula(f.key), children: _jsx(IconButton, { icon: XIcon, size: "icon-xs", label: `Remove formula ${f.label}` }) })] }))] }, f.key));
|
|
657
789
|
};
|
|
658
|
-
return (_jsxs("div", { className: "flex flex-col gap-px", "data-testid": "view-fields", children: [_jsxs("div", { className:
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
790
|
+
return (_jsxs("div", { className: "flex flex-col gap-px", "data-testid": "view-fields", children: [_jsxs("div", { className: cn('mb-1 flex items-center gap-1.5', main?.empty && 'hidden'), children: [_jsxs(InputGroup, { className: "min-w-0 flex-1 bg-background", children: [_jsx(InputGroupAddon, { children: _jsx(SearchIcon, {}) }), _jsx(InputGroupInput, { type: "search", className: "[&::-webkit-search-cancel-button]:hidden", "aria-label": "Search fields", placeholder: `Search ${domain.fields.length} fields`, value: query, onChange: (e) => setQuery(e.target.value) }), query && (_jsx(InputGroupAddon, { align: "inline-end", children: _jsx(InputGroupButton, { size: "icon-xs", "aria-label": "Clear search", onClick: () => setQuery(''), children: _jsx(XIcon, {}) }) }))] }), dataOptions.length > 0 && !main?.empty && (_jsx(LumenSelector, { trigger: "chip", icon: PlusIcon, placeholder: "Data", label: "Add data", align: "end", search: true, value: null, options: dataOptions, empty: "Nothing else to add", onChange: addData })), main && !main.empty && (_jsx(LumenConfirm, { title: "Clear all data?", description: "Every source leaves this widget. Pick its main data again, or Undo.", action: "Clear all", onConfirm: main.onClear, children: _jsx(IconButton, { icon: Trash2Icon, size: "icon-xs", label: "Clear all data" }) }))] }), recipeNote && _jsx("p", { className: "px-1.5 pb-1 text-xs text-destructive", children: recipeNote }), wish !== null && onNewRecipe && (_jsxs("div", { className: "mb-1 flex flex-col gap-1.5 rounded-md border bg-background p-2", children: [_jsx(Label, { htmlFor: "new-recipe", className: "text-xs font-medium", children: "New recipe" }), _jsx(Textarea, { id: "new-recipe", autoFocus: true, rows: 2, value: wish, placeholder: "How far each symbol is off its 52-week high", onChange: (e) => setWish(e.target.value), className: "min-h-14 text-sm" }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Saves this widget, then opens ", assistantName, " with your words to send. Its columns join here by ", Object.keys(domain.keys ?? {}).join(' or '), "."] }), _jsxs("div", { className: "flex justify-end gap-1.5", children: [_jsx(Button, { size: "xs", variant: "ghost", onClick: () => setWish(null), children: "Cancel" }), _jsxs(Button, { size: "xs", disabled: !wish.trim(), onClick: () => onNewRecipe(wish), children: [_jsx(SparklesIcon, {}), "Save and ask ", assistantName] })] })] })), main?.empty && (_jsxs("div", { className: "flex flex-col items-center gap-2 rounded-md border border-dashed px-3 py-6 text-center", children: [_jsx(DatabaseIcon, { className: "size-5 text-muted-foreground" }), _jsx("p", { className: "text-sm font-medium", children: "Pick the main data" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Its rows are what this widget shows." }), _jsx(LumenSelector, { id: "view-editor-main-data", trigger: "chip", icon: DatabaseIcon, placeholder: "Main data", label: "Pick the main data", search: true, value: null, options: main.options, empty: "No data to pick", onChange: main.onPick })] })), !main?.empty &&
|
|
791
|
+
groupsOf(domain).map((g, gi) => {
|
|
792
|
+
const all = domain.fields.filter((f) => groupOf(domain, f) === g.id);
|
|
793
|
+
const fields = all.filter(match);
|
|
794
|
+
const formulasGroup = g.id === FORMULAS_GROUP;
|
|
795
|
+
const linkTo = g.id.startsWith(LINK_PREFIX) ? g.id.slice(LINK_PREFIX.length) : null;
|
|
796
|
+
const rowsGroup = gi === 0 && !linkTo && !formulasGroup;
|
|
797
|
+
const recipe = !rowsGroup && g.id.startsWith(FRAME_PREFIX) ? g.id.slice(FRAME_PREFIX.length) : null;
|
|
798
|
+
// Not linked: no field pairs yet, so it adds nothing; its fields wait greyed out.
|
|
799
|
+
const unlinked = !!linkTo && !all.length && !(linkPairs?.onOf(linkTo).length ?? 0);
|
|
800
|
+
if (q && !fields.length)
|
|
801
|
+
return null;
|
|
802
|
+
if (!all.length && !formulasGroup && !linkTo)
|
|
803
|
+
return null;
|
|
804
|
+
const color = g.color ?? PLAIN;
|
|
805
|
+
// A link starts open, so its pairs and fields show where they are picked.
|
|
806
|
+
const open = linkTo
|
|
807
|
+
? !!q || (toggled.get(`g:${g.id}`) ?? true)
|
|
808
|
+
: isOpen(`g:${g.id}`, all) || (formulasGroup && !all.length);
|
|
809
|
+
const topics = [...new Set(fields.map((f) => f.topic ?? ''))];
|
|
810
|
+
return (_jsxs(Collapsible, { open: open, onOpenChange: () => flip(`g:${g.id}`, open), children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "h-7 min-w-0 flex-1 justify-start gap-2 px-1.5 text-[0.8125rem] font-semibold", children: [_jsx(ChevronRightIcon, { className: cn('text-muted-foreground transition-transform', open && 'rotate-90') }), _jsx("span", { className: "size-2.5 shrink-0 rounded-[3px]", style: { background: color } }), formulasGroup && _jsx(SigmaIcon, { className: "text-muted-foreground" }), linkTo &&
|
|
811
|
+
(unlinked ? (_jsx(Unlink2Icon, { className: "text-destructive" })) : (_jsx(Link2Icon, { className: "text-muted-foreground" }))), recipe && _jsx(SparklesIcon, { className: "text-muted-foreground" }), _jsx("span", { className: "truncate", children: rowsGroup && main ? main.label : g.label }), rowsGroup && (_jsx(Badge, { variant: "secondary", className: "h-4 px-1.5 text-[0.6875rem] font-normal", children: "Rows" })), unlinked && (_jsx(Badge, { variant: "destructive", className: "h-4 px-1.5 text-xs font-normal", children: "Not linked" })), _jsxs("span", { className: "num ml-auto text-xs font-normal text-muted-foreground", children: [all.filter((f) => picked(f.key)).length, "/", all.length] })] }) }), rowsGroup && main && (_jsxs(_Fragment, { children: [_jsx(LumenSelector, { trigger: "icon", icon: ReplaceIcon, label: "Change the main data", align: "end", search: true, value: null, options: main.options, empty: "No other data", onChange: main.onPick }), _jsx(IconButton, { icon: XIcon, size: "icon-xs", label: `Drop ${main.label}`, onClick: main.onDrop })] })), main && (linkTo || recipe) && (_jsx(IconButton, { icon: StarIcon, size: "icon-xs", label: `Make ${g.label} the main data`, onClick: () => main.onMakeMain(linkTo ?? `${FRAME_LINK}${recipe}`) })), main && linkTo && onUnlink && (_jsx(IconButton, { icon: XIcon, size: "icon-xs", label: `Drop ${g.label}`, onClick: () => onUnlink(linkTo) })), main && recipe && onRemoveRecipe && (_jsx(IconButton, { icon: XIcon, size: "icon-xs", label: `Drop ${g.label}`, onClick: () => onRemoveRecipe(recipe) })), !main && linkTo && onUnlink && (_jsx(LumenConfirm, { title: `Unlink ${g.label}?`, description: "Its columns leave this widget when you save.", action: "Unlink", onConfirm: () => onUnlink(linkTo), children: _jsx(IconButton, { icon: Unlink2Icon, size: "icon-xs", label: `Unlink ${g.label}` }) })), !main && recipe && onRemoveRecipe && (_jsx(LumenConfirm, { title: `Remove ${g.label}?`, description: `Its columns leave this widget when you save. The recipe stays in ${assistantName}.`, action: "Remove", onConfirm: () => onRemoveRecipe(recipe), children: _jsx(IconButton, { icon: XIcon, size: "icon-xs", label: `Remove ${g.label}` }) }))] }), _jsxs(CollapsibleContent, { className: "ml-[0.9rem] flex flex-col gap-px border-l-2 pl-2", style: { borderColor: guide(color) }, children: [g.note && !q && (_jsx("p", { className: cn('px-1 py-1 text-xs', unlinked ? 'text-destructive' : 'text-muted-foreground'), children: g.note })), linkTo && linkPairs && !q && (_jsx(LinkPairs, { label: g.label, on: linkPairs.onOf(linkTo), cardFields: linkPairs.cardFields, fields: linkPairs.fieldsOf(linkTo), onChange: (on) => linkPairs.set(linkTo, on) })), unlinked &&
|
|
812
|
+
linkPairs
|
|
813
|
+
?.fieldsOf(linkTo)
|
|
814
|
+
.filter(match)
|
|
815
|
+
.map((f) => (_jsxs("div", { className: "flex min-h-7 items-center gap-2 px-1 text-sm text-muted-foreground opacity-60", children: [_jsx(WhyOff, { note: "Link it first", children: _jsx(Checkbox, { checked: false, disabled: true, "aria-label": `Use ${f.label}` }) }), _jsx("span", { className: "truncate", children: f.label })] }, f.key))), topics.map((t) => {
|
|
816
|
+
const inTopic = fields.filter((f) => (f.topic ?? '') === t);
|
|
817
|
+
if (!t)
|
|
818
|
+
return inTopic.map(leaf);
|
|
819
|
+
const src = g.sources?.[t];
|
|
820
|
+
const id = `t:${g.id}:${t}`;
|
|
821
|
+
const topicOpen = isOpen(id, all.filter((f) => f.topic === t));
|
|
822
|
+
return (_jsxs(Collapsible, { open: topicOpen, onOpenChange: () => flip(id, topicOpen), children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "h-7 w-full justify-start gap-1.5 px-1 text-[0.8125rem] font-semibold", children: [_jsx(ChevronRightIcon, { className: cn('text-muted-foreground transition-transform', topicOpen && 'rotate-90') }), _jsx("span", { className: "truncate", children: t }), _jsxs("span", { className: "num text-xs font-normal text-muted-foreground", children: [inTopic.filter((f) => picked(f.key)).length, "/", inTopic.length] }), src && (_jsxs("span", { className: "ml-auto flex min-w-0 items-center gap-1.5 text-xs font-normal text-muted-foreground", children: [_jsx("span", { className: "size-1.5 shrink-0 rounded-full", style: { background: src.color } }), _jsx("span", { className: "font-medium", style: { color: src.color }, children: src.name }), src.detail && (_jsxs("span", { className: "truncate max-md:hidden", children: ["\u00B7 ", src.detail] }))] }))] }) }), _jsx(CollapsibleContent, { className: "ml-[0.7rem] flex flex-col gap-px border-l-2 pl-2", style: { borderColor: guide(src?.color ?? PLAIN) }, children: inTopic.map(leaf) })] }, t));
|
|
823
|
+
}), formulasGroup && onAddFormula && !q && (_jsxs(Button, { variant: "ghost", size: "sm", className: "h-7 justify-start gap-2 px-1 font-normal text-muted-foreground", onClick: onAddFormula, children: [_jsx(PlusIcon, {}), "Add formula"] }))] })] }, g.id));
|
|
824
|
+
})] }));
|
|
684
825
|
}
|
|
685
826
|
/** A key's name as a person reads it: `symbol` is Symbol. */
|
|
686
827
|
const fieldName = (k) => (k[0]?.toUpperCase() ?? '') + k.slice(1).replace(/_/g, ' ');
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The data a widget can take as its main data: this page's rows, the app's live domains with a
|
|
2
|
+
// reader, and the person's recipes. Add widget's first step and Edit widget ▸ Data both pick from it.
|
|
3
|
+
import { CATALOG, FRAME_LINK, catalogKeys, readableDomain, } from '@columenlabs/lumen-ui-types/catalog';
|
|
4
|
+
import { DatabaseIcon, SparklesIcon, TableIcon } from 'lucide-react';
|
|
5
|
+
import { asOf, useFrameChoices } from './joins.js';
|
|
6
|
+
import { useApp } from './shell.js';
|
|
7
|
+
/** The page's own rows, as a data option's value. */
|
|
8
|
+
export const PAGE_SOURCE = '_page';
|
|
9
|
+
export function useWidgetDataOptions(enabled, pageLabel) {
|
|
10
|
+
const app = useApp();
|
|
11
|
+
const choices = useFrameChoices(enabled);
|
|
12
|
+
return [
|
|
13
|
+
{ value: PAGE_SOURCE, label: pageLabel, group: 'This page', icon: TableIcon },
|
|
14
|
+
...Object.entries(CATALOG)
|
|
15
|
+
.filter(([id, d]) => d.desk === app?.id && d.status === 'live' && readableDomain(id))
|
|
16
|
+
.map(([id, d]) => ({
|
|
17
|
+
value: id,
|
|
18
|
+
label: d.label,
|
|
19
|
+
group: `${app?.label ?? 'This app'} data`,
|
|
20
|
+
detail: catalogKeys(id).join(', ') || undefined,
|
|
21
|
+
icon: DatabaseIcon,
|
|
22
|
+
keywords: [d.about],
|
|
23
|
+
})),
|
|
24
|
+
...(choices.data ?? []).map((f) => ({
|
|
25
|
+
value: `${FRAME_LINK}${f.id}`,
|
|
26
|
+
label: f.name,
|
|
27
|
+
group: 'Recipes',
|
|
28
|
+
detail: asOf(f.at),
|
|
29
|
+
icon: SparklesIcon,
|
|
30
|
+
})),
|
|
31
|
+
];
|
|
32
|
+
}
|
|
@@ -75,6 +75,14 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
75
75
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
76
76
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
77
77
|
}[];
|
|
78
|
+
source?: string | undefined;
|
|
79
|
+
connections?: {
|
|
80
|
+
to: string;
|
|
81
|
+
on: {
|
|
82
|
+
from: string;
|
|
83
|
+
to: string;
|
|
84
|
+
}[];
|
|
85
|
+
}[] | undefined;
|
|
78
86
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
79
87
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
80
88
|
kind: "table";
|
|
@@ -130,6 +138,14 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
130
138
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
131
139
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
132
140
|
}[];
|
|
141
|
+
source?: string | undefined;
|
|
142
|
+
connections?: {
|
|
143
|
+
to: string;
|
|
144
|
+
on: {
|
|
145
|
+
from: string;
|
|
146
|
+
to: string;
|
|
147
|
+
}[];
|
|
148
|
+
}[] | undefined;
|
|
133
149
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
134
150
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
135
151
|
kind: "table";
|
|
@@ -157,6 +173,14 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
157
173
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
158
174
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
159
175
|
}[];
|
|
176
|
+
source?: string | undefined;
|
|
177
|
+
connections?: {
|
|
178
|
+
to: string;
|
|
179
|
+
on: {
|
|
180
|
+
from: string;
|
|
181
|
+
to: string;
|
|
182
|
+
}[];
|
|
183
|
+
}[] | undefined;
|
|
160
184
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
161
185
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
162
186
|
kind: "metric";
|
|
@@ -225,6 +249,14 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
225
249
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
226
250
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
227
251
|
}[];
|
|
252
|
+
source?: string | undefined;
|
|
253
|
+
connections?: {
|
|
254
|
+
to: string;
|
|
255
|
+
on: {
|
|
256
|
+
from: string;
|
|
257
|
+
to: string;
|
|
258
|
+
}[];
|
|
259
|
+
}[] | undefined;
|
|
228
260
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
229
261
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
230
262
|
kind: "metric";
|
|
@@ -287,6 +319,14 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
287
319
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
288
320
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
289
321
|
}[];
|
|
322
|
+
source?: string | undefined;
|
|
323
|
+
connections?: {
|
|
324
|
+
to: string;
|
|
325
|
+
on: {
|
|
326
|
+
from: string;
|
|
327
|
+
to: string;
|
|
328
|
+
}[];
|
|
329
|
+
}[] | undefined;
|
|
290
330
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
291
331
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
292
332
|
w: number;
|
|
@@ -316,6 +356,14 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
316
356
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
317
357
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
318
358
|
}[];
|
|
359
|
+
source?: string | undefined;
|
|
360
|
+
connections?: {
|
|
361
|
+
to: string;
|
|
362
|
+
on: {
|
|
363
|
+
from: string;
|
|
364
|
+
to: string;
|
|
365
|
+
}[];
|
|
366
|
+
}[] | undefined;
|
|
319
367
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
320
368
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
321
369
|
kind: "allocation";
|
|
@@ -343,6 +391,14 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
343
391
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
344
392
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
345
393
|
}[];
|
|
394
|
+
source?: string | undefined;
|
|
395
|
+
connections?: {
|
|
396
|
+
to: string;
|
|
397
|
+
on: {
|
|
398
|
+
from: string;
|
|
399
|
+
to: string;
|
|
400
|
+
}[];
|
|
401
|
+
}[] | undefined;
|
|
346
402
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
347
403
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
348
404
|
kind: "allocation";
|
|
@@ -370,6 +426,14 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
370
426
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
371
427
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
372
428
|
}[];
|
|
429
|
+
source?: string | undefined;
|
|
430
|
+
connections?: {
|
|
431
|
+
to: string;
|
|
432
|
+
on: {
|
|
433
|
+
from: string;
|
|
434
|
+
to: string;
|
|
435
|
+
}[];
|
|
436
|
+
}[] | undefined;
|
|
373
437
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
374
438
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
375
439
|
kind: "allocation";
|
|
@@ -400,6 +464,14 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
400
464
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
401
465
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
402
466
|
}[];
|
|
467
|
+
source?: string | undefined;
|
|
468
|
+
connections?: {
|
|
469
|
+
to: string;
|
|
470
|
+
on: {
|
|
471
|
+
from: string;
|
|
472
|
+
to: string;
|
|
473
|
+
}[];
|
|
474
|
+
}[] | undefined;
|
|
403
475
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
404
476
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
405
477
|
kind: "chart";
|
|
@@ -424,6 +496,14 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
424
496
|
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
425
497
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
426
498
|
}[];
|
|
499
|
+
source?: string | undefined;
|
|
500
|
+
connections?: {
|
|
501
|
+
to: string;
|
|
502
|
+
on: {
|
|
503
|
+
from: string;
|
|
504
|
+
to: string;
|
|
505
|
+
}[];
|
|
506
|
+
}[] | undefined;
|
|
427
507
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
428
508
|
controls?: Partial<Record<"ask" | "columns" | "download" | "filter" | "footer" | "group" | "header" | "legend" | "pager" | "refresh" | "window", boolean>> | undefined;
|
|
429
509
|
w: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@codemirror/lang-javascript": "6.2.5",
|
|
42
42
|
"@codemirror/lang-json": "6.0.2",
|
|
43
43
|
"@codemirror/lang-sql": "6.10.0",
|
|
44
|
-
"@columenlabs/lumen-ui-types": "0.2.
|
|
44
|
+
"@columenlabs/lumen-ui-types": "0.2.1",
|
|
45
45
|
"@dnd-kit/core": "6.3.1",
|
|
46
46
|
"@dnd-kit/sortable": "10.0.0",
|
|
47
47
|
"@dnd-kit/utilities": "3.2.2",
|