@proteinjs/db-ui 1.12.1 → 1.13.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.
Files changed (31) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/src/form/RecordForm.d.ts.map +1 -1
  7. package/dist/src/form/RecordForm.js +63 -3
  8. package/dist/src/form/RecordForm.js.map +1 -1
  9. package/dist/src/table/RecordTable.d.ts +10 -0
  10. package/dist/src/table/RecordTable.d.ts.map +1 -1
  11. package/dist/src/table/RecordTable.js +126 -49
  12. package/dist/src/table/RecordTable.js.map +1 -1
  13. package/dist/src/table/ReferenceCellValue.d.ts +14 -0
  14. package/dist/src/table/ReferenceCellValue.d.ts.map +1 -0
  15. package/dist/src/table/ReferenceCellValue.js +168 -0
  16. package/dist/src/table/ReferenceCellValue.js.map +1 -0
  17. package/dist/test/recordFormStructuredFields.test.d.ts +5 -0
  18. package/dist/test/recordFormStructuredFields.test.d.ts.map +1 -0
  19. package/dist/test/recordFormStructuredFields.test.js +301 -0
  20. package/dist/test/recordFormStructuredFields.test.js.map +1 -0
  21. package/dist/test/recordTableDefaultRenderers.test.d.ts +5 -0
  22. package/dist/test/recordTableDefaultRenderers.test.d.ts.map +1 -0
  23. package/dist/test/recordTableDefaultRenderers.test.js +309 -0
  24. package/dist/test/recordTableDefaultRenderers.test.js.map +1 -0
  25. package/index.ts +1 -0
  26. package/package.json +5 -4
  27. package/src/form/RecordForm.tsx +76 -3
  28. package/src/table/RecordTable.tsx +154 -49
  29. package/src/table/ReferenceCellValue.tsx +133 -0
  30. package/test/recordFormStructuredFields.test.tsx +169 -0
  31. package/test/recordTableDefaultRenderers.test.tsx +218 -0
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ var __extends = (this && this.__extends) || (function () {
3
+ var extendStatics = function (d, b) {
4
+ extendStatics = Object.setPrototypeOf ||
5
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6
+ function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
7
+ return extendStatics(d, b);
8
+ };
9
+ return function (d, b) {
10
+ if (typeof b !== "function" && b !== null)
11
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
12
+ extendStatics(d, b);
13
+ function __() { this.constructor = d; }
14
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
15
+ };
16
+ })();
17
+ var __assign = (this && this.__assign) || function () {
18
+ __assign = Object.assign || function(t) {
19
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
20
+ s = arguments[i];
21
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
22
+ t[p] = s[p];
23
+ }
24
+ return t;
25
+ };
26
+ return __assign.apply(this, arguments);
27
+ };
28
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
29
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
30
+ return new (P || (P = Promise))(function (resolve, reject) {
31
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
32
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
33
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
34
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
35
+ });
36
+ };
37
+ var __generator = (this && this.__generator) || function (thisArg, body) {
38
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
39
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
40
+ function verb(n) { return function (v) { return step([n, v]); }; }
41
+ function step(op) {
42
+ if (f) throw new TypeError("Generator is already executing.");
43
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
44
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
45
+ if (y = 0, t) op = [op[0] & 2, t.value];
46
+ switch (op[0]) {
47
+ case 0: case 1: t = op; break;
48
+ case 4: _.label++; return { value: op[1], done: false };
49
+ case 5: _.label++; y = op[1]; op = [0]; continue;
50
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
51
+ default:
52
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
53
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
54
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
55
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
56
+ if (t[2]) _.ops.pop();
57
+ _.trys.pop(); continue;
58
+ }
59
+ op = body.call(thisArg, _);
60
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
61
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
62
+ }
63
+ };
64
+ var __importDefault = (this && this.__importDefault) || function (mod) {
65
+ return (mod && mod.__esModule) ? mod : { "default": mod };
66
+ };
67
+ Object.defineProperty(exports, "__esModule", { value: true });
68
+ /**
69
+ * @jest-environment jsdom
70
+ *
71
+ * RecordTable's polished defaults (the admin-surface polish rev). Contracts as OUTCOMES:
72
+ * 1. Column pick is tiered meaningful-data, not schema order: identity strings → status-like →
73
+ * booleans → references → rest, hidden columns never picked, long-text demoted; capped at
74
+ * five + created/updated.
75
+ * 2. Booleans render the check/dash grammar (no ✅/❌ emoji).
76
+ * 3. DateTime values render humanized with the precise absolute on the title.
77
+ * 4. Status-like short strings render as the quiet chip.
78
+ * 5. References render as a LINK to the referenced record's form — short id immediately,
79
+ * enriched to the record's name once resolution lands.
80
+ * 6. Numeric columns right-align.
81
+ */
82
+ var react_1 = __importDefault(require("react"));
83
+ var moment_1 = __importDefault(require("moment"));
84
+ var client_1 = require("react-dom/client");
85
+ var test_utils_1 = require("react-dom/test-utils");
86
+ var react_router_dom_1 = require("react-router-dom");
87
+ var react_query_1 = require("react-query");
88
+ var ui_1 = require("@proteinjs/ui");
89
+ var db_1 = require("@proteinjs/db");
90
+ var RecordTable_1 = require("../src/table/RecordTable");
91
+ var ReferenceCellValue_1 = require("../src/table/ReferenceCellValue");
92
+ // ReferenceCellValue resolves through Reference.get(); the fake resolves user-9 to a named
93
+ // record so the enrichment path is observable without a db.
94
+ jest.mock('@proteinjs/db', function () {
95
+ var actual = jest.requireActual('@proteinjs/db');
96
+ var FakeReference = /** @class */ (function () {
97
+ function FakeReference(_table, _id) {
98
+ this._table = _table;
99
+ this._id = _id;
100
+ }
101
+ FakeReference.prototype.get = function () {
102
+ return __awaiter(this, void 0, void 0, function () {
103
+ return __generator(this, function (_a) {
104
+ if (this._id === 'user-9') {
105
+ return [2 /*return*/, { id: 'user-9', name: 'Brent Test' }];
106
+ }
107
+ throw new Error('not visible');
108
+ });
109
+ });
110
+ };
111
+ return FakeReference;
112
+ }());
113
+ return __assign(__assign({}, actual), { Reference: FakeReference });
114
+ });
115
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
116
+ var StubIntersectionObserver = /** @class */ (function () {
117
+ function StubIntersectionObserver() {
118
+ }
119
+ StubIntersectionObserver.prototype.observe = function () { };
120
+ StubIntersectionObserver.prototype.unobserve = function () { };
121
+ StubIntersectionObserver.prototype.disconnect = function () { };
122
+ return StubIntersectionObserver;
123
+ }());
124
+ globalThis.IntersectionObserver = StubIntersectionObserver;
125
+ var MemberTable = /** @class */ (function (_super) {
126
+ __extends(MemberTable, _super);
127
+ function MemberTable() {
128
+ var _this = _super !== null && _super.apply(this, arguments) || this;
129
+ _this.name = 'admin_test_member';
130
+ _this.columns = (0, db_1.withRecordColumns)({
131
+ // schema order is deliberately adversarial: the tiers must reorder it
132
+ secret: new db_1.StringColumn('secret', { ui: { hidden: true } }),
133
+ notes: new db_1.StringColumn('notes', {}, 4000),
134
+ verified: new db_1.BooleanColumn('verified'),
135
+ invitedBy: new db_1.ReferenceColumn('invited_by', 'user', false),
136
+ email: new db_1.StringColumn('email'),
137
+ status: new db_1.StringColumn('status'),
138
+ loginCount: new db_1.IntegerColumn('login_count'),
139
+ lastSeen: new db_1.DateTimeColumn('last_seen'),
140
+ });
141
+ return _this;
142
+ }
143
+ return MemberTable;
144
+ }(db_1.Table));
145
+ var lastSeen = (0, moment_1.default)('2026-05-04 14:30:00', 'YYYY-MM-DD HH:mm:ss');
146
+ var row = {
147
+ id: 'member-1',
148
+ secret: 'never-shown',
149
+ notes: 'a long note',
150
+ verified: true,
151
+ invitedBy: { _table: 'user', _id: 'user-9' },
152
+ email: 'brent+test@n3xa.io',
153
+ status: 'active',
154
+ loginCount: 12,
155
+ lastSeen: lastSeen,
156
+ created: (0, moment_1.default)('2026-01-02T03:04:05.000Z'),
157
+ updated: (0, moment_1.default)('2026-02-03T04:05:06.000Z'),
158
+ };
159
+ describe('defaultRecordTableColumns (meaningful-data tiers)', function () {
160
+ it('picks identity → status → booleans → references, skips hidden, demotes long text, caps at five + created/updated', function () {
161
+ var columns = (0, RecordTable_1.defaultRecordTableColumns)(new MemberTable());
162
+ expect(columns).toEqual(['email', 'status', 'verified', 'invitedBy', 'loginCount', 'created', 'updated']);
163
+ });
164
+ it('compound identity names promote too (userEmail leads a session-shaped table)', function () {
165
+ var SessionishTable = /** @class */ (function (_super) {
166
+ __extends(SessionishTable, _super);
167
+ function SessionishTable() {
168
+ var _this = _super !== null && _super.apply(this, arguments) || this;
169
+ _this.name = 'admin_test_sessionish';
170
+ _this.columns = (0, db_1.withRecordColumns)({
171
+ sessionId: new db_1.StringColumn('session_id'),
172
+ session: new db_1.StringColumn('serialized_session', {}, 4000),
173
+ userEmail: new db_1.StringColumn('user_email'),
174
+ });
175
+ return _this;
176
+ }
177
+ return SessionishTable;
178
+ }(db_1.Table));
179
+ var columns = (0, RecordTable_1.defaultRecordTableColumns)(new SessionishTable());
180
+ expect(columns[0]).toBe('userEmail');
181
+ // the long-text blob demotes behind the short columns
182
+ expect(columns.indexOf('session')).toBeGreaterThan(columns.indexOf('sessionId'));
183
+ });
184
+ it('name leads when the table has one', function () {
185
+ var NamedTable = /** @class */ (function (_super) {
186
+ __extends(NamedTable, _super);
187
+ function NamedTable() {
188
+ var _this = _super !== null && _super.apply(this, arguments) || this;
189
+ _this.name = 'admin_test_named';
190
+ _this.columns = (0, db_1.withRecordColumns)({
191
+ email: new db_1.StringColumn('email'),
192
+ name: new db_1.StringColumn('name'),
193
+ });
194
+ return _this;
195
+ }
196
+ return NamedTable;
197
+ }(db_1.Table));
198
+ var columns = (0, RecordTable_1.defaultRecordTableColumns)(new NamedTable());
199
+ expect(columns[0]).toBe('name');
200
+ expect(columns).toContain('email');
201
+ });
202
+ });
203
+ describe('RecordTable default renderers', function () {
204
+ var container;
205
+ var root;
206
+ var client;
207
+ beforeEach(function () {
208
+ (0, ReferenceCellValue_1.clearReferenceNameCache)();
209
+ container = document.createElement('div');
210
+ document.body.appendChild(container);
211
+ root = (0, client_1.createRoot)(container);
212
+ client = new react_query_1.QueryClient({ defaultOptions: { queries: { retry: false, cacheTime: 0 } } });
213
+ });
214
+ afterEach(function () {
215
+ (0, test_utils_1.act)(function () { return root.unmount(); });
216
+ container.remove();
217
+ });
218
+ var mount = function (columns) { return __awaiter(void 0, void 0, void 0, function () {
219
+ return __generator(this, function (_a) {
220
+ switch (_a.label) {
221
+ case 0: return [4 /*yield*/, (0, test_utils_1.act)(function () { return __awaiter(void 0, void 0, void 0, function () {
222
+ return __generator(this, function (_a) {
223
+ root.render(react_1.default.createElement(react_query_1.QueryClientProvider, { client: client },
224
+ react_1.default.createElement(react_router_dom_1.MemoryRouter, null,
225
+ react_1.default.createElement(RecordTable_1.RecordTable, __assign({ table: new MemberTable(), tableLoader: new ui_1.StaticTableLoader([row], { dataKey: "default-renderers-".concat(Math.random()), dataQueryKey: 'all' }), hideButtons: true }, (columns ? { columns: columns } : {}))))));
226
+ return [2 /*return*/];
227
+ });
228
+ }); })];
229
+ case 1:
230
+ _a.sent();
231
+ return [4 /*yield*/, (0, test_utils_1.act)(function () { return __awaiter(void 0, void 0, void 0, function () {
232
+ return __generator(this, function (_a) {
233
+ switch (_a.label) {
234
+ case 0: return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 0); })];
235
+ case 1:
236
+ _a.sent();
237
+ return [2 /*return*/];
238
+ }
239
+ });
240
+ }); })];
241
+ case 2:
242
+ _a.sent();
243
+ return [2 /*return*/];
244
+ }
245
+ });
246
+ }); };
247
+ var cellFor = function (index) { return container.querySelectorAll('tbody td')[index]; };
248
+ it('booleans render check/dash, datetimes humanized with absolute title, status as the chip', function () { return __awaiter(void 0, void 0, void 0, function () {
249
+ var titled, chip;
250
+ return __generator(this, function (_a) {
251
+ switch (_a.label) {
252
+ case 0: return [4 /*yield*/, mount(['verified', 'lastSeen', 'status'])];
253
+ case 1:
254
+ _a.sent();
255
+ expect(cellFor(0).querySelector('svg')).not.toBeNull();
256
+ expect(cellFor(0).textContent).not.toContain('✅');
257
+ expect(cellFor(1).textContent).toBe('May 4, 2026');
258
+ titled = cellFor(1).querySelector('[title]');
259
+ expect(titled.getAttribute('title')).toBe(lastSeen.format('ddd, MMM D YYYY, h:mm:ss A'));
260
+ chip = cellFor(2);
261
+ expect(chip.textContent).toBe('active');
262
+ // the chip carries its tone dot (an inner span before the text)
263
+ expect(chip.querySelectorAll('span span').length).toBeGreaterThan(0);
264
+ return [2 /*return*/];
265
+ }
266
+ });
267
+ }); });
268
+ it('references render as a link to the record form — short id first, the name once resolved', function () { return __awaiter(void 0, void 0, void 0, function () {
269
+ var link;
270
+ return __generator(this, function (_a) {
271
+ switch (_a.label) {
272
+ case 0: return [4 /*yield*/, mount(['invitedBy'])];
273
+ case 1:
274
+ _a.sent();
275
+ link = cellFor(0).querySelector('a');
276
+ expect(link).not.toBeNull();
277
+ expect(link.getAttribute('href')).toBe('/record/form?table=user&record=user-9');
278
+ // resolution lands: the short id enriches to the referenced record's name
279
+ return [4 /*yield*/, (0, test_utils_1.act)(function () { return __awaiter(void 0, void 0, void 0, function () {
280
+ return __generator(this, function (_a) {
281
+ switch (_a.label) {
282
+ case 0: return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 0); })];
283
+ case 1:
284
+ _a.sent();
285
+ return [2 /*return*/];
286
+ }
287
+ });
288
+ }); })];
289
+ case 2:
290
+ // resolution lands: the short id enriches to the referenced record's name
291
+ _a.sent();
292
+ expect(cellFor(0).textContent).toBe('Brent Test');
293
+ return [2 /*return*/];
294
+ }
295
+ });
296
+ }); });
297
+ it('numeric columns right-align', function () { return __awaiter(void 0, void 0, void 0, function () {
298
+ return __generator(this, function (_a) {
299
+ switch (_a.label) {
300
+ case 0: return [4 /*yield*/, mount(['loginCount'])];
301
+ case 1:
302
+ _a.sent();
303
+ expect(cellFor(0).className).toContain('MuiTableCell-alignRight');
304
+ return [2 /*return*/];
305
+ }
306
+ });
307
+ }); });
308
+ });
309
+ //# sourceMappingURL=recordTableDefaultRenderers.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recordTableDefaultRenderers.test.js","sourceRoot":"","sources":["../../test/recordTableDefaultRenderers.test.tsx"],"names":[],"mappings":"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"}
package/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './src/table/RecordTable';
2
+ export * from './src/table/ReferenceCellValue';
2
3
  export * from './src/table/QueryTableLoader';
