blixify-ui-web-mcp 0.1.44 → 0.1.47

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.
@@ -679,6 +679,18 @@ Editors (`edit.allEditable`) get add, edit, remove and move on each chart; add i
679
679
 
680
680
  ---
681
681
 
682
+ ### Related records
683
+
684
+ Set `organise.showRelatedTab` to give the read view a tab for each other space with a `reference` or `listReference` column pointing at this record, beside Card View and named with its row count (e.g. `Orders (3)`). It is off by default: it shows other spaces' rows, and opening a record then runs one schema query plus one single-row count per referencing column. Spaces are found the same way the delete check finds them: through `devSettings.schemaEndpoint` (the workspace's datasheets, which needs the `space` prop) or `bareSettings.bareReferenceDel`. Only spaces with matching rows get a tab.
685
+
686
+ A tab shows that space as a read-only list Data Template (table, search, paging), filtered with `=` for a reference or `in` for a list reference. Picking a row opens it in the same tab with a Back button, so the record's own tabs stay on screen.
687
+
688
+ ```tsx
689
+ organise={{ viewId, handleNavigate, showRelatedTab: true }}
690
+ ```
691
+
692
+ ---
693
+
682
694
  ### With search and filter
683
695
 
684
696
  Use `organise.additionalQuery` to add filter controls above the list. Each entry maps to a filter UI control.
@@ -1145,6 +1157,11 @@ Which field types can be edited from the list table, and how. Types not listed u
1145
1157
  | `persistKey` | `string` | Keeps a separate saved view state for this instance: the storage key becomes `blixify-dtw-persist-<collectionId>-<persistKey>`. Saved state is read once, on mount, so give the component a React `key` that changes with `persistKey` |
1146
1158
  | `savedViews` | `boolean` | Shows a row of view tags under the tab bar: save the current filters, flex filters, sort, rows per page and columns under a name, reapply it, update it or delete it. Views belong to the tab they were saved on and are kept in `localStorage` under `blixify-dtw-views-<collectionId>[-<persistKey>][-<currentUser.userEmail>]`. Sort is held by the template while it is on, as with `persistViewState`, and applying a view calls `onChangeSort`, `onChangePagination(0)` and `bareListHandleFilter` when those change. Each write re-reads storage first, so views saved in another browser tab are kept. With `onChangeHide` / `onChangeDrag`, a view's hides and column order stay on screen only: a toggle or reorder after applying one writes back the schema's columns plus that single change |
1147
1159
  | `defaultSavedViews` | `DefaultSavedView[]` | Views provided by the app (`{ id?, name, tabId?, state }`), listed before the user's own and never deleted. Without `tabId` a view is listed on every tab. A field left out of `state` is reset to its default when the view is applied. `id` must be unique; without one the view gets `default-<index>`, its position in the whole array |
1160
+ | `sharedViews` | `SharedSavedView[]` | Views the app stores for everyone (`{ id, name, tabId, state }`), e.g. on the record the list belongs to. Given, it replaces this browser's storage: the list shows only these, saving goes to `onChangeSharedViews`, and nothing is read from or written to `localStorage` |
1161
+ | `onChangeSharedViews` | `(views: SharedSavedView[]) => void` | Called with the whole list after a view is saved, updated or deleted, for the app to store and pass back as `sharedViews`; a second change before it comes back builds on the list just sent. The whole list is replaced each time, so two editors saving at once is last-writer-wins. Without it the app's views are read-only and “+ Save view” is hidden |
1162
+ | `defaultSavedViewId` | `string` | The view everyone opens on, marked ★, stored by the app. The list opens on it when no `initialSavedViewId` is given — also when it arrives after mount — unless `persistViewState` restored a state or a view was already picked. A view that no longer exists leaves the list on All. Opening on it does not call `onChangeSort` |
1163
+ | `onChangeDefaultSavedViewId` | `(id: string \| null) => void` | Gives each view a ☆ that makes it the default; the ★ on the default takes it off (`null`, the list opens on All). Deleting the default view also sends `null` |
1164
+ | `initialSavedViewId` | `string` | A view the list opens on, once, in place of the ★ default — e.g. a page embedding a datasheet opens it on a chosen view. A view that no longer exists, or is on another tab, leaves the list on All. Opening on it does not call `onChangeSort` |
1148
1165
  | `groupBy` | `string` | Column ID to group list results by |
