torch-glare 2.4.4 → 2.5.0
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/apps/lib/components/DataViews/{badgeAdapter.ts → badge.ts} +2 -2
- package/apps/lib/components/DataViews/cell.tsx +324 -0
- package/apps/lib/components/DataViews/context.ts +144 -0
- package/apps/lib/components/DataViews/data-views.tsx +383 -0
- package/apps/lib/components/DataViews/filters/children.tsx +98 -0
- package/apps/lib/components/DataViews/filters/custom.tsx +34 -0
- package/apps/lib/components/DataViews/filters/filters.tsx +163 -0
- package/apps/lib/components/DataViews/filters/index.ts +4 -0
- package/apps/lib/components/DataViews/filters/labelled.tsx +20 -0
- package/apps/lib/components/DataViews/filters/presets.tsx +65 -0
- package/apps/lib/components/DataViews/filters/summary.tsx +65 -0
- package/apps/lib/components/DataViews/filters/sync.tsx +35 -0
- package/apps/lib/components/DataViews/filters/values.ts +173 -0
- package/apps/lib/components/DataViews/header.tsx +217 -0
- package/apps/lib/components/DataViews/hooks/index.ts +5 -0
- package/apps/lib/components/DataViews/hooks/useActiveRow.ts +22 -0
- package/apps/lib/components/DataViews/hooks/useControllable.ts +52 -0
- package/apps/lib/components/DataViews/index.ts +74 -26
- package/apps/lib/components/DataViews/panel/columns.tsx +153 -0
- package/apps/lib/components/DataViews/panel/controls.tsx +106 -0
- package/apps/lib/components/DataViews/panel/index.ts +3 -0
- package/apps/lib/components/DataViews/panel/panel.tsx +164 -0
- package/apps/lib/components/DataViews/panel/saved-views.tsx +67 -0
- package/apps/lib/components/DataViews/panel/section.tsx +79 -0
- package/apps/lib/components/DataViews/panel/sort.tsx +42 -0
- package/apps/lib/components/DataViews/panel/tab.tsx +31 -0
- package/apps/lib/components/DataViews/slots.ts +63 -0
- package/apps/lib/components/DataViews/states.tsx +38 -0
- package/apps/lib/components/DataViews/types.ts +485 -178
- package/apps/lib/components/DataViews/views/board-view.tsx +379 -0
- package/apps/lib/components/DataViews/views/card-rows.tsx +36 -0
- package/apps/lib/components/DataViews/views/inbox-view.tsx +257 -0
- package/apps/lib/components/DataViews/views/pane-views.tsx +192 -0
- package/apps/lib/components/DataViews/views/table-view.tsx +426 -0
- package/apps/lib/components/DataViews/views/tree-view.tsx +365 -0
- package/apps/lib/components/FormBuilder/context.ts +20 -6
- package/apps/lib/components/FormBuilder/field-kind.ts +28 -0
- package/apps/lib/components/FormBuilder/fields/DateField.tsx +3 -3
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +7 -6
- package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +30 -4
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +7 -7
- package/apps/lib/components/FormBuilder/fields/TableField.tsx +80 -52
- package/apps/lib/components/FormBuilder/fields/TextField.tsx +9 -9
- package/apps/lib/components/FormBuilder/form-builder.tsx +66 -6
- package/apps/lib/components/FormBuilder/index.ts +3 -1
- package/apps/lib/components/FormBuilder/types.ts +40 -0
- package/apps/lib/components/Input.tsx +3 -0
- package/apps/lib/components/SearchableTable.tsx +5 -4
- package/apps/lib/components/SectionBlock.tsx +58 -11
- package/apps/lib/components/Select.tsx +3 -1
- package/apps/lib/components/TabSwitch.tsx +16 -4
- package/apps/lib/components/Table.tsx +265 -67
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +6 -3
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +16 -14
- package/apps/lib/components/TreeFolder/index.ts +1 -1
- package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +70 -207
- package/apps/lib/hooks/useDragDrop.tsx +365 -0
- package/apps/lib/hooks/useInfiniteScroll.ts +108 -0
- package/apps/lib/registry.json +159 -4
- package/apps/lib/tsconfig.tsbuildinfo +1 -1
- package/apps/lib/utils/dataViews/path.ts +67 -0
- package/apps/lib/utils/dataViews/query.ts +73 -0
- package/apps/lib/utils/dataViews/types.ts +187 -0
- package/docs/components/breadcrumb.md +1 -1
- package/docs/components/button-group.md +1 -1
- package/docs/components/button.md +1 -1
- package/docs/components/card.md +1 -1
- package/docs/components/checkbox.md +1 -1
- package/docs/components/data-views/backend-response.md +324 -0
- package/docs/components/data-views/examples/a11y-rtl.md +250 -0
- package/docs/components/data-views/examples/api-orders-route.md +130 -0
- package/docs/components/data-views/examples/fields.md +362 -0
- package/docs/components/data-views/examples/filters.md +308 -0
- package/docs/components/data-views/examples/inbox-routing.md +218 -0
- package/docs/components/data-views/examples/index.md +29 -0
- package/docs/components/data-views/examples/overview.md +244 -0
- package/docs/components/data-views/examples/panel.md +212 -0
- package/docs/components/data-views/examples/scale.md +231 -0
- package/docs/components/data-views/examples/server-side.md +210 -0
- package/docs/components/data-views/examples/state.md +250 -0
- package/docs/components/data-views/examples/tree-custom.md +388 -0
- package/docs/components/data-views/examples/view-registry.md +313 -0
- package/docs/components/data-views/examples/views.md +534 -0
- package/docs/components/data-views/guide.md +405 -0
- package/docs/components/data-views/index.md +1504 -0
- package/docs/components/data-views/migration.md +79 -0
- package/docs/components/date-picker.md +0 -1
- package/docs/components/form-builder.md +19 -8
- package/docs/components/form-renderer.md +2 -1
- package/docs/components/form.md +1 -1
- package/docs/components/input-field.md +1 -1
- package/docs/components/input-otp.md +1 -1
- package/docs/components/input.md +1 -1
- package/docs/components/labeled-check-box.md +1 -1
- package/docs/components/labeled-radio.md +1 -1
- package/docs/components/radio-card.md +1 -1
- package/docs/components/radio.md +1 -1
- package/docs/components/search-field.md +1 -1
- package/docs/components/section-block.md +79 -3
- package/docs/components/select.md +1 -1
- package/docs/components/simple-select.md +1 -1
- package/docs/components/switch.md +1 -1
- package/docs/components/tab-switch.md +1 -1
- package/docs/components/table.md +45 -8
- package/docs/components/text-editor.md +1 -1
- package/docs/components/textarea.md +1 -1
- package/docs/components/toggle-button.md +1 -1
- package/docs/components/toggle.md +1 -1
- package/docs/components/tree-folder.md +110 -0
- package/docs/how-to/forms-with-form-builder.md +6 -4
- package/docs/reference/components.md +16 -6
- package/docs/tutorials/component-composition.md +11 -13
- package/package.json +3 -2
- package/apps/lib/components/DataViews/DataViewRadio.tsx +0 -49
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +0 -393
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +0 -207
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +0 -332
- package/apps/lib/components/DataViews/FilterPanel.tsx +0 -493
- package/apps/lib/components/DataViews/HeaderSearch.tsx +0 -93
- package/apps/lib/components/DataViews/InboxView.tsx +0 -463
- package/apps/lib/components/DataViews/InboxViewCard.tsx +0 -127
- package/apps/lib/components/DataViews/KanbanView.tsx +0 -336
- package/apps/lib/components/DataViews/PanelControls.tsx +0 -39
- package/apps/lib/components/DataViews/SettingsPanel.tsx +0 -279
- package/apps/lib/components/DataViews/TableView.tsx +0 -212
- package/apps/lib/components/DataViews/TreeView.tsx +0 -364
- package/apps/lib/components/DataViews/fieldRenderers.tsx +0 -299
- package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +0 -87
- package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +0 -120
- package/apps/lib/components/DataViews/filters/PresetChips.tsx +0 -45
- package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +0 -165
- package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +0 -50
- package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +0 -74
- package/apps/lib/hooks/useDataViewsState.ts +0 -175
- package/apps/lib/utils/dataViews/columnUtils.ts +0 -132
- package/apps/lib/utils/dataViews/fieldUtils.ts +0 -197
- package/apps/lib/utils/dataViews/nestedDataUtils.tsx +0 -371
- package/apps/lib/utils/dataViews/pathUtils.ts +0 -139
- package/apps/lib/utils/dataViews/rangeUtils.ts +0 -234
- package/apps/lib/utils/dataViews/treeUtils.ts +0 -396
- package/docs/components/data-views-config-panel.md +0 -208
- package/docs/components/data-views-layout.md +0 -291
- package/docs/components/inbox-view.md +0 -170
- package/docs/components/kanban-view.md +0 -135
- package/docs/components/table-view.md +0 -141
- package/docs/components/tree-view.md +0 -147
- package/docs/how-to/data-views-from-backend-response.md +0 -194
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: DataViews example — Inbox → route
|
|
3
|
+
description: `itemHref` + `linkComponent` — the detail pane driven by the URL.
|
|
4
|
+
group: examples
|
|
5
|
+
component: DataViews
|
|
6
|
+
keywords: [data-views, example, examples, inbox, routing]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# DataViews example — Inbox → route
|
|
10
|
+
|
|
11
|
+
`itemHref` + `linkComponent` — the detail pane driven by the URL.
|
|
12
|
+
|
|
13
|
+
Complete and runnable — this is the page itself, not an excerpt. In the monorepo it lives at `apps/app/data-views/inbox-routing/page.tsx` and `apps/app/data-views/inbox-routing/[id]/page.tsx`.
|
|
14
|
+
|
|
15
|
+
See the [component reference](../index.md) for what each prop does, or the [guide](../guide.md) for the same ground as scenarios.
|
|
16
|
+
|
|
17
|
+
## `apps/app/data-views/inbox-routing/page.tsx`
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
"use client";
|
|
21
|
+
|
|
22
|
+
import { useMemo, useState } from "react";
|
|
23
|
+
import Link from "next/link";
|
|
24
|
+
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
25
|
+
import { Filter, Settings } from "lucide-react";
|
|
26
|
+
import { DataViews, emptyQuery, queryToParams, type SavedView } from "@/components/DataViews";
|
|
27
|
+
import { FormBuilder } from "@/components/FormBuilder";
|
|
28
|
+
import type {
|
|
29
|
+
DataViewsQuery,
|
|
30
|
+
FieldConfig,
|
|
31
|
+
Row,
|
|
32
|
+
} from "@/utils/dataViews/types";
|
|
33
|
+
|
|
34
|
+
// ─── Data ─────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
interface Message extends Row {
|
|
37
|
+
id: string;
|
|
38
|
+
subject: string;
|
|
39
|
+
from: string;
|
|
40
|
+
preview?: string;
|
|
41
|
+
receivedAt?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const FIELDS: FieldConfig[] = [
|
|
45
|
+
{ path: "subject", label: "Subject", type: "text" },
|
|
46
|
+
{ path: "from", label: "From", type: "text" },
|
|
47
|
+
{ path: "preview", label: "Preview", type: "text" },
|
|
48
|
+
{ path: "receivedAt", label: "Received", type: "date-format", dateFormat: "DD MMM" },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const FROM_OPTIONS = [
|
|
52
|
+
{ label: "logistics@acme.test", value: "logistics@acme.test" },
|
|
53
|
+
{ label: "ap@initech.test", value: "ap@initech.test" },
|
|
54
|
+
{ label: "supply@umbrella.test", value: "supply@umbrella.test" },
|
|
55
|
+
{ label: "procurement@hooli.test", value: "procurement@hooli.test" },
|
|
56
|
+
{ label: "ops@cyberdyne.test", value: "ops@cyberdyne.test" },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The request this page makes. The querying happens in `app/api/messages/route.ts`.
|
|
61
|
+
*/
|
|
62
|
+
async function fetchMessages(q: DataViewsQuery): Promise<{ rows: Message[]; total: number }> {
|
|
63
|
+
const params = queryToParams(q);
|
|
64
|
+
const res = await fetch(`/api/messages?${params}`);
|
|
65
|
+
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
66
|
+
return res.json();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Page ─────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* `itemHref` turns each row into a link and `linkComponent` swaps the plain anchor for the
|
|
73
|
+
* router's, so navigation stays client-side.
|
|
74
|
+
*
|
|
75
|
+
* Following one unmounts this page — the App Router replaces pages while keeping layouts — so the
|
|
76
|
+
* detail route has to stand on its own. Keeping the list beside the detail would mean hoisting
|
|
77
|
+
* `<DataViews>` into `inbox-routing/layout.tsx` and letting each page supply only the pane.
|
|
78
|
+
*/
|
|
79
|
+
export default function InboxRoutingExample() {
|
|
80
|
+
const [query, setQuery] = useState(emptyQuery());
|
|
81
|
+
const [saved, setSaved] = useState<SavedView[]>([]);
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
const { data, isPending, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
|
|
85
|
+
queryKey: ["messages", { ...query, page: undefined }],
|
|
86
|
+
queryFn: ({ pageParam }) => fetchMessages({ ...query, page: pageParam }),
|
|
87
|
+
initialPageParam: 1,
|
|
88
|
+
// Undefined means "no more" — which is what the component's `hasMore` resolves to.
|
|
89
|
+
getNextPageParam: (last, pages) => {
|
|
90
|
+
const loaded = pages.reduce((n, page) => n + page.rows.length, 0);
|
|
91
|
+
return loaded < last.total ? pages.length + 1 : undefined;
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const rows = useMemo(() => data?.pages.flatMap((page) => page.rows) ?? [], [data]);
|
|
96
|
+
const total = data?.pages[0]?.total ?? 0;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="flex h-full min-h-0 flex-col p-4">
|
|
100
|
+
<DataViews
|
|
101
|
+
rows={rows}
|
|
102
|
+
fields={FIELDS}
|
|
103
|
+
total={total}
|
|
104
|
+
loading={isPending}
|
|
105
|
+
onLoadMore={fetchNextPage}
|
|
106
|
+
loadingMore={isFetchingNextPage}
|
|
107
|
+
onQueryChange={setQuery}
|
|
108
|
+
className="h-full"
|
|
109
|
+
>
|
|
110
|
+
<DataViews.Header title="Inbox">
|
|
111
|
+
<DataViews.ViewSwitch />
|
|
112
|
+
<DataViews.Search />
|
|
113
|
+
<DataViews.PanelToggle />
|
|
114
|
+
</DataViews.Header>
|
|
115
|
+
|
|
116
|
+
<DataViews.Inbox
|
|
117
|
+
titlePath="subject"
|
|
118
|
+
datePath="receivedAt"
|
|
119
|
+
itemHref={(_row, id) => `/data-views/inbox-routing/${id}`}
|
|
120
|
+
linkComponent={Link}
|
|
121
|
+
placeholder={
|
|
122
|
+
<div className="typography-body-medium-regular text-content-presentation-global-secondary flex h-full items-center justify-center p-8">
|
|
123
|
+
Click a message to navigate to its own page.
|
|
124
|
+
</div>
|
|
125
|
+
}
|
|
126
|
+
/>
|
|
127
|
+
|
|
128
|
+
<DataViews.Panel>
|
|
129
|
+
<DataViews.Panel.Tab value="config" label="Config." icon={<Settings />}>
|
|
130
|
+
{/* Saving is the app's job — a view outlives the component. Restoring is not:
|
|
131
|
+
hand the snapshot back and selecting it puts everything back internally. */}
|
|
132
|
+
<DataViews.Panel.SavedViews
|
|
133
|
+
views={saved}
|
|
134
|
+
onSave={(snapshot) =>
|
|
135
|
+
setSaved((prev) => [
|
|
136
|
+
...prev,
|
|
137
|
+
{ id: `view-${prev.length + 1}`, label: `View ${prev.length + 1}`, snapshot },
|
|
138
|
+
])
|
|
139
|
+
}
|
|
140
|
+
/>
|
|
141
|
+
<DataViews.Panel.Columns />
|
|
142
|
+
<DataViews.Panel.Sort />
|
|
143
|
+
</DataViews.Panel.Tab>
|
|
144
|
+
|
|
145
|
+
<DataViews.Panel.Tab value="filters" label="Filters" icon={<Filter />}>
|
|
146
|
+
{/* The controls are FormBuilder fields — the same Select and Slider any form in
|
|
147
|
+
this library uses. The <FormBuilder> itself lives inside Filters; you write only
|
|
148
|
+
its fields, and Filters reads each one's name, label and bounds to learn what
|
|
149
|
+
it is. */}
|
|
150
|
+
<DataViews.Filters
|
|
151
|
+
title={null}
|
|
152
|
+
className="border-b-0 p-0"
|
|
153
|
+
>
|
|
154
|
+
{/* A message carries no enum, no numeric field and one date, so only the
|
|
155
|
+
searchable single-select, the date range and the text match have data to bind
|
|
156
|
+
to. A checkbox list over a field that does not exist would filter to nothing. */}
|
|
157
|
+
<FormBuilder.Text name="subject" label="Subject contains" />
|
|
158
|
+
<FormBuilder.SearchableSelect name="from" label="From" options={FROM_OPTIONS} />
|
|
159
|
+
<FormBuilder.DateRange name="receivedAt" label="Received" />
|
|
160
|
+
</DataViews.Filters>
|
|
161
|
+
</DataViews.Panel.Tab>
|
|
162
|
+
</DataViews.Panel>
|
|
163
|
+
</DataViews>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## `apps/app/data-views/inbox-routing/[id]/page.tsx`
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import Link from "next/link";
|
|
173
|
+
import { notFound } from "next/navigation";
|
|
174
|
+
import { MESSAGES } from "../../../api/_lib/messages";
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Where `itemHref` lands. A plain server-rendered page — no `DataViews` involved, because by this
|
|
178
|
+
* point the inbox has unmounted.
|
|
179
|
+
*
|
|
180
|
+
* This route is scoped to the one example that needs it. A `[id]` directly under `data-views/`
|
|
181
|
+
* would swallow every mistyped example slug and render a "detail" page instead of a 404.
|
|
182
|
+
*
|
|
183
|
+
* It reads the same store `/api/messages` serves rather than fetching from it: this already runs
|
|
184
|
+
* on the server, and a server component calling its own HTTP endpoint pays for a round trip to
|
|
185
|
+
* itself. The list page fetches because it runs in the browser.
|
|
186
|
+
*/
|
|
187
|
+
|
|
188
|
+
export default async function MessageDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
|
189
|
+
const { id } = await params;
|
|
190
|
+
const message = MESSAGES.find((m) => m.id === id);
|
|
191
|
+
if (!message) notFound();
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div className="flex h-full flex-col gap-4 overflow-y-auto p-8">
|
|
195
|
+
<Link
|
|
196
|
+
href="/data-views/inbox-routing"
|
|
197
|
+
className="typography-body-small-regular text-content-presentation-action-link flex w-fit items-center gap-1"
|
|
198
|
+
>
|
|
199
|
+
<i className="ri-arrow-left-line" aria-hidden />
|
|
200
|
+
Back to the inbox
|
|
201
|
+
</Link>
|
|
202
|
+
|
|
203
|
+
<article className="flex flex-col gap-3">
|
|
204
|
+
<h1 className="typography-headers-medium-medium text-content-presentation-global-primary">
|
|
205
|
+
{message.subject}
|
|
206
|
+
</h1>
|
|
207
|
+
<p className="typography-body-small-regular text-content-presentation-global-secondary">
|
|
208
|
+
{message.from}
|
|
209
|
+
{message.receivedAt ? ` · ${message.receivedAt}` : " · no date"}
|
|
210
|
+
</p>
|
|
211
|
+
<p className="typography-body-medium-regular text-content-presentation-global-primary max-w-[65ch]">
|
|
212
|
+
{message.preview ?? "This message has no preview text."}
|
|
213
|
+
</p>
|
|
214
|
+
</article>
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: DataViews examples
|
|
3
|
+
description: Every runnable DataViews example page, as shipped documentation.
|
|
4
|
+
group: examples
|
|
5
|
+
component: DataViews
|
|
6
|
+
keywords: [data-views, example, examples, index]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# DataViews examples
|
|
10
|
+
|
|
11
|
+
Each page below is complete and runnable. They ship with the package, so the code an agent is pointed at is code it can actually read.
|
|
12
|
+
|
|
13
|
+
| Example | Shows |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| [Views](./views.md) | All four views over one dataset, with drag round-trips. |
|
|
16
|
+
| [Inbox → route](./inbox-routing.md) | `itemHref` + `linkComponent` — the detail pane driven by the URL. |
|
|
17
|
+
| [Tree → custom UI](./tree-custom.md) | Every custom-UI seam of the tree: `renderNode`, `paneRows`, a custom cell, card and tab, `paneActions`, and a whole-pane override. |
|
|
18
|
+
| [Fields](./fields.md) | The field types, painted. |
|
|
19
|
+
| [Filters](./filters.md) | Every filter control, presets, custom filters and the summary. |
|
|
20
|
+
| [Server-side](./server-side.md) | `queryToParams` on the way out, `parseQuery` on the way in. |
|
|
21
|
+
| [Panel](./panel.md) | The settings rail: saved views, columns, sort — and the pane-mode round trip. |
|
|
22
|
+
| [State](./state.md) | Controlled versus uncontrolled query. |
|
|
23
|
+
| [View registry](./view-registry.md) | A view of your own via `markView`, beside the built-in four. |
|
|
24
|
+
| [Scale](./scale.md) | Virtualization and scroll loading at size. |
|
|
25
|
+
| [Keyboard & RTL](./a11y-rtl.md) | Keyboard paths and the RTL mirror. |
|
|
26
|
+
| [Everything at once](./overview.md) | Every part at once — the fastest way to see the whole shape. |
|
|
27
|
+
| [The endpoint these examples call](./api-orders-route.md) | The route handler every DataViews example fetches from — and the only worked example of `parseQuery` on the server. |
|
|
28
|
+
|
|
29
|
+
See the [component reference](../index.md) and the [guide](../guide.md).
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: DataViews example — Everything at once
|
|
3
|
+
description: Every part at once — the fastest way to see the whole shape.
|
|
4
|
+
group: examples
|
|
5
|
+
component: DataViews
|
|
6
|
+
keywords: [data-views, example, examples, overview]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# DataViews example — Everything at once
|
|
10
|
+
|
|
11
|
+
Every part at once — the fastest way to see the whole shape.
|
|
12
|
+
|
|
13
|
+
Complete and runnable — this is the page itself, not an excerpt. In the monorepo it lives at `apps/app/data-views/overview/page.tsx`.
|
|
14
|
+
|
|
15
|
+
See the [component reference](../index.md) for what each prop does, or the [guide](../guide.md) for the same ground as scenarios.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
"use client";
|
|
19
|
+
|
|
20
|
+
import { useMemo, useState } from "react";
|
|
21
|
+
import { useMutation, useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
|
22
|
+
import { Filter, Settings } from "lucide-react";
|
|
23
|
+
import { Button } from "@/components/Button";
|
|
24
|
+
import { DataViews, emptyQuery, queryToParams, type SavedView } from "@/components/DataViews";
|
|
25
|
+
import { FormBuilder } from "@/components/FormBuilder";
|
|
26
|
+
import type {
|
|
27
|
+
DataViewsQuery,
|
|
28
|
+
FieldConfig,
|
|
29
|
+
Row,
|
|
30
|
+
RowGroup,
|
|
31
|
+
TreeNode,
|
|
32
|
+
} from "@/utils/dataViews/types";
|
|
33
|
+
|
|
34
|
+
// ─── Data ─────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
interface Order extends Row {
|
|
37
|
+
id: number;
|
|
38
|
+
customer: { name: string };
|
|
39
|
+
status: "Pending" | "Shipped" | "Delivered";
|
|
40
|
+
priority: "High" | "Medium" | "Low";
|
|
41
|
+
total: number;
|
|
42
|
+
createdAt: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const FIELDS: FieldConfig[] = [
|
|
46
|
+
{ path: "id", label: "Order #", type: "number" },
|
|
47
|
+
{ path: "customer.name", label: "Customer", type: "text" },
|
|
48
|
+
{ path: "brand.name", label: "Brand", type: "text" },
|
|
49
|
+
{ path: "status", label: "Status", type: "enum-badge", variants: { Pending: "yellow", Shipped: "blue", Delivered: "green" } },
|
|
50
|
+
{ path: "priority", label: "Priority", type: "enum-badge", variants: { High: "redOrange", Medium: "purple", Low: "gray" } },
|
|
51
|
+
{ path: "total", label: "Total", type: "currency", currency: "USD" },
|
|
52
|
+
{ path: "createdAt", label: "Created", type: "date-format", dateFormat: "YYYY-MM-DD" },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/** Dynamic sets — in a real app these come from the endpoint that also does the filtering. */
|
|
56
|
+
const CUSTOMER_OPTIONS = [
|
|
57
|
+
"Acme Inc.", "Globex Corp.", "Initech", "Umbrella",
|
|
58
|
+
"Hooli", "Stark Industries", "Wayne Enterprises", "Cyberdyne",
|
|
59
|
+
].map((v) => ({ label: v, value: v }));
|
|
60
|
+
|
|
61
|
+
const BRAND_OPTIONS = ["Bosch", "Makita", "DeWalt", "Hilti"].map((v) => ({ label: v, value: v }));
|
|
62
|
+
|
|
63
|
+
const PRIORITY_OPTIONS = ["High", "Medium", "Low"].map((v) => ({ label: v, value: v }));
|
|
64
|
+
|
|
65
|
+
const STATUS_OPTIONS = [
|
|
66
|
+
{ label: "Pending", value: "Pending" },
|
|
67
|
+
{ label: "Shipped", value: "Shipped" },
|
|
68
|
+
{ label: "Delivered", value: "Delivered" },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The request this page makes. The querying itself happens in `app/api/orders/route.ts` —
|
|
73
|
+
* nothing on this page filters, sorts or pages anything, which is the split DataViews is built
|
|
74
|
+
* around.
|
|
75
|
+
*/
|
|
76
|
+
async function fetchOrders(q: DataViewsQuery): Promise<{ rows: Order[]; total: number }> {
|
|
77
|
+
const params = queryToParams(q);
|
|
78
|
+
const res = await fetch(`/api/orders?${params}`);
|
|
79
|
+
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
80
|
+
return res.json();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The other half of `onRowMove`: the board emits intent, this is what persists it. */
|
|
84
|
+
async function moveOrder(body: { id?: number; status?: string; reset?: boolean }) {
|
|
85
|
+
const res = await fetch("/api/orders", {
|
|
86
|
+
method: "PATCH",
|
|
87
|
+
headers: { "Content-Type": "application/json" },
|
|
88
|
+
body: JSON.stringify(body),
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
91
|
+
return res.json();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const groupByStatus = (rows: readonly Order[]): RowGroup[] =>
|
|
95
|
+
(["Pending", "Shipped", "Delivered", "Cancelled"] as const).map((status) => ({
|
|
96
|
+
id: status,
|
|
97
|
+
label: status,
|
|
98
|
+
color: ({ Pending: "gray", Shipped: "blue", Delivered: "green", Cancelled: "red" } as const)[status],
|
|
99
|
+
rows: rows.filter((row) => row.status === status),
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
/** Every node is a real row here, so the detail pane fills whichever one you select. */
|
|
103
|
+
const nodesFromRows = (rows: readonly Order[]): TreeNode[] => {
|
|
104
|
+
const [first, ...rest] = rows;
|
|
105
|
+
if (!first) return [];
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
id: String(first.id),
|
|
109
|
+
row: first,
|
|
110
|
+
depth: 0,
|
|
111
|
+
children: rest.slice(0, 4).map((row) => ({ id: String(row.id), row, depth: 1, children: [] })),
|
|
112
|
+
},
|
|
113
|
+
...rest.slice(4).map((row) => ({ id: String(row.id), row, depth: 0, children: [] })),
|
|
114
|
+
];
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// ─── Page ─────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/** Every part at once: four views, the rail, filters, an empty state and a drag round-trip. */
|
|
120
|
+
export default function OverviewExample() {
|
|
121
|
+
const queryClient = useQueryClient();
|
|
122
|
+
const [query, setQuery] = useState(emptyQuery());
|
|
123
|
+
const [saved, setSaved] = useState<SavedView[]>([]);
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
const { data, isPending, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
|
|
128
|
+
// The key *is* the query: touch any part of it and TanStack refetches, and a response that
|
|
129
|
+
// has been superseded is discarded rather than landing on top of a newer one.
|
|
130
|
+
queryKey: ["overview-orders", { ...query, page: undefined }],
|
|
131
|
+
queryFn: ({ pageParam }) => fetchOrders({ ...query, page: pageParam }),
|
|
132
|
+
initialPageParam: 1,
|
|
133
|
+
// Undefined means "no more" — which is what the component's `hasMore` resolves to.
|
|
134
|
+
getNextPageParam: (last, pages) => {
|
|
135
|
+
const loaded = pages.reduce((n, page) => n + page.rows.length, 0);
|
|
136
|
+
return loaded < last.total ? pages.length + 1 : undefined;
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Memoised because `data?.rows ?? []` is a new array on every render, which would make the
|
|
141
|
+
// `groups`/`nodes` memos below miss every time and hand the board a new array to diff.
|
|
142
|
+
const rows = useMemo(() => data?.pages.flatMap((page) => page.rows) ?? [], [data]);
|
|
143
|
+
const total = data?.pages[0]?.total ?? 0;
|
|
144
|
+
const groups = useMemo(() => groupByStatus(rows), [rows]);
|
|
145
|
+
const nodes = useMemo(() => nodesFromRows(rows), [rows]);
|
|
146
|
+
|
|
147
|
+
// Dropping a card emits intent; this is what persists it. The card settles where it landed
|
|
148
|
+
// only once the refetched rows agree.
|
|
149
|
+
const move = useMutation({
|
|
150
|
+
mutationFn: moveOrder,
|
|
151
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["overview-orders"] }),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<div className="flex h-full min-h-0 flex-col p-4">
|
|
156
|
+
<DataViews
|
|
157
|
+
rows={rows}
|
|
158
|
+
fields={FIELDS}
|
|
159
|
+
total={total}
|
|
160
|
+
loading={isPending}
|
|
161
|
+
onLoadMore={fetchNextPage}
|
|
162
|
+
loadingMore={isFetchingNextPage}
|
|
163
|
+
onQueryChange={setQuery}
|
|
164
|
+
className="h-full"
|
|
165
|
+
>
|
|
166
|
+
<DataViews.Header title="Orders">
|
|
167
|
+
<DataViews.ViewSwitch />
|
|
168
|
+
<DataViews.Search />
|
|
169
|
+
<DataViews.Actions>
|
|
170
|
+
<Button variant="BluColStyle" size="M">New order</Button>
|
|
171
|
+
<Button variant="BluColStyle" size="M" onClick={() => move.mutate({ reset: true })}>
|
|
172
|
+
Reset
|
|
173
|
+
</Button>
|
|
174
|
+
</DataViews.Actions>
|
|
175
|
+
<DataViews.PanelToggle />
|
|
176
|
+
</DataViews.Header>
|
|
177
|
+
|
|
178
|
+
<DataViews.Table selectable />
|
|
179
|
+
<DataViews.Board
|
|
180
|
+
groups={groups}
|
|
181
|
+
titlePath="customer.name"
|
|
182
|
+
// A drop outside any column has nowhere to go, so there is nothing to persist.
|
|
183
|
+
onRowMove={(intent) => {
|
|
184
|
+
if (intent.to) move.mutate({ id: Number(intent.id), status: intent.to });
|
|
185
|
+
}}
|
|
186
|
+
/>
|
|
187
|
+
<DataViews.Inbox
|
|
188
|
+
titlePath="customer.name"
|
|
189
|
+
datePath="createdAt"
|
|
190
|
+
>
|
|
191
|
+
<DataViews.Detail />
|
|
192
|
+
</DataViews.Inbox>
|
|
193
|
+
{/* The pane's tabs are children, like every other part: pass none and there is no pane. */}
|
|
194
|
+
<DataViews.Tree nodes={nodes} labelPath="customer.name">
|
|
195
|
+
<DataViews.Tree.Table />
|
|
196
|
+
<DataViews.Tree.Cards />
|
|
197
|
+
</DataViews.Tree>
|
|
198
|
+
|
|
199
|
+
<DataViews.Panel>
|
|
200
|
+
<DataViews.Panel.Tab value="config" label="Config." icon={<Settings />}>
|
|
201
|
+
{/* Saving is the app's job — a view outlives the component. Restoring is not:
|
|
202
|
+
hand the snapshot back and selecting it puts everything back internally. */}
|
|
203
|
+
<DataViews.Panel.SavedViews
|
|
204
|
+
views={saved}
|
|
205
|
+
onSave={(snapshot) =>
|
|
206
|
+
setSaved((prev) => [
|
|
207
|
+
...prev,
|
|
208
|
+
{ id: `view-${prev.length + 1}`, label: `View ${prev.length + 1}`, snapshot },
|
|
209
|
+
])
|
|
210
|
+
}
|
|
211
|
+
/>
|
|
212
|
+
<DataViews.Panel.Columns />
|
|
213
|
+
<DataViews.Panel.Sort />
|
|
214
|
+
</DataViews.Panel.Tab>
|
|
215
|
+
|
|
216
|
+
<DataViews.Panel.Tab value="filters" label="Filters" icon={<Filter />}>
|
|
217
|
+
{/* The controls are FormBuilder fields — the same Select and Slider any form in
|
|
218
|
+
this library uses. The <FormBuilder> itself lives inside Filters; you write only
|
|
219
|
+
its fields, and Filters reads each one's name, label and bounds to learn what
|
|
220
|
+
it is. */}
|
|
221
|
+
<DataViews.Filters
|
|
222
|
+
title={null}
|
|
223
|
+
className="border-b-0 p-0"
|
|
224
|
+
>
|
|
225
|
+
{/* One control per section type. Which one a field gets is decided by the data:
|
|
226
|
+
can the option set grow, and can the user pick more than one. */}
|
|
227
|
+
<FormBuilder.CheckboxGroup name="status" label="Status" options={STATUS_OPTIONS} />
|
|
228
|
+
<FormBuilder.RadioList name="priority" label="Priority" options={PRIORITY_OPTIONS} />
|
|
229
|
+
<FormBuilder.SearchableSelect
|
|
230
|
+
name="customer.name"
|
|
231
|
+
label="Customer"
|
|
232
|
+
options={CUSTOMER_OPTIONS}
|
|
233
|
+
/>
|
|
234
|
+
<FormBuilder.MultiSelect name="brand.name" label="Brand" options={BRAND_OPTIONS} />
|
|
235
|
+
<FormBuilder.Slider name="total" label="Total" range min={0} max={15000} step={100} />
|
|
236
|
+
<FormBuilder.DateRange name="createdAt" label="Created" />
|
|
237
|
+
</DataViews.Filters>
|
|
238
|
+
</DataViews.Panel.Tab>
|
|
239
|
+
</DataViews.Panel>
|
|
240
|
+
</DataViews>
|
|
241
|
+
</div>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
```
|