3
4
  export * from './src/table/QueryCursorLoader';
4
5
  export * from './src/form/RecordForm';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteinjs/db-ui",
3
- "version": "1.12.1",
3
+ "version": "1.13.0",
4
4
  "description": "Db ui components",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -26,12 +26,13 @@
26
26
  "@emotion/styled": "11.11.0",
27
27
  "@mui/icons-material": "5.14.11",
28
28
  "@mui/material": "5.14.11",
29
- "@proteinjs/db": "^1.35.1",
29
+ "@proteinjs/db": "^1.36.0",
30
30
  "@proteinjs/reflection": "^1.2.0",
31
- "@proteinjs/ui": "^4.10.0",
31
+ "@proteinjs/ui": "^4.11.0",
32
32
  "@proteinjs/util": "^1.6.1",
33
33
  "moment": "2.29.4",
34
34
  "react": "18.2.0",
35
+ "react-router-dom": "6.16.0",
35
36
  "string": "3.3.3",
36
37
  "uuid": "8.3.0"
37
38
  },
@@ -55,5 +56,5 @@
55
56
  },
56
57
  "main": "./dist/generated/index.js",
57
58
  "types": "./dist/generated/index.d.ts",
58
- "gitHead": "ab6c2ca7ce824c1c1f22fdc7853eb93cb5334172"
59
+ "gitHead": "2f13a452ede8635dbb130f70b5e5a9342c67f2f7"
59
60
  }