1149
1166
  | `groupIdCollapsible` | `{ [key: string]: boolean }` | Initial collapsed/expanded state per group ID |
1150
1167
  | `sortGroupByAlphabetically` | `boolean` | Sorts group-by values alphabetically |
@@ -1159,6 +1176,7 @@ Which field types can be edited from the list table, and how. Types not listed u
1159
1176
  | `customPaginationCount` | `(pageDataCount: number) => void` | Called after each data fetch with the actual record count. Use this to override the count shown in the pagination bar — store the value in your own state and pass it back via `customPaginationSize` on `Table`, `Grid`, `List`, or `Pagination` if needed, or use it to display a custom count elsewhere in your UI |
1160
1177
  | `onAddColumns` | `(model: DataStructure[]) => void` | Called when a new column is added |
1161
1178
  | `handleNavigate` | `(type: string, selectedId?: string, viewId?: string) => void` | Called when the component needs to navigate (e.g. row click → read view) |
1179
+ | `showRelatedTab` | `boolean` | Adds a read view tab per other space whose rows reference the record. Default `false` |
1162
1180
  | `handleCustomDownloadFile` | `(url: string, fileName?: string) => void` | Overrides the default file download behaviour |
1163
1181
  | `renderReferenceLabelOptions` | `(spaceId?: string, includeReference?: boolean, type?: string) => Option[]` | Returns column options for reference label selectors |
1164
1182
  | `renderReferenceSpaceOptions` | `() => Option[]` | Returns space options for reference space selectors |
@@ -14,7 +14,7 @@ import {
14
14
  renderDemoRecords,
15
15
  TABS,
16
16
  } from "./savedViewsDemo.fixtures";
17
- import { buildSavedViewsKey } from "./viewStatePersistence";
17
+ import { buildSavedViewsKey, SharedSavedView } from "./viewStatePersistence";
18
18
 
19
19
  const meta = { title: "Data/Data Template" };
20
20
  export default meta;
@@ -140,3 +140,146 @@ export const SavedViews = () => (
140
140
  <SavedViewsDemo />
141
141
  </div>
142
142
  );
