@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.
- package/CHANGELOG.md +19 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/src/form/RecordForm.d.ts.map +1 -1
- package/dist/src/form/RecordForm.js +63 -3
- package/dist/src/form/RecordForm.js.map +1 -1
- package/dist/src/table/RecordTable.d.ts +10 -0
- package/dist/src/table/RecordTable.d.ts.map +1 -1
- package/dist/src/table/RecordTable.js +126 -49
- package/dist/src/table/RecordTable.js.map +1 -1
- package/dist/src/table/ReferenceCellValue.d.ts +14 -0
- package/dist/src/table/ReferenceCellValue.d.ts.map +1 -0
- package/dist/src/table/ReferenceCellValue.js +168 -0
- package/dist/src/table/ReferenceCellValue.js.map +1 -0
- package/dist/test/recordFormStructuredFields.test.d.ts +5 -0
- package/dist/test/recordFormStructuredFields.test.d.ts.map +1 -0
- package/dist/test/recordFormStructuredFields.test.js +301 -0
- package/dist/test/recordFormStructuredFields.test.js.map +1 -0
- package/dist/test/recordTableDefaultRenderers.test.d.ts +5 -0
- package/dist/test/recordTableDefaultRenderers.test.d.ts.map +1 -0
- package/dist/test/recordTableDefaultRenderers.test.js +309 -0
- package/dist/test/recordTableDefaultRenderers.test.js.map +1 -0
- package/index.ts +1 -0
- package/package.json +5 -4
- package/src/form/RecordForm.tsx +76 -3
- package/src/table/RecordTable.tsx +154 -49
- package/src/table/ReferenceCellValue.tsx +133 -0
- package/test/recordFormStructuredFields.test.tsx +169 -0
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/db-ui",
|
|
3
|
-
"version": "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.
|
|
29
|
+
"@proteinjs/db": "^1.36.0",
|
|
30
30
|
"@proteinjs/reflection": "^1.2.0",
|
|
31
|
-
"@proteinjs/ui": "^4.
|
|
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": "
|
|
59
|
+
"gitHead": "2f13a452ede8635dbb130f70b5e5a9342c67f2f7"
|
|
59
60
|
}
|
package/src/form/RecordForm.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
494
|
-
|
|
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;
|