@@ -15,6 +15,9 @@ import {
15
15
  ReferenceArray,
16
16
  ReferenceColumn,
17
17
  ReferenceArrayColumn,
18
+ StringColumn,
19
+ ObjectColumn,
20
+ ArrayColumn,
18
21
  } from '@proteinjs/db';
19
22
  import { recordTableLink } from '../pages/RecordTablePage';
20
23
  import { recordFormLink } from '../pages/RecordFormPage';
@@ -206,6 +209,20 @@ export function RecordForm<T extends Record>({ table, record }: RecordFormProps<
206
209
  return recordFormCustomization.getFieldRenderer(columnPropertyName, record);
207
210
  }
208
211
 
212
+ /**
213
+ * A long-text column (maxLength past the 255 default) gets a multiline input — a
214
+ * single-line input truncates exactly the values (descriptions, failure messages,
215
+ * serialized payloads) an admin opens the form to read.
216
+ */
217
+ function isLongTextColumn(column: Column<T, any>) {
218
+ if (!isInstanceOf(column, StringColumn)) {
219
+ return false;
220
+ }
221
+
222
+ const { maxLength } = column as unknown as StringColumn;
223
+ return maxLength === 'MAX' || maxLength > 255;
224
+ }
225
+
209
226
  /** Pick the field control that tells the truth about the column's type. */