143
+
144
+ const SharedViewsDemo = () => {
145
+ const [viewId, setViewId] = useState("general");
146
+ const [isMockReady, setIsMockReady] = useState(false);
147
+ // INFO: what an app would store on the record the list belongs to
148
+ const [sharedViews, setSharedViews] = useState<SharedSavedView[]>([
149
+ {
150
+ id: "shared-due-last",
151
+ name: "Due last",
152
+ tabId: "general",
153
+ state: { sort: { sortId: "dueDate", type: "desc" } },
154
+ },
155
+ ]);
156
+ const [defaultSavedViewId, setDefaultSavedViewId] = useState<string | null>(
157
+ null,
158
+ );
159
+ const [openOn, setOpenOn] = useState("");
160
+ const [isEditor, setIsEditor] = useState(true);
161
+ // INFO: remounts the list, as reopening the page would
162
+ const [reloadKey, setReloadKey] = useState(0);
163
+ // INFO: hands the default over a moment after mount, as an app loading it
164
+ // from its record does
165
+ const [isDefaultLoading, setIsDefaultLoading] = useState(false);
166
+
167
+ const handleReloadWithLateDefault = () => {
168
+ setIsDefaultLoading(true);
169
+ setReloadKey((prev) => prev + 1);
170
+ setTimeout(() => setIsDefaultLoading(false), 1000);
171
+ };
172
+ const store = useMemo(() => renderDemoRecords(), []);
173
+
174
+ useEffect(() => {
175
+ const mock = handleInstallDemoMock(store);
176
+ setIsMockReady(true);
177
+ return () => {
178
+ mock.restore();
179
+ setIsMockReady(false);
180
+ };
181
+ }, [store]);
182
+
183
+ return (
184
+ <div className="w-full">
185
+ <div className="mb-4 flex flex-wrap items-center gap-3">
186
+ <ButtonGroup
187
+ selection={[
188
+ { id: "editor", name: "Editor" },
189
+ { id: "reader", name: "Reader" },
190
+ ]}
191
+ selectedId={isEditor ? "editor" : "reader"}
192
+ variant="tag"
193
+ size="small"
194
+ onClick={(id) => setIsEditor(id === "editor")}
195
+ />
196
+ <select
197
+ className="rounded-md border border-gray-300 px-2 py-1 text-sm"
198
+ value={openOn}
199
+ onChange={(event) => setOpenOn(event.target.value)}
200
+ >
201
+ <option value="">Open on: ★ default</option>
202
+ <option value="missing-view">Open on: a deleted view</option>
203
+ {sharedViews.map((eachView) => (
204
+ <option key={eachView.id} value={eachView.id}>
205
+ Open on: {eachView.name}
206
+ </option>
207
+ ))}
208
+ </select>
209
+ <button
210
+ type="button"
211
+ className="rounded-md border border-gray-300 bg-white px-3 py-1 text-sm text-gray-700 hover:bg-gray-50"
212
+ onClick={() => setReloadKey((prev) => prev + 1)}
213
+ >
214
+ Reload
215
+ </button>
216
+ <button
217
+ type="button"
218
+ className="rounded-md border border-gray-300 bg-white px-3 py-1 text-sm text-gray-700 hover:bg-gray-50"
219
+ onClick={handleReloadWithLateDefault}
220
+ >
221
+ Reload, default arrives late
222
+ </button>
223
+ </div>
224
+ {isMockReady && (
225
+ <DataTemplateImpl
226
+ key={`${openOn}-${isEditor}-${reloadKey}`}
227
+ title="Tasks"
228
+ description="40 offline records"
229
+ type="list"
230
+ collectionId={DEMO_COLLECTION}
231
+ model={DEMO_SCHEMA}
232
+ localURLEndpoint=""
233
+ devSettings={demoDevSettings}
234
+ lib={{ axios, Menu, Transition, papaParser: Papa }}
235
+ limit={10}
236
+ organise={{
237
+ extendedTabs: TABS,
238
+ viewId,
239
+ savedViews: true,
240
+ sharedViews,
241
+ onChangeSharedViews: isEditor ? setSharedViews : undefined,
242
+ defaultSavedViewId: isDefaultLoading
243
+ ? undefined
244
+ : (defaultSavedViewId ?? undefined),
245
+ onChangeDefaultSavedViewId: isEditor
246
+ ? setDefaultSavedViewId
247
+ : undefined,
248
+ initialSavedViewId: openOn || undefined,
249
+ handleNavigate: (type: string, _id?: string, view?: string) => {
250
+ if (type === "list" && view) setViewId(view);
251
+ },
252
+ }}
253
+ bareSettings={{ bareNewDTW: true, bareFlexFilters: true }}
254
+ />
255
+ )}
256
+ <pre
257
+ data-testid="shared-views-store"
258
+ className="mt-4 max-h-48 overflow-auto rounded bg-gray-50 p-3 text-xs text-gray-600"
259
+ >
260
+ {JSON.stringify({ defaultSavedViewId, views: sharedViews }, null, 2)}
261
+ </pre>
262
+ </div>
263
+ );
264
+ };
265
+
266
+ export const SharedSavedViews = () => (
267
+ <div className="w-full">
268
+ <div className="font-semibold mb-1 text-gray-900">
269
+ Data Template — views stored by the app
270
+ </div>
271
+ <div className="text-sm mb-4 text-gray-600">
272
+ With sharedViews, views come from the app — here story state, shown below
273
+ — instead of this browser, so the whole team sees the same ones. As Editor
274
+ (onChangeSharedViews and onChangeDefaultSavedViewId given), “+ Save view”
275
+ hands the list to the app, and the ☆ on a view makes it the ★ default
276
+ everyone opens on; ★ again takes it off, leaving All. As Reader the views
277
+ are used, the default shows ★, and nothing can be changed. “Open on” sets
278
+ initialSavedViewId, how a page embedding a datasheet opens it on a chosen
279
+ view; a deleted view opens on All. “Reload” remounts the list, and
280
+ “default arrives late” hands the ★ over a second after mount, as an app
281
+ loading it does.
282
+ </div>
283
+ <SharedViewsDemo />
284
+ </div>
285
+ );
@@ -752,3 +752,275 @@ export const ChartView = () => (
752
752
  <ChartViewDemo />
753
753
  </div>
754
754
  );
755
+
756
+ // INFO: a Customers space that three other datasheets point at — Orders with a
757
+ // reference column, Support Tickets with a list reference, and Invoices whose
758
+ // rows reference other customers only. Notes holds a text column that names
759
+ // the space but is not a reference, which the Related tab must ignore.
760
+ const RELATED_WORKSPACE = "demo-workspace";
761
+ const RELATED_CUSTOMERS = "demo-customers";
762
+
763
+ const RELATED_CUSTOMER_SCHEMA: DataStructure[] = [
764
+ { id: "name", name: "Customer", type: "string" },
765
+ { id: "email", name: "Email", type: "email" },
766
+ {
767
+ id: "tier",
768
+ name: "Tier",
769
+ type: "list",
770
+ listSelection: ["standard", "gold"],
771
+ listSelectionLabel: ["Standard", "Gold"],
772
+ },
773
+ ];
774
+
775
+ const RELATED_SPACES: any[] = [
776
+ {
777
+ _id: "demo-orders",
778
+ name: "Orders",
779
+ type: "datasheet",
780
+ workspaceId: RELATED_WORKSPACE,
781
+ structure: [
782
+ { id: "orderNo", name: "Order No", type: "string" },
783
+ {
784
+ id: "status",
785
+ name: "Status",
786
+ type: "list",
787
+ listSelection: ["pending", "paid", "shipped"],
788
+ listSelectionLabel: ["Pending", "Paid", "Shipped"],
789
+ },
790
+ { id: "amount", name: "Amount", type: "number" },
791
+ { id: "createdAt", name: "Created At", type: "date" },
792
+ {
793
+ id: "customerId",
794
+ name: "Customer",
795
+ type: "reference",
796
+ referenceSpaceId: RELATED_CUSTOMERS,
797
+ referenceLabel: "name",
798
+ },
799
+ ],
800
+ },
801
+ {
802
+ _id: "demo-tickets",
803
+ name: "Support Tickets",
804
+ type: "datasheet",
805
+ workspaceId: RELATED_WORKSPACE,
806
+ structure: [
807
+ { id: "subject", name: "Subject", type: "string" },
808
+ {
809
+ id: "priority",
810
+ name: "Priority",
811
+ type: "list",
812
+ listSelection: ["low", "high"],
813
+ listSelectionLabel: ["Low", "High"],
814
+ },
815
+ { id: "openedAt", name: "Opened", type: "date" },
816
+ {
817
+ id: "customerIds",
818
+ name: "Customers",
819
+ type: "listReference",
820
+ referenceSpaceId: RELATED_CUSTOMERS,
821
+ referenceLabel: "name",
822
+ },
823
+ ],
824
+ },
825
+ {
826
+ _id: "demo-invoices",
827
+ name: "Invoices",
828
+ type: "datasheet",
829
+ workspaceId: RELATED_WORKSPACE,
830
+ structure: [
831
+ { id: "invoiceNo", name: "Invoice No", type: "string" },
832
+ { id: "total", name: "Total", type: "number" },
833
+ {
834
+ id: "customerId",
835
+ name: "Customer",
836
+ type: "reference",
837
+ referenceSpaceId: RELATED_CUSTOMERS,
838
+ referenceLabel: "name",
839
+ },
840
+ ],
841
+ },
842
+ {
843
+ _id: "demo-notes",
844
+ name: "Notes",
845
+ type: "datasheet",
846
+ workspaceId: RELATED_WORKSPACE,
847
+ structure: [
848
+ { id: "text", name: "Text", type: "string" },
849
+ {
850
+ id: "customerName",
851
+ name: "Customer Name",
852
+ type: "string",
853
+ referenceSpaceId: RELATED_CUSTOMERS,
854
+ },
855
+ ],
856
+ },
857
+ ];
858
+
859
+ const renderRelatedRecords = (): { [collection: string]: any[] } => {
860
+ const day = (offset: number) =>
861
+ new Date(Date.UTC(2026, 9, 12 - offset, 9)).toISOString();
862
+ return {
863
+ [RELATED_CUSTOMERS]: [
864
+ {
865
+ _id: "c1",
866
+ name: "Northwind Traders",
867
+ email: "orders@northwind.example",
868
+ tier: "gold",
869
+ },
870
+ {
871
+ _id: "c2",
872
+ name: "Contoso Retail",
873
+ email: "hello@contoso.example",
874
+ tier: "standard",
875
+ },
876
+ ],
877
+ "demo-orders": [
878
+ ["o1", "ORD-1001", "paid", 420, 1, "c1"],
879
+ ["o2", "ORD-1002", "shipped", 1280, 3, "c1"],
880
+ ["o3", "ORD-1003", "pending", 95, 5, "c2"],
881
+ ["o4", "ORD-1004", "pending", 310, 6, "c1"],
882
+ ].map(([_id, orderNo, status, amount, ago, customerId]) => ({
883
+ _id,
884
+ orderNo,
885
+ status,
886
+ amount,
887
+ createdAt: day(ago as number),
888
+ customerId,
889
+ })),
890
+ "demo-tickets": [
891
+ {
892
+ _id: "t1",
893
+ subject: "Late delivery",
894
+ priority: "high",
895
+ openedAt: day(2),
896
+ customerIds: ["c1"],
897
+ },
898
+ {
899
+ _id: "t2",
900
+ subject: "Invoice copy",
901
+ priority: "low",
902
+ openedAt: day(4),
903
+ customerIds: ["c1", "c2"],
904
+ },
905
+ ],
906
+ "demo-invoices": [
907
+ { _id: "i1", invoiceNo: "INV-77", total: 95, customerId: "c2" },
908
+ ],
909
+ "demo-notes": [{ _id: "n1", text: "Call back", customerName: "c1" }],
910
+ };
911
+ };
912
+
913
+ const handleInstallRelatedMock = (store: { [collection: string]: any[] }) => {
914
+ const mock = new MockAdapter(axios, {
915
+ onNoMatch: "passthrough",
916
+ delayResponse: 120,
917
+ });
918
+ const handleParseBody = (config: any) => {
919
+ try {
920
+ return JSON.parse(config?.data ?? "{}");
921
+ } catch (err) {
922
+ return {};
923
+ }
924
+ };
925
+ // INFO: "=" for a reference column, "in" for a list reference — the two
926
+ // queries the Related tab sends
927
+ const handleMatch = (record: any, query: any[] = []) =>
928
+ query.every((eachQuery) => {
929
+ const value = record[eachQuery.queryId];
930
+ if (eachQuery.type === "=") return value === eachQuery.value;
931
+ if (eachQuery.type === "in") {
932
+ const values = Array.isArray(value) ? value : [value];
933
+ return values.some((eachValue) => eachQuery.value.includes(eachValue));
934
+ }
935
+ return true;
936
+ });
937
+
938
+ mock.onPost(`${DEMO_ENDPOINT}/spaces/list`).reply(200, {
939
+ data: RELATED_SPACES,
940
+ count: RELATED_SPACES.length,
941
+ });
942
+
943
+ mock.onPost(new RegExp(`${DEMO_ENDPOINT}/([^/]+)/list$`)).reply((config) => {
944
+ const collection = config.url?.split("/").slice(-2)[0] ?? "";
945
+ const body = handleParseBody(config);
946
+ const rows = (store[collection] ?? []).filter((eachRecord) =>
947
+ handleMatch(eachRecord, body.query),
948
+ );
949
+ const limit = typeof body.limit === "number" ? body.limit : 10;
950
+ return [200, { data: rows.slice(0, limit), count: rows.length }];
951
+ });
952
+
953
+ mock.onPost(new RegExp(`${DEMO_ENDPOINT}/([^/]+)/get$`)).reply((config) => {
954
+ const collection = config.url?.split("/").slice(-2)[0] ?? "";
955
+ const body = handleParseBody(config);
956
+ return [
957
+ 200,
958
+ {
959
+ data:
960
+ (store[collection] ?? []).find(
961
+ (eachRecord) => eachRecord._id === body.id,
962
+ ) ?? null,
963
+ },
964
+ ];
965
+ });
966
+
967
+ return mock;
968
+ };
969
+
970
+ const RelatedRecordsDemo = () => {
971
+ const store = React.useMemo(() => renderRelatedRecords(), []);
972
+ React.useMemo(() => handleInstallRelatedMock(store), [store]);
973
+
974
+ const [typeParam, setTypeParam] = useState<"list" | "read" | "update">(
975
+ "read",
976
+ );
977
+ const [dataId, setDataId] = useState<string>("c1");
978
+ const [viewId, setViewId] = useState<string | undefined>(undefined);
979
+
980
+ return (
981
+ <div className="w-full">
982
+ <DataTemplateImpl
983
+ title="Customers"
984
+ type={typeParam}
985
+ id={dataId}
986
+ collectionId={RELATED_CUSTOMERS}
987
+ model={RELATED_CUSTOMER_SCHEMA}
988
+ localURLEndpoint=""
989
+ devSettings={demoDevSettings}
990
+ lib={{ axios, Menu, Transition, papaParser: Papa }}
991
+ edit={{ deleteBtn: true }}
992
+ space={
993
+ {
994
+ _id: RELATED_CUSTOMERS,
995
+ name: "Customers",
996
+ workspaceId: RELATED_WORKSPACE,
997
+ } as any
998
+ }
999
+ organise={{
1000
+ viewId,
1001
+ showRelatedTab: true,
1002
+ handleNavigate: (type: string, id?: string, view?: string) => {
1003
+ setTypeParam(type as any);
1004
+ setDataId(id ?? "");
1005
+ setViewId(view);
1006
+ },
1007
+ }}
1008
+ />
1009
+ </div>
1010
+ );
1011
+ };
1012
+
1013
+ export const RelatedRecords = () => (
1014
+ <div className="w-full">
1015
+ <div className="font-semibold mb-1 text-gray-900">
1016
+ Data Template — related records
1017
+ </div>
1018
+ <div className="text-sm mb-4 text-gray-600">
1019
+ The read view adds a tab for each other space whose <code>reference</code>{" "}
1020
+ or <code>listReference</code> column points at this record. A tab lists
1021
+ those rows; a row opens in place with Back to the list, and the record's
1022
+ tabs stay on screen.
1023
+ </div>
1024
+ <RelatedRecordsDemo />
1025
+ </div>
1026
+ );
@@ -123,6 +123,36 @@ grey dashboard, but on a page that is already white it shows only a stray
123
123
  outline and a shadow floating for no reason. Through `DataTemplate` the flag is