210
227
  function createField(columnPropertyName: string, column: Column<T, any>) {
211
228
  const name = columnPropertyName;
@@ -235,6 +252,8 @@ export function RecordForm<T extends Record>({ table, record }: RecordFormProps<
235
252
  return dateField({ name, label, includeTime: true });
236
253
  }
237
254
 
255
+ // Reference columns keep their id controls — they must sit BEFORE the structured/long-text
256
+ // branches (ReferenceArrayColumn descends from ObjectColumn, whose storage is a MAX string).
238
257
  if (isInstanceOf(column, ReferenceColumn)) {
239
258
  const { referenceTable } = column as unknown as ReferenceColumn<any>;
240
259
  return textField({
@@ -258,6 +277,16 @@ export function RecordForm<T extends Record>({ table, record }: RecordFormProps<
258
277
  });
259
278
  }
260
279
 
280
+ // Structured values edit as pretty-printed JSON in a mono multiline (loaded/saved through
281
+ // onLoad/getFieldValue).
282
+ if (isInstanceOf(column, ObjectColumn) || isInstanceOf(column, ArrayColumn)) {
283
+ return textField({ name, label, description: 'JSON', multiline: true, monospace: true });
284
+ }
285
+
286
+ if (isLongTextColumn(column)) {
287
+ return textField({ name, label, multiline: true });
288
+ }
289
+
261
290
  return textField({ name, label });
262
291
  }
263
292
 
@@ -298,9 +327,26 @@ export function RecordForm<T extends Record>({ table, record }: RecordFormProps<
298
327
  const layoutColumns = Object.entries(columns).length > 6 ? 2 : 1;
299
328
  if (layoutColumns > 1) {
300
329
  const layout: (keyof T)[][] = [];
330
+ let lastRowIsSolo = false;
301
331
  for (const columnPropertyName in columns) {
302
- if (layout.length == 0 || layout[layout.length - 1].length >= layoutColumns) {
332
+ const column = columns[columnPropertyName];
333
+ // Multiline fields (long text / JSON) take a full-width row of their own — half a
334
+ // two-column row squeezes exactly the fields that hold the most text.
335
+ const isMultiline =
336
+ !isReadonlyField(columnPropertyName, column) &&
337
+ !getFieldRenderer(columnPropertyName) &&
338
+ (isLongTextColumn(column) ||
339
+ ((isInstanceOf(column, ObjectColumn) || isInstanceOf(column, ArrayColumn)) &&
340
+ !isInstanceOf(column, ReferenceArrayColumn)));
341
+ if (isMultiline) {
342
+ layout.push([columnPropertyName as keyof T]);
343
+ lastRowIsSolo = true;
344
+ continue;
345
+ }
346
+
347
+ if (layout.length == 0 || lastRowIsSolo || layout[layout.length - 1].length >= layoutColumns) {
303
348
  layout.push([]);
349
+ lastRowIsSolo = false;
304
350
  }
305
351
 
306
352
  layout[layout.length - 1].push(columnPropertyName as keyof T);
@@ -346,6 +392,23 @@ export function RecordForm<T extends Record>({ table, record }: RecordFormProps<
346
392
  return parseDateInputValue(fieldValue);
347
393
  }
348
394
 
395
+ // Structured columns round-trip through the JSON the multiline field presents; a paste
396
+ // that isn't JSON fails the save with a message naming the field, not a driver error.
397
+ if (
398
+ (isInstanceOf(column, ObjectColumn) || isInstanceOf(column, ArrayColumn)) &&
399
+ !isInstanceOf(column, ReferenceArrayColumn)
400
+ ) {
401
+ if (typeof fieldValue !== 'string' || !fieldValue.trim()) {
402
+ return null;
403
+ }
404
+
405
+ try {
406
+ return JSON.parse(fieldValue);
407
+ } catch {
408
+ throw new Error(`${StringUtil.humanizeCamel(columnPropertyName)} must be valid JSON`);
409
+ }
410
+ }
411
+
349
412
  return fieldValue;
350
413
  }
351
414
 
@@ -490,8 +553,18 @@ export function RecordForm<T extends Record>({ table, record }: RecordFormProps<
490
553
  // The native date input takes its own value format
491
554
  fieldValue = moment(fieldValue).format('YYYY-MM-DD');
492
555
  } else if (moment.isMoment(fieldValue)) {
493
- // Readonly timestamps (created/updated/DateTimeColumn) display human-formatted, copyable
494
- fieldValue = fieldValue.format('ddd, MMM Do YY, h:mm:ss a');
556
+ // Readonly timestamps (created/updated/DateTimeColumn) display compact and copyable,
557
+ // with the relative read ('2 hours ago') as the field's helper line.
558
+ field.description = fieldValue.fromNow();
559
+ fieldValue = fieldValue.format('MMM D, YYYY, h:mm A');
560
+ } else if (
561
+ (isInstanceOf(column, ObjectColumn) || isInstanceOf(column, ArrayColumn)) &&
562
+ !isInstanceOf(column, ReferenceArrayColumn) &&
563
+ fieldValue != null &&
564
+ typeof fieldValue !== 'string'
565
+ ) {
566
+ // Structured values present as pretty-printed JSON in their multiline field
567
+ fieldValue = JSON.stringify(fieldValue, null, 2);
495
568
  }
496
569
 
497
570
  field.value = fieldValue;