124
124
  `bareSettings.bareFlatTable`.
125
125
 
126
+ ##### Changing a column
127
+
128
+ `advSettings.onEditColumn` with `advSettings.getColumnSchema` puts a pencil
129
+ beside the hide icon, revealed on hover — not on a math column, which opens its
130
+ formula from its own pencil. It opens
131
+ the add-column panel filled in from `getColumnSchema(columnId)` (the headers do
132
+ not carry options or a reference target), and saving hands the changed column
133
+ to `onEditColumn`. The id never changes, so the column's values stay attached;
134
+ settings that belong to a type the column no longer has (options, a reference
135
+ target) are dropped.
136
+
137
+ The values already in the rows are never converted, so a column only changes to
138
+ a type its existing values are always valid as — specific to general, never back
139
+ (`handleGetEditableColumnTypes`): Phone or Email to Text or Long text, Single
140
+ select to Text or Long text, Text, Long text and Markdown among themselves,
141
+ Image to File and Images to Files. Text never becomes Phone, Email or Single
142
+ select, whose values it may not meet. Any other column keeps its type; its name,
143
+ options and reference target can still change.
144
+
145
+ Options are edited as one comma-separated line. Unchanged, they are kept exactly
146
+ as stored; changed, each option keeps its own colour and label
147
+ (`listSelectionColour`, `listSelectionLabel`), which follow it by value. A list
148
+ whose options contain a comma cannot be edited this way, and its options are
149
+ left as they are. Settings of the old type, and those read from an old reference
150
+ target, are dropped with the change. The pencil only shows where the built-in
151
+ panel can show the column — with `onAddColumn` and no `renderAddColumnElements`,
152
+ on a type the panel offers. Through `DataTemplate` (with `bareSettings.bareAddColumns`)
153
+ it arrives at the same `organise.onAddColumns(model)` as adding one, with the
154
+ column replaced in place.
155
+
126
156
  ##### Deleting a column
127
157
 
128
158
  `advSettings.onDeleteColumn` puts a bin beside the hide icon, revealed on hover.
@@ -205,6 +205,26 @@ const EditableWithAddColumn = () => {
205
205
  setColumns((previous) =>
206
206
  previous.filter((eachColumn) => eachColumn.key !== columnId),
207
207
  ),
208
+ // INFO: an app hands back its stored column definition; the story
209
+ // builds one from the header's own fields
210
+ getColumnSchema: (columnId: string) => {
211
+ const column: any = columns.find(
212
+ (eachColumn) => eachColumn.key === columnId,
213
+ );
214
+ if (!column) return undefined;
215
+ const { key, title, ...rest } = column;
216
+ return { ...rest, id: key, name: title, optional: true };
217
+ },
218
+ onEditColumn: (column: any) => {
219
+ setLastAdded(column);
220
+ setColumns((previous) =>
221
+ previous.map((eachColumn) =>
222
+ eachColumn.key === column.id
223
+ ? { ...column, key: column.id, title: column.name }
224
+ : eachColumn,
225
+ ),
226
+ );
227
+ },
208
228
  }}
209
229
  />
210
230
  {lastAdded && (
@@ -586,7 +606,7 @@ export const NewTable = () => {
586
606
 
587
607
  <StorySection
588
608
  title="Editable"
589
- description="editable.active=true makes cells clickable for inline editing. The + at the end of the header row adds a column — name it, pick a type, and the field id is derived from the name. Reference types ask for their endpoint and label; list types ask for options. The new column is editable straight away."
609
+ description="editable.active=true makes cells clickable for inline editing. The + at the end of the header row adds a column — name it, pick a type, and the field id is derived from the name. Reference types ask for their endpoint and label; list types ask for options. The new column is editable straight away. Hover a header for ✎ (onEditColumn with getColumnSchema): the same panel, filled in, changes the column's name, type, options or reference under the same id."
590
610
  >
591
611
  <EditableWithAddColumn />
592
612
  </StorySection>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blixify-ui-web-mcp",
3
- "version": "0.1.44",
3
+ "version": "0.1.47",
4
4
  "license": "MIT",
5
5
  "main": "lib/mcp/server.js",
6
6
  "bin": {