tempest-express-sdk 0.26.0 → 0.28.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/README.md +1 -1
- package/dist/{chunk-GLZYNX63.js → chunk-3NS5KVHT.js} +3 -3
- package/dist/{chunk-GLZYNX63.js.map → chunk-3NS5KVHT.js.map} +1 -1
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +837 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +301 -4
- package/dist/index.d.ts +301 -4
- package/dist/index.js +832 -16
- package/dist/index.js.map +1 -1
- package/package.json +7 -1
package/dist/index.cjs
CHANGED
|
@@ -9461,6 +9461,23 @@ function partitionTotal(partition) {
|
|
|
9461
9461
|
return partition.segments.reduce((total, segment) => total + segment.value, 0);
|
|
9462
9462
|
}
|
|
9463
9463
|
|
|
9464
|
+
// src/admin/inlines.ts
|
|
9465
|
+
function adminInline(options) {
|
|
9466
|
+
const slug = options.model.tablename;
|
|
9467
|
+
if (typeof slug !== "string" || slug === "") {
|
|
9468
|
+
throw new Error("adminInline requires a concrete model with a tablename");
|
|
9469
|
+
}
|
|
9470
|
+
return {
|
|
9471
|
+
model: options.model,
|
|
9472
|
+
slug,
|
|
9473
|
+
fkField: options.fkField,
|
|
9474
|
+
listDisplay: options.listDisplay === void 0 ? null : [...options.listDisplay],
|
|
9475
|
+
label: options.label ?? null,
|
|
9476
|
+
editable: options.editable ?? false,
|
|
9477
|
+
canDelete: options.canDelete ?? false
|
|
9478
|
+
};
|
|
9479
|
+
}
|
|
9480
|
+
|
|
9464
9481
|
// src/admin/lenses.ts
|
|
9465
9482
|
function slugify(name) {
|
|
9466
9483
|
return name.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "lens";
|
|
@@ -9498,6 +9515,85 @@ function adminAction(options, handler) {
|
|
|
9498
9515
|
};
|
|
9499
9516
|
}
|
|
9500
9517
|
|
|
9518
|
+
// src/admin/multipart.ts
|
|
9519
|
+
var BUSBOY_HINT = "The admin's upload and CSV-import screens need the optional peer `busboy`. Install it with: npm install busboy";
|
|
9520
|
+
async function loadBusboy() {
|
|
9521
|
+
try {
|
|
9522
|
+
const module = await import('busboy');
|
|
9523
|
+
return module.default ?? module;
|
|
9524
|
+
} catch {
|
|
9525
|
+
throw new Error(BUSBOY_HINT);
|
|
9526
|
+
}
|
|
9527
|
+
}
|
|
9528
|
+
var MultipartLimitError = class extends Error {
|
|
9529
|
+
/**
|
|
9530
|
+
* @param message - The operator-facing explanation.
|
|
9531
|
+
*/
|
|
9532
|
+
constructor(message) {
|
|
9533
|
+
super(message);
|
|
9534
|
+
this.name = "MultipartLimitError";
|
|
9535
|
+
}
|
|
9536
|
+
};
|
|
9537
|
+
async function parseMultipart(req, options = {}) {
|
|
9538
|
+
const maxFileBytes = options.maxFileBytes ?? 10 * 1024 * 1024;
|
|
9539
|
+
const maxFiles = options.maxFiles ?? 10;
|
|
9540
|
+
const busboy = await loadBusboy();
|
|
9541
|
+
return await new Promise((resolve, reject) => {
|
|
9542
|
+
const fields = {};
|
|
9543
|
+
const files = [];
|
|
9544
|
+
let settled = false;
|
|
9545
|
+
const fail = (error) => {
|
|
9546
|
+
if (settled) return;
|
|
9547
|
+
settled = true;
|
|
9548
|
+
reject(error);
|
|
9549
|
+
};
|
|
9550
|
+
const parser = busboy({
|
|
9551
|
+
headers: req.headers,
|
|
9552
|
+
limits: { fileSize: maxFileBytes, files: maxFiles }
|
|
9553
|
+
});
|
|
9554
|
+
parser.on("field", ((name, value) => {
|
|
9555
|
+
fields[name] = value;
|
|
9556
|
+
}));
|
|
9557
|
+
parser.on("file", ((name, stream, info) => {
|
|
9558
|
+
const chunks = [];
|
|
9559
|
+
stream.on("data", ((chunk) => chunks.push(chunk)));
|
|
9560
|
+
stream.on("limit", () => {
|
|
9561
|
+
fail(
|
|
9562
|
+
new MultipartLimitError(
|
|
9563
|
+
`The file is larger than the ${Math.floor(maxFileBytes / 1024 / 1024)} MB limit.`
|
|
9564
|
+
)
|
|
9565
|
+
);
|
|
9566
|
+
});
|
|
9567
|
+
stream.on("end", () => {
|
|
9568
|
+
const data = Buffer.concat(chunks);
|
|
9569
|
+
const filename = (info.filename ?? "").split(/[/\\]/).pop() ?? "";
|
|
9570
|
+
if (filename === "" || data.length === 0) return;
|
|
9571
|
+
files.push({
|
|
9572
|
+
field: name,
|
|
9573
|
+
filename,
|
|
9574
|
+
contentType: info.mimeType ?? "application/octet-stream",
|
|
9575
|
+
data
|
|
9576
|
+
});
|
|
9577
|
+
});
|
|
9578
|
+
}));
|
|
9579
|
+
parser.on("filesLimit", () => {
|
|
9580
|
+
fail(new MultipartLimitError(`At most ${maxFiles} files can be uploaded at once.`));
|
|
9581
|
+
});
|
|
9582
|
+
parser.on("error", ((error) => {
|
|
9583
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
9584
|
+
}));
|
|
9585
|
+
parser.on("close", () => {
|
|
9586
|
+
if (settled) return;
|
|
9587
|
+
settled = true;
|
|
9588
|
+
resolve({ fields, files });
|
|
9589
|
+
});
|
|
9590
|
+
req.pipe(parser);
|
|
9591
|
+
});
|
|
9592
|
+
}
|
|
9593
|
+
function isMultipart(req) {
|
|
9594
|
+
return (req.header("content-type") ?? "").toLowerCase().startsWith("multipart/form-data");
|
|
9595
|
+
}
|
|
9596
|
+
|
|
9501
9597
|
// src/admin/columns.ts
|
|
9502
9598
|
function humanizeField(name) {
|
|
9503
9599
|
return name.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim().split(/\s+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
@@ -9591,6 +9687,8 @@ var NEVER_EDITABLE = [
|
|
|
9591
9687
|
"id",
|
|
9592
9688
|
"createdAt",
|
|
9593
9689
|
"updatedAt",
|
|
9690
|
+
"createdBy",
|
|
9691
|
+
"updatedBy",
|
|
9594
9692
|
"hashedPassword"
|
|
9595
9693
|
];
|
|
9596
9694
|
var NEVER_LISTED = ["hashedPassword"];
|
|
@@ -9627,6 +9725,16 @@ var AdminModel = class {
|
|
|
9627
9725
|
auditModel;
|
|
9628
9726
|
/** Saved list-view presets, in declaration order. */
|
|
9629
9727
|
lenses;
|
|
9728
|
+
/** Columns rendered as file inputs. */
|
|
9729
|
+
uploadFields;
|
|
9730
|
+
/** Backend persisting uploaded files, or `null`. */
|
|
9731
|
+
uploadStorage;
|
|
9732
|
+
/** Whether the CSV import page is exposed. */
|
|
9733
|
+
canImport;
|
|
9734
|
+
/** Foreign-key columns rendered as a typed search box. */
|
|
9735
|
+
autocompleteFields;
|
|
9736
|
+
/** Related child models listed on the detail view. */
|
|
9737
|
+
inlines;
|
|
9630
9738
|
actions = /* @__PURE__ */ new Map();
|
|
9631
9739
|
slugOverride;
|
|
9632
9740
|
listDisplayOverride;
|
|
@@ -9654,6 +9762,16 @@ var AdminModel = class {
|
|
|
9654
9762
|
this.canDelete = options.canDelete ?? true;
|
|
9655
9763
|
this.auditModel = options.auditModel ?? null;
|
|
9656
9764
|
this.lenses = [...options.lenses ?? []];
|
|
9765
|
+
this.uploadFields = [...options.uploadFields ?? []];
|
|
9766
|
+
this.uploadStorage = options.uploadStorage ?? null;
|
|
9767
|
+
this.canImport = options.canImport ?? false;
|
|
9768
|
+
this.autocompleteFields = [...options.autocompleteFields ?? []];
|
|
9769
|
+
this.inlines = [...options.inlines ?? []];
|
|
9770
|
+
if (this.uploadFields.length > 0 && this.uploadStorage === null) {
|
|
9771
|
+
throw new Error(
|
|
9772
|
+
`AdminModel(${this.model.tablename}).uploadFields requires an uploadStorage (e.g. LocalUploadStorage / S3UploadStorage) \u2014 without one there is nowhere to write the file.`
|
|
9773
|
+
);
|
|
9774
|
+
}
|
|
9657
9775
|
for (const action of options.actions ?? []) {
|
|
9658
9776
|
if (this.actions.has(action.name)) {
|
|
9659
9777
|
throw new Error(
|
|
@@ -9668,7 +9786,9 @@ var AdminModel = class {
|
|
|
9668
9786
|
["listFilter", this.listFilter],
|
|
9669
9787
|
["searchFields", this.searchFields],
|
|
9670
9788
|
["readonlyFields", this.readonlyFields],
|
|
9671
|
-
["identityField", [this.identityField]]
|
|
9789
|
+
["identityField", [this.identityField]],
|
|
9790
|
+
["uploadFields", this.uploadFields],
|
|
9791
|
+
["autocompleteFields", this.autocompleteFields]
|
|
9672
9792
|
]) {
|
|
9673
9793
|
for (const name of names) {
|
|
9674
9794
|
if (!known.has(name)) {
|
|
@@ -9850,11 +9970,15 @@ function buildFormFields(admin, options = {}) {
|
|
|
9850
9970
|
const values = options.values ?? {};
|
|
9851
9971
|
const errors = options.errors ?? {};
|
|
9852
9972
|
const foreignKeys = options.foreignKeyOptions ?? {};
|
|
9973
|
+
const autocompleteUrls = options.autocompleteUrls ?? {};
|
|
9974
|
+
const autocompleteLabels = options.autocompleteLabels ?? {};
|
|
9975
|
+
const uploads = new Set(admin.uploadFields);
|
|
9853
9976
|
return admin.editableFieldNames().flatMap((name) => {
|
|
9854
9977
|
const column6 = columns[name];
|
|
9855
9978
|
if (column6 === void 0) return [];
|
|
9856
9979
|
const related = foreignKeys[name];
|
|
9857
|
-
const
|
|
9980
|
+
const autocompleteUrl = autocompleteUrls[name];
|
|
9981
|
+
const spec = uploads.has(name) ? { widget: "file", step: null, options: [] } : autocompleteUrl !== void 0 ? { widget: "autocomplete", step: null, options: [] } : related === void 0 ? widgetForColumn(column6) : { widget: "select", step: null, options: related };
|
|
9858
9982
|
const raw = name in values ? values[name] : literalDefault(column6);
|
|
9859
9983
|
return [
|
|
9860
9984
|
{
|
|
@@ -9866,7 +9990,9 @@ function buildFormFields(admin, options = {}) {
|
|
|
9866
9990
|
checked: spec.widget === "checkbox" && toBoolean(raw),
|
|
9867
9991
|
step: spec.step,
|
|
9868
9992
|
options: spec.options,
|
|
9869
|
-
error: errors[name] ?? null
|
|
9993
|
+
error: errors[name] ?? null,
|
|
9994
|
+
autocompleteUrl: autocompleteUrl ?? null,
|
|
9995
|
+
displayLabel: autocompleteLabels[name] ?? ""
|
|
9870
9996
|
}
|
|
9871
9997
|
];
|
|
9872
9998
|
});
|
|
@@ -9914,13 +10040,17 @@ function coerceValue(column6, widget, raw) {
|
|
|
9914
10040
|
return raw;
|
|
9915
10041
|
}
|
|
9916
10042
|
}
|
|
9917
|
-
function parseFormBody(admin, body) {
|
|
10043
|
+
function parseFormBody(admin, body, options = {}) {
|
|
9918
10044
|
const columns = adminColumns(admin.model);
|
|
9919
10045
|
const data = {};
|
|
9920
10046
|
const errors = {};
|
|
10047
|
+
const uploads = options.uploadsAsText === true ? /* @__PURE__ */ new Set() : new Set(admin.uploadFields);
|
|
10048
|
+
const only = options.only === void 0 ? null : new Set(options.only);
|
|
9921
10049
|
for (const name of admin.editableFieldNames()) {
|
|
10050
|
+
if (only !== null && !only.has(name)) continue;
|
|
9922
10051
|
const column6 = columns[name];
|
|
9923
10052
|
if (column6 === void 0) continue;
|
|
10053
|
+
if (uploads.has(name)) continue;
|
|
9924
10054
|
const { widget } = widgetForColumn(column6);
|
|
9925
10055
|
if (widget === "checkbox") {
|
|
9926
10056
|
data[name] = toBoolean(body[name]);
|
|
@@ -12206,6 +12336,7 @@ function renderListPage(context, view) {
|
|
|
12206
12336
|
</form>
|
|
12207
12337
|
<div class="tempest-admin-list__actions">
|
|
12208
12338
|
${view.newUrl !== null ? `<a class="tempest-admin-list__new" href="${escapeHtml(view.newUrl)}">+ New</a>` : ""}
|
|
12339
|
+
${view.importUrl !== null ? `<a href="${escapeHtml(view.importUrl)}">Import CSV</a>` : ""}
|
|
12209
12340
|
<a href="${escapeHtml(view.exportCsvUrl)}">Export CSV</a>
|
|
12210
12341
|
<a href="${escapeHtml(view.exportJsonUrl)}">Export JSON</a>
|
|
12211
12342
|
</div>
|
|
@@ -12250,10 +12381,97 @@ function renderDetailPage(context, view) {
|
|
|
12250
12381
|
</div>
|
|
12251
12382
|
</header>
|
|
12252
12383
|
<dl class="tempest-admin-detail__fields">${fields}</dl>
|
|
12384
|
+
${view.inlines.map((inline) => renderInline(inline, context.session?.csrfToken ?? "", view.inlineError)).join("")}
|
|
12253
12385
|
${auditPanel}
|
|
12254
12386
|
</section>`;
|
|
12255
12387
|
return renderLayout(context, `${view.title} \xB7 ${view.identity}`, body);
|
|
12256
12388
|
}
|
|
12389
|
+
function renderInlineCell(field) {
|
|
12390
|
+
const required = field.required ? " required" : "";
|
|
12391
|
+
const name = escapeHtml(field.name);
|
|
12392
|
+
const value = escapeHtml(field.value);
|
|
12393
|
+
let control;
|
|
12394
|
+
switch (field.widget) {
|
|
12395
|
+
case "checkbox":
|
|
12396
|
+
control = `<input type="checkbox" name="${name}" value="true"${field.checked ? " checked" : ""}>`;
|
|
12397
|
+
break;
|
|
12398
|
+
case "textarea":
|
|
12399
|
+
case "json":
|
|
12400
|
+
control = `<textarea name="${name}" rows="2"${required}>${value}</textarea>`;
|
|
12401
|
+
break;
|
|
12402
|
+
case "select": {
|
|
12403
|
+
const blank = field.required ? "" : '<option value="">\u2014 none \u2014</option>';
|
|
12404
|
+
const options = field.options.map(
|
|
12405
|
+
(option) => `<option value="${escapeHtml(option.value)}"${option.value === field.value ? " selected" : ""}>${escapeHtml(option.label)}</option>`
|
|
12406
|
+
).join("");
|
|
12407
|
+
control = `<select name="${name}"${required}>${blank}${options}</select>`;
|
|
12408
|
+
break;
|
|
12409
|
+
}
|
|
12410
|
+
case "number":
|
|
12411
|
+
control = `<input type="number" name="${name}" value="${value}"${field.step !== null ? ` step="${escapeHtml(field.step)}"` : ""}${required}>`;
|
|
12412
|
+
break;
|
|
12413
|
+
case "datetime":
|
|
12414
|
+
control = `<input type="datetime-local" name="${name}" value="${value}"${required}>`;
|
|
12415
|
+
break;
|
|
12416
|
+
case "date":
|
|
12417
|
+
control = `<input type="date" name="${name}" value="${value}"${required}>`;
|
|
12418
|
+
break;
|
|
12419
|
+
case "time":
|
|
12420
|
+
control = `<input type="time" name="${name}" value="${value}"${required}>`;
|
|
12421
|
+
break;
|
|
12422
|
+
default:
|
|
12423
|
+
control = `<input type="text" name="${name}" value="${value}"${required}>`;
|
|
12424
|
+
}
|
|
12425
|
+
const error = field.error !== null ? `<small class="tempest-admin-form__field-error">${escapeHtml(field.error)}</small>` : "";
|
|
12426
|
+
return `${control}${error}`;
|
|
12427
|
+
}
|
|
12428
|
+
function renderInline(inline, csrfToken, error) {
|
|
12429
|
+
const heading = `<header class="tempest-admin-inline__header">
|
|
12430
|
+
<h2>${escapeHtml(inline.label)}${inline.total > 0 ? ` <span class="tempest-admin-inline__count">(${escapeHtml(inline.total)})</span>` : ""}</h2>
|
|
12431
|
+
${inline.addUrl !== null ? `<a class="tempest-admin-btn" href="${escapeHtml(inline.addUrl)}">Add</a>` : ""}
|
|
12432
|
+
</header>`;
|
|
12433
|
+
const more = inline.truncated ? `<p class="tempest-admin-inline__more"><em>Showing the first ${escapeHtml(inline.rows.length)} of ${escapeHtml(inline.total)}.</em></p>` : "";
|
|
12434
|
+
if (!inline.editable) {
|
|
12435
|
+
if (inline.rows.length === 0) {
|
|
12436
|
+
return `<section class="tempest-admin-inline">${heading}<p><em>No related records.</em></p></section>`;
|
|
12437
|
+
}
|
|
12438
|
+
const head2 = `<tr>${inline.columns.map((column6) => `<th>${escapeHtml(column6)}</th>`).join("")}<th></th></tr>`;
|
|
12439
|
+
const body = inline.rows.map(
|
|
12440
|
+
(row) => `<tr>${row.cells.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}<td>${row.url === null ? "" : `<a href="${escapeHtml(row.url)}">View</a>`}</td></tr>`
|
|
12441
|
+
).join("");
|
|
12442
|
+
return `<section class="tempest-admin-inline">
|
|
12443
|
+
${heading}
|
|
12444
|
+
<div class="tempest-admin-inline__scroll">
|
|
12445
|
+
<table class="tempest-admin-inline__table">
|
|
12446
|
+
<thead>${head2}</thead>
|
|
12447
|
+
<tbody>${body}</tbody>
|
|
12448
|
+
</table>
|
|
12449
|
+
</div>
|
|
12450
|
+
${more}
|
|
12451
|
+
</section>`;
|
|
12452
|
+
}
|
|
12453
|
+
const head = `<tr>${inline.columns.map((column6) => `<th>${escapeHtml(column6)}</th>`).join("")}${inline.canDelete ? "<th>Delete</th>" : ""}</tr>`;
|
|
12454
|
+
const renderRow = (row, isNew) => `<tr${isNew ? ' class="tempest-admin-inline__new"' : ""}>${row.fields.map((field) => `<td>${renderInlineCell(field)}</td>`).join("")}${inline.canDelete ? `<td class="tempest-admin-inline__del">${isNew ? "" : `<input type="checkbox" name="row.${escapeHtml(row.key)}.__delete" value="true">`}</td>` : ""}</tr>`;
|
|
12455
|
+
const rows = inline.rows.map((row) => renderRow(row, false)).join("");
|
|
12456
|
+
const blank = inline.newRow === null ? "" : renderRow(inline.newRow, true);
|
|
12457
|
+
return `<section class="tempest-admin-inline">
|
|
12458
|
+
${heading}
|
|
12459
|
+
${error !== null ? `<p class="tempest-admin-form__error">${escapeHtml(error)}</p>` : ""}
|
|
12460
|
+
<form method="post" action="${escapeHtml(inline.formAction)}" class="tempest-admin-inline__form">
|
|
12461
|
+
<input type="hidden" name="csrf_token" value="${escapeHtml(csrfToken)}">
|
|
12462
|
+
<div class="tempest-admin-inline__scroll">
|
|
12463
|
+
<table class="tempest-admin-inline__table">
|
|
12464
|
+
<thead>${head}</thead>
|
|
12465
|
+
<tbody>${rows}${blank}</tbody>
|
|
12466
|
+
</table>
|
|
12467
|
+
</div>
|
|
12468
|
+
<div class="tempest-admin-form__actions">
|
|
12469
|
+
<button type="submit">Save ${escapeHtml(inline.label)}</button>
|
|
12470
|
+
</div>
|
|
12471
|
+
</form>
|
|
12472
|
+
${more}
|
|
12473
|
+
</section>`;
|
|
12474
|
+
}
|
|
12257
12475
|
function renderAuditPanel(audit) {
|
|
12258
12476
|
const rows = audit.fields.map(
|
|
12259
12477
|
(field) => `<dt>${escapeHtml(field.label)}</dt><dd>${field.value === "" ? "<em>\u2014</em>" : escapeHtml(field.value)}</dd>`
|
|
@@ -12305,6 +12523,18 @@ function renderFormField(field) {
|
|
|
12305
12523
|
control = `<label><span>${label}${field.required ? " *" : ""}</span><select name="${name}"${required}>${blank}${options}</select></label>`;
|
|
12306
12524
|
break;
|
|
12307
12525
|
}
|
|
12526
|
+
case "file":
|
|
12527
|
+
control = `<label><span>${label}${field.required ? " *" : ""}</span><input type="file" name="${name}"${required}></label>${field.value === "" ? "" : `<small class="tempest-admin-form__hint">Current: ${value} \u2014 choose a file to replace it.</small>`}`;
|
|
12528
|
+
break;
|
|
12529
|
+
case "autocomplete":
|
|
12530
|
+
control = `<label><span>${label}${field.required ? " *" : ""}</span>
|
|
12531
|
+
<div class="tempest-admin-ac" data-ac data-ac-url="${escapeHtml(field.autocompleteUrl)}">
|
|
12532
|
+
<input type="text" class="tempest-admin-ac__search" value="${escapeHtml(field.displayLabel)}" placeholder="Search\u2026" autocomplete="off" data-ac-search>
|
|
12533
|
+
<input type="hidden" name="${name}" value="${value}"${required} data-ac-value>
|
|
12534
|
+
<ul class="tempest-admin-ac__results" data-ac-results hidden></ul>
|
|
12535
|
+
</div>
|
|
12536
|
+
</label>`;
|
|
12537
|
+
break;
|
|
12308
12538
|
case "number":
|
|
12309
12539
|
control = `<label><span>${label}${field.required ? " *" : ""}</span><input type="number" name="${name}" value="${value}"${field.step !== null ? ` step="${escapeHtml(field.step)}"` : ""}${required}></label>`;
|
|
12310
12540
|
break;
|
|
@@ -12332,7 +12562,7 @@ function renderFormPage(context, view) {
|
|
|
12332
12562
|
<a href="${escapeHtml(view.backUrl)}">\u2190 Back</a>
|
|
12333
12563
|
</header>
|
|
12334
12564
|
${view.error !== null ? `<p class="tempest-admin-form__error">${escapeHtml(view.error)}</p>` : ""}
|
|
12335
|
-
<form method="post" action="${escapeHtml(view.actionUrl)}" class="tempest-admin-form__form">
|
|
12565
|
+
<form method="post" action="${escapeHtml(view.actionUrl)}" class="tempest-admin-form__form"${view.fields.some((field) => field.widget === "file") ? ' enctype="multipart/form-data"' : ""}>
|
|
12336
12566
|
<input type="hidden" name="csrf_token" value="${escapeHtml(context.session.csrfToken)}">
|
|
12337
12567
|
${view.fields.map(renderFormField).join("")}
|
|
12338
12568
|
<div class="tempest-admin-form__actions">
|
|
@@ -12340,10 +12570,98 @@ function renderFormPage(context, view) {
|
|
|
12340
12570
|
<a href="${escapeHtml(view.backUrl)}" class="tempest-admin-form__cancel">Cancel</a>
|
|
12341
12571
|
</div>
|
|
12342
12572
|
</form>
|
|
12573
|
+
${view.fields.some((field) => field.widget === "autocomplete") ? AUTOCOMPLETE_SCRIPT : ""}
|
|
12343
12574
|
</section>`;
|
|
12344
12575
|
return renderLayout(context, `${heading} \xB7 ${context.site.title}`, body);
|
|
12345
12576
|
}
|
|
12577
|
+
function renderImportPage(context, view) {
|
|
12578
|
+
if (context.session === null) throw new Error("The import page requires a session");
|
|
12579
|
+
const summary = view.created === null ? "" : `<p class="tempest-admin-import__summary">Created ${escapeHtml(view.created)} record${view.created === 1 ? "" : "s"}.</p>`;
|
|
12580
|
+
const failures = view.rowErrors.length > 0 ? `<table class="tempest-admin-import__errors"><thead><tr><th>Row</th><th>Problem</th></tr></thead><tbody>${view.rowErrors.map(
|
|
12581
|
+
(failure) => `<tr><td>${escapeHtml(failure.row)}</td><td>${escapeHtml(failure.message)}</td></tr>`
|
|
12582
|
+
).join("")}</tbody></table>` : "";
|
|
12583
|
+
const body = `<section class="tempest-admin-import">
|
|
12584
|
+
<header class="tempest-admin-detail__header">
|
|
12585
|
+
<h1>Import ${escapeHtml(view.title)}</h1>
|
|
12586
|
+
<a href="${escapeHtml(view.backUrl)}">\u2190 Back</a>
|
|
12587
|
+
</header>
|
|
12588
|
+
${view.error !== null ? `<p class="tempest-admin-form__error">${escapeHtml(view.error)}</p>` : ""}
|
|
12589
|
+
${summary}
|
|
12590
|
+
${failures}
|
|
12591
|
+
<form method="post" action="${escapeHtml(view.actionUrl)}" class="tempest-admin-form__form" enctype="multipart/form-data">
|
|
12592
|
+
<input type="hidden" name="csrf_token" value="${escapeHtml(context.session.csrfToken)}">
|
|
12593
|
+
<div class="tempest-admin-form__field">
|
|
12594
|
+
<label>
|
|
12595
|
+
<span>CSV file *</span>
|
|
12596
|
+
<input type="file" name="file" accept=".csv,text/csv" required>
|
|
12597
|
+
</label>
|
|
12598
|
+
<small class="tempest-admin-form__hint">
|
|
12599
|
+
UTF-8, comma-separated, with a header row. Recognised columns:
|
|
12600
|
+
<code>${escapeHtml(view.columns.join(", "))}</code>. Unknown columns are ignored.
|
|
12601
|
+
</small>
|
|
12602
|
+
</div>
|
|
12603
|
+
<div class="tempest-admin-form__actions">
|
|
12604
|
+
<button type="submit">Import</button>
|
|
12605
|
+
<a href="${escapeHtml(view.backUrl)}" class="tempest-admin-form__cancel">Cancel</a>
|
|
12606
|
+
</div>
|
|
12607
|
+
</form>
|
|
12608
|
+
</section>`;
|
|
12609
|
+
return renderLayout(context, `Import ${view.title} \xB7 ${context.site.title}`, body);
|
|
12610
|
+
}
|
|
12611
|
+
var AUTOCOMPLETE_SCRIPT = `<script>
|
|
12612
|
+
(function () {
|
|
12613
|
+
document.querySelectorAll("[data-ac]").forEach(function (box) {
|
|
12614
|
+
var search = box.querySelector("[data-ac-search]");
|
|
12615
|
+
var value = box.querySelector("[data-ac-value]");
|
|
12616
|
+
var results = box.querySelector("[data-ac-results]");
|
|
12617
|
+
var url = box.getAttribute("data-ac-url");
|
|
12618
|
+
var timer = null;
|
|
12619
|
+
|
|
12620
|
+
function close() {
|
|
12621
|
+
results.hidden = true;
|
|
12622
|
+
results.innerHTML = "";
|
|
12623
|
+
}
|
|
12624
|
+
|
|
12625
|
+
function pick(option) {
|
|
12626
|
+
value.value = option.value;
|
|
12627
|
+
search.value = option.label;
|
|
12628
|
+
close();
|
|
12629
|
+
}
|
|
12630
|
+
|
|
12631
|
+
function run() {
|
|
12632
|
+
var term = search.value.trim();
|
|
12633
|
+
fetch(url + "?q=" + encodeURIComponent(term), { credentials: "same-origin" })
|
|
12634
|
+
.then(function (response) { return response.ok ? response.json() : { options: [] }; })
|
|
12635
|
+
.then(function (payload) {
|
|
12636
|
+
results.innerHTML = "";
|
|
12637
|
+
(payload.options || []).forEach(function (option) {
|
|
12638
|
+
var item = document.createElement("li");
|
|
12639
|
+
item.textContent = option.label;
|
|
12640
|
+
item.setAttribute("role", "option");
|
|
12641
|
+
item.addEventListener("mousedown", function (event) {
|
|
12642
|
+
event.preventDefault();
|
|
12643
|
+
pick(option);
|
|
12644
|
+
});
|
|
12645
|
+
results.appendChild(item);
|
|
12646
|
+
});
|
|
12647
|
+
results.hidden = results.children.length === 0;
|
|
12648
|
+
})
|
|
12649
|
+
.catch(close);
|
|
12650
|
+
}
|
|
12651
|
+
|
|
12652
|
+
search.addEventListener("input", function () {
|
|
12653
|
+
value.value = "";
|
|
12654
|
+
window.clearTimeout(timer);
|
|
12655
|
+
timer = window.setTimeout(run, 250);
|
|
12656
|
+
});
|
|
12657
|
+
search.addEventListener("focus", run);
|
|
12658
|
+
search.addEventListener("blur", function () { window.setTimeout(close, 150); });
|
|
12659
|
+
});
|
|
12660
|
+
})();
|
|
12661
|
+
</script>`;
|
|
12346
12662
|
var logger2 = new JSONLogger("tempest_express_sdk.admin.router");
|
|
12663
|
+
var INLINE_ROW_LIMIT = 50;
|
|
12664
|
+
var AUTOCOMPLETE_LIMIT = 20;
|
|
12347
12665
|
var AUDIT_HISTORY_LIMIT = 50;
|
|
12348
12666
|
var FK_OPTION_CAP = 1e3;
|
|
12349
12667
|
var FLASH_MAX_LENGTH = 300;
|
|
@@ -12373,6 +12691,7 @@ function makeAdminRouter(site, options) {
|
|
|
12373
12691
|
const theme = resolveAdminTheme(site.theme);
|
|
12374
12692
|
const showMetrics = options.showMetrics ?? true;
|
|
12375
12693
|
const exportMaxRows = options.exportMaxRows ?? 5e3;
|
|
12694
|
+
const maxUploadBytes = options.maxUploadBytes ?? 10 * 1024 * 1024;
|
|
12376
12695
|
const sessions = new AdminSessionStore({
|
|
12377
12696
|
secret: options.secretKey,
|
|
12378
12697
|
...options.cookieName === void 0 ? {} : { cookieName: options.cookieName },
|
|
@@ -12630,7 +12949,8 @@ function makeAdminRouter(site, options) {
|
|
|
12630
12949
|
mode: "create",
|
|
12631
12950
|
title: admin.verboseName(),
|
|
12632
12951
|
fields: buildFormFields(admin, {
|
|
12633
|
-
foreignKeyOptions: await foreignKeyOptionsFor(admin, state.dbSession)
|
|
12952
|
+
foreignKeyOptions: await foreignKeyOptionsFor(admin, state.dbSession),
|
|
12953
|
+
autocompleteUrls: autocompleteUrlsFor(admin)
|
|
12634
12954
|
}),
|
|
12635
12955
|
actionUrl: `${prefix}/m/${admin.slug()}/new`,
|
|
12636
12956
|
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
@@ -12650,10 +12970,28 @@ function makeAdminRouter(site, options) {
|
|
|
12650
12970
|
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12651
12971
|
return;
|
|
12652
12972
|
}
|
|
12653
|
-
|
|
12654
|
-
|
|
12973
|
+
let uploadError = null;
|
|
12974
|
+
let submission;
|
|
12975
|
+
try {
|
|
12976
|
+
submission = await readSubmission(req);
|
|
12977
|
+
} catch (error) {
|
|
12978
|
+
if (!(error instanceof MultipartLimitError)) throw error;
|
|
12979
|
+
submission = { fields: {}, files: [] };
|
|
12980
|
+
uploadError = error.message;
|
|
12981
|
+
}
|
|
12982
|
+
const body = submission.fields;
|
|
12983
|
+
if (uploadError === null && !csrfTokenMatches(state.session, body.csrf_token)) {
|
|
12984
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 403);
|
|
12985
|
+
return;
|
|
12986
|
+
}
|
|
12655
12987
|
const parsed = parseFormBody(admin, body);
|
|
12988
|
+
await applyUploads(admin, parsed.data, parsed.errors, submission.files, true);
|
|
12656
12989
|
const foreignKeyOptions = await foreignKeyOptionsFor(admin, state.dbSession);
|
|
12990
|
+
const autocompleteLabels = await autocompleteLabelsFor(
|
|
12991
|
+
admin,
|
|
12992
|
+
body,
|
|
12993
|
+
state.dbSession
|
|
12994
|
+
);
|
|
12657
12995
|
const rerender = (error, status) => {
|
|
12658
12996
|
html(
|
|
12659
12997
|
res,
|
|
@@ -12663,7 +13001,9 @@ function makeAdminRouter(site, options) {
|
|
|
12663
13001
|
fields: buildFormFields(admin, {
|
|
12664
13002
|
values: body,
|
|
12665
13003
|
errors: parsed.errors,
|
|
12666
|
-
foreignKeyOptions
|
|
13004
|
+
foreignKeyOptions,
|
|
13005
|
+
autocompleteUrls: autocompleteUrlsFor(admin),
|
|
13006
|
+
autocompleteLabels
|
|
12667
13007
|
}),
|
|
12668
13008
|
actionUrl: `${prefix}/m/${admin.slug()}/new`,
|
|
12669
13009
|
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
@@ -12672,6 +13012,10 @@ function makeAdminRouter(site, options) {
|
|
|
12672
13012
|
status
|
|
12673
13013
|
);
|
|
12674
13014
|
};
|
|
13015
|
+
if (uploadError !== null) {
|
|
13016
|
+
rerender(uploadError, 400);
|
|
13017
|
+
return;
|
|
13018
|
+
}
|
|
12675
13019
|
if (Object.keys(parsed.errors).length > 0) {
|
|
12676
13020
|
rerender("Please fix the highlighted fields.", 400);
|
|
12677
13021
|
return;
|
|
@@ -12791,6 +13135,129 @@ function makeAdminRouter(site, options) {
|
|
|
12791
13135
|
);
|
|
12792
13136
|
})
|
|
12793
13137
|
);
|
|
13138
|
+
router.get(
|
|
13139
|
+
`${prefix}/m/:slug/import`,
|
|
13140
|
+
guarded(async (req, res) => {
|
|
13141
|
+
const state = await authenticate(req, res);
|
|
13142
|
+
if (state === null) return;
|
|
13143
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.CREATE);
|
|
13144
|
+
if (admin === null) return;
|
|
13145
|
+
if (!admin.canImport) {
|
|
13146
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
13147
|
+
return;
|
|
13148
|
+
}
|
|
13149
|
+
html(res, renderImport(req, state, admin, null, null, []));
|
|
13150
|
+
})
|
|
13151
|
+
);
|
|
13152
|
+
router.post(
|
|
13153
|
+
`${prefix}/m/:slug/import`,
|
|
13154
|
+
guarded(async (req, res) => {
|
|
13155
|
+
const state = await authenticate(req, res);
|
|
13156
|
+
if (state === null) return;
|
|
13157
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.CREATE);
|
|
13158
|
+
if (admin === null) return;
|
|
13159
|
+
if (!admin.canImport) {
|
|
13160
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
13161
|
+
return;
|
|
13162
|
+
}
|
|
13163
|
+
let submission;
|
|
13164
|
+
try {
|
|
13165
|
+
submission = await readSubmission(req);
|
|
13166
|
+
} catch (error) {
|
|
13167
|
+
if (!(error instanceof MultipartLimitError)) throw error;
|
|
13168
|
+
html(res, renderImport(req, state, admin, error.message, null, []), 400);
|
|
13169
|
+
return;
|
|
13170
|
+
}
|
|
13171
|
+
if (!csrfTokenMatches(state.session, submission.fields.csrf_token)) {
|
|
13172
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 403);
|
|
13173
|
+
return;
|
|
13174
|
+
}
|
|
13175
|
+
const file = submission.files[0];
|
|
13176
|
+
if (file === void 0) {
|
|
13177
|
+
html(
|
|
13178
|
+
res,
|
|
13179
|
+
renderImport(req, state, admin, "Choose a CSV file to import.", null, []),
|
|
13180
|
+
400
|
|
13181
|
+
);
|
|
13182
|
+
return;
|
|
13183
|
+
}
|
|
13184
|
+
let rows;
|
|
13185
|
+
try {
|
|
13186
|
+
rows = parseCsv(file.data.toString("utf8"));
|
|
13187
|
+
} catch (error) {
|
|
13188
|
+
const message = error instanceof Error ? error.message : "Could not read the file as UTF-8 CSV.";
|
|
13189
|
+
html(res, renderImport(req, state, admin, message, null, []), 400);
|
|
13190
|
+
return;
|
|
13191
|
+
}
|
|
13192
|
+
const repository = admin.repository(state.dbSession);
|
|
13193
|
+
const actorId = backend.principalId(state.principal);
|
|
13194
|
+
const rowErrors = [];
|
|
13195
|
+
let created = 0;
|
|
13196
|
+
for (const [index, row] of rows.entries()) {
|
|
13197
|
+
const parsed = parseFormBody(admin, row, { uploadsAsText: true });
|
|
13198
|
+
const failures = Object.entries(parsed.errors);
|
|
13199
|
+
if (failures.length > 0) {
|
|
13200
|
+
rowErrors.push({
|
|
13201
|
+
row: index + 2,
|
|
13202
|
+
message: failures.map(([field, error]) => `${field}: ${error}`).join("; ")
|
|
13203
|
+
});
|
|
13204
|
+
continue;
|
|
13205
|
+
}
|
|
13206
|
+
stampActor(admin, parsed.data, actorId, true);
|
|
13207
|
+
try {
|
|
13208
|
+
await repository.create(parsed.data);
|
|
13209
|
+
created += 1;
|
|
13210
|
+
} catch (error) {
|
|
13211
|
+
rowErrors.push({
|
|
13212
|
+
row: index + 2,
|
|
13213
|
+
message: error instanceof Error ? error.message : String(error)
|
|
13214
|
+
});
|
|
13215
|
+
}
|
|
13216
|
+
}
|
|
13217
|
+
html(res, renderImport(req, state, admin, null, created, rowErrors));
|
|
13218
|
+
})
|
|
13219
|
+
);
|
|
13220
|
+
router.get(
|
|
13221
|
+
`${prefix}/m/:slug/autocomplete/:field`,
|
|
13222
|
+
guarded(async (req, res) => {
|
|
13223
|
+
if (sessions.load(req) === null) {
|
|
13224
|
+
res.status(401).json({ options: [] });
|
|
13225
|
+
return;
|
|
13226
|
+
}
|
|
13227
|
+
const state = await authenticate(req, res);
|
|
13228
|
+
if (state === null) return;
|
|
13229
|
+
const admin = site.get(String(req.params.slug));
|
|
13230
|
+
const field = String(req.params.field);
|
|
13231
|
+
if (admin === null || !admin.autocompleteFields.includes(field) || !await allows(state.principal, admin, AdminPermission.VIEW)) {
|
|
13232
|
+
res.status(404).json({ options: [] });
|
|
13233
|
+
return;
|
|
13234
|
+
}
|
|
13235
|
+
const column6 = adminColumns(admin.model)[field];
|
|
13236
|
+
const table = column6 === void 0 ? null : foreignKeyTable(column6);
|
|
13237
|
+
const referenced = table === null ? null : site.get(table);
|
|
13238
|
+
if (referenced === null) {
|
|
13239
|
+
res.json({ options: [] });
|
|
13240
|
+
return;
|
|
13241
|
+
}
|
|
13242
|
+
const term = queryString(req.query.q);
|
|
13243
|
+
const searchable = referenced.searchFields.filter((name) => {
|
|
13244
|
+
const target = adminColumns(referenced.model)[name];
|
|
13245
|
+
return target !== void 0 && isSearchableColumn(target);
|
|
13246
|
+
});
|
|
13247
|
+
const filters = term === "" || searchable.length === 0 ? void 0 : tempestDbJs.or(...searchable.map((name) => ({ [name]: { ilike: `%${term}%` } })));
|
|
13248
|
+
const page2 = await referenced.repository(state.dbSession).paginate({
|
|
13249
|
+
page: 1,
|
|
13250
|
+
pageSize: AUTOCOMPLETE_LIMIT,
|
|
13251
|
+
...filters === void 0 ? {} : { filters }
|
|
13252
|
+
});
|
|
13253
|
+
res.json({
|
|
13254
|
+
options: page2.items.map((row) => ({
|
|
13255
|
+
value: String(row[referenced.identityField]),
|
|
13256
|
+
label: foreignKeyLabel(referenced, row)
|
|
13257
|
+
}))
|
|
13258
|
+
});
|
|
13259
|
+
})
|
|
13260
|
+
);
|
|
12794
13261
|
router.get(
|
|
12795
13262
|
`${prefix}/m/:slug/:identity`,
|
|
12796
13263
|
guarded(async (req, res) => {
|
|
@@ -12810,6 +13277,8 @@ function makeAdminRouter(site, options) {
|
|
|
12810
13277
|
title: admin.verboseName(),
|
|
12811
13278
|
identity,
|
|
12812
13279
|
audit: await buildAuditView(admin, row, state.dbSession),
|
|
13280
|
+
inlines: await buildInlines(admin, row, state, identity),
|
|
13281
|
+
inlineError: null,
|
|
12813
13282
|
fields: admin.detailFieldNames().map((name) => ({ label: name, value: formatCellValue(row[name]) })),
|
|
12814
13283
|
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
12815
13284
|
editUrl: await allows(state.principal, admin, AdminPermission.EDIT) ? `${prefix}/m/${admin.slug()}/${identity}/edit` : null,
|
|
@@ -12838,7 +13307,9 @@ function makeAdminRouter(site, options) {
|
|
|
12838
13307
|
title: admin.verboseName(),
|
|
12839
13308
|
fields: buildFormFields(admin, {
|
|
12840
13309
|
values: row,
|
|
12841
|
-
foreignKeyOptions: await foreignKeyOptionsFor(admin, state.dbSession)
|
|
13310
|
+
foreignKeyOptions: await foreignKeyOptionsFor(admin, state.dbSession),
|
|
13311
|
+
autocompleteUrls: autocompleteUrlsFor(admin),
|
|
13312
|
+
autocompleteLabels: await autocompleteLabelsFor(admin, row, state.dbSession)
|
|
12842
13313
|
}),
|
|
12843
13314
|
actionUrl: `${prefix}/m/${admin.slug()}/${identity}/edit`,
|
|
12844
13315
|
backUrl: `${prefix}/m/${admin.slug()}/${identity}`,
|
|
@@ -12859,10 +13330,28 @@ function makeAdminRouter(site, options) {
|
|
|
12859
13330
|
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12860
13331
|
return;
|
|
12861
13332
|
}
|
|
12862
|
-
|
|
12863
|
-
|
|
13333
|
+
let uploadError = null;
|
|
13334
|
+
let submission;
|
|
13335
|
+
try {
|
|
13336
|
+
submission = await readSubmission(req);
|
|
13337
|
+
} catch (error) {
|
|
13338
|
+
if (!(error instanceof MultipartLimitError)) throw error;
|
|
13339
|
+
submission = { fields: {}, files: [] };
|
|
13340
|
+
uploadError = error.message;
|
|
13341
|
+
}
|
|
13342
|
+
const body = submission.fields;
|
|
13343
|
+
if (uploadError === null && !csrfTokenMatches(state.session, body.csrf_token)) {
|
|
13344
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 403);
|
|
13345
|
+
return;
|
|
13346
|
+
}
|
|
12864
13347
|
const parsed = parseFormBody(admin, body);
|
|
13348
|
+
await applyUploads(admin, parsed.data, parsed.errors, submission.files, false);
|
|
12865
13349
|
const foreignKeyOptions = await foreignKeyOptionsFor(admin, state.dbSession);
|
|
13350
|
+
const autocompleteLabels = await autocompleteLabelsFor(
|
|
13351
|
+
admin,
|
|
13352
|
+
body,
|
|
13353
|
+
state.dbSession
|
|
13354
|
+
);
|
|
12866
13355
|
const rerender = (error, status) => {
|
|
12867
13356
|
html(
|
|
12868
13357
|
res,
|
|
@@ -12872,7 +13361,9 @@ function makeAdminRouter(site, options) {
|
|
|
12872
13361
|
fields: buildFormFields(admin, {
|
|
12873
13362
|
values: body,
|
|
12874
13363
|
errors: parsed.errors,
|
|
12875
|
-
foreignKeyOptions
|
|
13364
|
+
foreignKeyOptions,
|
|
13365
|
+
autocompleteUrls: autocompleteUrlsFor(admin),
|
|
13366
|
+
autocompleteLabels
|
|
12876
13367
|
}),
|
|
12877
13368
|
actionUrl: `${prefix}/m/${admin.slug()}/${identity}/edit`,
|
|
12878
13369
|
backUrl: `${prefix}/m/${admin.slug()}/${identity}`,
|
|
@@ -12881,6 +13372,10 @@ function makeAdminRouter(site, options) {
|
|
|
12881
13372
|
status
|
|
12882
13373
|
);
|
|
12883
13374
|
};
|
|
13375
|
+
if (uploadError !== null) {
|
|
13376
|
+
rerender(uploadError, 400);
|
|
13377
|
+
return;
|
|
13378
|
+
}
|
|
12884
13379
|
if (Object.keys(parsed.errors).length > 0) {
|
|
12885
13380
|
rerender("Please fix the highlighted fields.", 400);
|
|
12886
13381
|
return;
|
|
@@ -12904,6 +13399,103 @@ function makeAdminRouter(site, options) {
|
|
|
12904
13399
|
res.redirect(`${prefix}/m/${admin.slug()}/${identity}?ok=updated`);
|
|
12905
13400
|
})
|
|
12906
13401
|
);
|
|
13402
|
+
router.post(
|
|
13403
|
+
`${prefix}/m/:slug/:identity/inlines/:child`,
|
|
13404
|
+
guarded(async (req, res) => {
|
|
13405
|
+
const state = await authenticate(req, res);
|
|
13406
|
+
if (state === null) return;
|
|
13407
|
+
const admin = await resolveAdmin(req, res, state);
|
|
13408
|
+
if (admin === null) return;
|
|
13409
|
+
if (!checkCsrf(req, res, state)) return;
|
|
13410
|
+
const childSlug = String(req.params.child);
|
|
13411
|
+
const inline = admin.inlines.find(
|
|
13412
|
+
(entry) => entry.slug === childSlug && entry.editable
|
|
13413
|
+
);
|
|
13414
|
+
const childAdmin = inline === void 0 ? null : site.get(childSlug);
|
|
13415
|
+
if (inline === void 0 || childAdmin === null || !await allows(state.principal, childAdmin, AdminPermission.EDIT)) {
|
|
13416
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
13417
|
+
return;
|
|
13418
|
+
}
|
|
13419
|
+
const identity = String(req.params.identity);
|
|
13420
|
+
const parent = await findRow(admin, state.dbSession, identity);
|
|
13421
|
+
if (parent === null) {
|
|
13422
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
13423
|
+
return;
|
|
13424
|
+
}
|
|
13425
|
+
const parentId = parent[admin.identityField];
|
|
13426
|
+
const { rows: grouped, deletions } = groupInlineSubmission(
|
|
13427
|
+
req.body
|
|
13428
|
+
);
|
|
13429
|
+
const names = inlineFieldNames(childAdmin, inline);
|
|
13430
|
+
const childRepo = childAdmin.repository(state.dbSession);
|
|
13431
|
+
const actorId = backend.principalId(state.principal);
|
|
13432
|
+
const canDelete = inline.canDelete && await allows(state.principal, childAdmin, AdminPermission.DELETE);
|
|
13433
|
+
const failed = [];
|
|
13434
|
+
let formError = null;
|
|
13435
|
+
const owned = async (key) => await childRepo.first({
|
|
13436
|
+
[childAdmin.identityField]: key,
|
|
13437
|
+
[inline.fkField]: parentId
|
|
13438
|
+
});
|
|
13439
|
+
for (const [key, values] of Object.entries(grouped)) {
|
|
13440
|
+
const isNew = key.startsWith("new");
|
|
13441
|
+
if (!isNew && canDelete && deletions.has(key)) {
|
|
13442
|
+
if (await owned(key) !== null) {
|
|
13443
|
+
await childRepo.delete({ [childAdmin.identityField]: key });
|
|
13444
|
+
}
|
|
13445
|
+
continue;
|
|
13446
|
+
}
|
|
13447
|
+
if (isNew && Object.values(values).every((value) => value.trim() === "")) {
|
|
13448
|
+
continue;
|
|
13449
|
+
}
|
|
13450
|
+
const parsed = parseFormBody(childAdmin, values, { only: names });
|
|
13451
|
+
if (Object.keys(parsed.errors).length > 0) {
|
|
13452
|
+
failed.push({ key, values, errors: parsed.errors });
|
|
13453
|
+
continue;
|
|
13454
|
+
}
|
|
13455
|
+
try {
|
|
13456
|
+
if (isNew) {
|
|
13457
|
+
stampActor(childAdmin, parsed.data, actorId, true);
|
|
13458
|
+
await childRepo.create({
|
|
13459
|
+
...parsed.data,
|
|
13460
|
+
[inline.fkField]: parentId
|
|
13461
|
+
});
|
|
13462
|
+
} else {
|
|
13463
|
+
if (await owned(key) === null) continue;
|
|
13464
|
+
stampActor(childAdmin, parsed.data, actorId, false);
|
|
13465
|
+
await childRepo.update(
|
|
13466
|
+
{ [childAdmin.identityField]: key },
|
|
13467
|
+
parsed.data
|
|
13468
|
+
);
|
|
13469
|
+
}
|
|
13470
|
+
} catch (error) {
|
|
13471
|
+
formError = describeWriteFailure(childAdmin, error);
|
|
13472
|
+
failed.push({ key, values, errors: {} });
|
|
13473
|
+
}
|
|
13474
|
+
}
|
|
13475
|
+
if (failed.length > 0) {
|
|
13476
|
+
const fresh = await findRow(admin, state.dbSession, identity) ?? parent;
|
|
13477
|
+
html(
|
|
13478
|
+
res,
|
|
13479
|
+
renderDetailPage(context(req, state.session, state.visible), {
|
|
13480
|
+
title: admin.verboseName(),
|
|
13481
|
+
identity,
|
|
13482
|
+
audit: await buildAuditView(admin, fresh, state.dbSession),
|
|
13483
|
+
inlines: await buildInlines(admin, fresh, state, identity, {
|
|
13484
|
+
[childSlug]: failed
|
|
13485
|
+
}),
|
|
13486
|
+
inlineError: formError ?? "Some inline rows could not be saved.",
|
|
13487
|
+
fields: admin.detailFieldNames().map((name) => ({ label: name, value: formatCellValue(fresh[name]) })),
|
|
13488
|
+
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
13489
|
+
editUrl: await allows(state.principal, admin, AdminPermission.EDIT) ? `${prefix}/m/${admin.slug()}/${identity}/edit` : null,
|
|
13490
|
+
deleteUrl: await allows(state.principal, admin, AdminPermission.DELETE) ? `${prefix}/m/${admin.slug()}/${identity}/delete` : null
|
|
13491
|
+
}),
|
|
13492
|
+
400
|
|
13493
|
+
);
|
|
13494
|
+
return;
|
|
13495
|
+
}
|
|
13496
|
+
res.redirect(`${prefix}/m/${admin.slug()}/${identity}?ok=updated`);
|
|
13497
|
+
})
|
|
13498
|
+
);
|
|
12907
13499
|
router.post(
|
|
12908
13500
|
`${prefix}/m/:slug/:identity/delete`,
|
|
12909
13501
|
guarded(async (req, res) => {
|
|
@@ -12925,6 +13517,89 @@ function makeAdminRouter(site, options) {
|
|
|
12925
13517
|
const principal = await backend.loadPrincipal(dbSession, String(actor));
|
|
12926
13518
|
return principal === null ? String(actor) : backend.displayName(principal);
|
|
12927
13519
|
}
|
|
13520
|
+
async function buildInlines(admin, parent, state, identity, overrides = {}) {
|
|
13521
|
+
const parentId = parent[admin.identityField];
|
|
13522
|
+
const blocks = [];
|
|
13523
|
+
for (const inline of admin.inlines) {
|
|
13524
|
+
const childAdmin = site.get(inline.slug);
|
|
13525
|
+
const repository = childAdmin === null ? new tempestDbJs.BaseRepository(inline.model, state.dbSession) : childAdmin.repository(state.dbSession);
|
|
13526
|
+
const children = await repository.list({
|
|
13527
|
+
[inline.fkField]: parentId
|
|
13528
|
+
});
|
|
13529
|
+
const columns = inline.listDisplay ?? childAdmin?.listDisplayNames() ?? Object.keys(adminColumns(inline.model));
|
|
13530
|
+
const label = inline.label ?? childAdmin?.verboseNamePlural() ?? humanizeField(inline.slug);
|
|
13531
|
+
const visible = children.slice(0, INLINE_ROW_LIMIT);
|
|
13532
|
+
const editable = inline.editable && childAdmin !== null && await allows(state.principal, childAdmin, AdminPermission.EDIT);
|
|
13533
|
+
const addUrl = childAdmin !== null && await allows(state.principal, childAdmin, AdminPermission.CREATE) ? `${prefix}/m/${inline.slug}/new` : null;
|
|
13534
|
+
if (!editable || childAdmin === null) {
|
|
13535
|
+
blocks.push({
|
|
13536
|
+
label,
|
|
13537
|
+
total: children.length,
|
|
13538
|
+
columns,
|
|
13539
|
+
editable: false,
|
|
13540
|
+
canDelete: false,
|
|
13541
|
+
addUrl,
|
|
13542
|
+
formAction: "",
|
|
13543
|
+
rows: visible.map((child) => ({
|
|
13544
|
+
key: String(child[childAdmin?.identityField ?? "id"]),
|
|
13545
|
+
cells: columns.map((column6) => formatCellValue(child[column6])),
|
|
13546
|
+
fields: [],
|
|
13547
|
+
url: childAdmin === null ? null : `${prefix}/m/${inline.slug}/${String(child[childAdmin.identityField])}`
|
|
13548
|
+
})),
|
|
13549
|
+
newRow: null,
|
|
13550
|
+
truncated: children.length > visible.length
|
|
13551
|
+
});
|
|
13552
|
+
continue;
|
|
13553
|
+
}
|
|
13554
|
+
const names = inlineFieldNames(childAdmin, inline);
|
|
13555
|
+
const submitted = overrides[inline.slug];
|
|
13556
|
+
const rows = [];
|
|
13557
|
+
if (submitted !== void 0) {
|
|
13558
|
+
for (const entry of submitted) {
|
|
13559
|
+
rows.push({
|
|
13560
|
+
key: entry.key,
|
|
13561
|
+
cells: [],
|
|
13562
|
+
fields: inlineFields(
|
|
13563
|
+
childAdmin,
|
|
13564
|
+
names,
|
|
13565
|
+
entry.key,
|
|
13566
|
+
entry.values,
|
|
13567
|
+
entry.errors
|
|
13568
|
+
),
|
|
13569
|
+
url: null
|
|
13570
|
+
});
|
|
13571
|
+
}
|
|
13572
|
+
} else {
|
|
13573
|
+
for (const child of visible) {
|
|
13574
|
+
const key = String(child[childAdmin.identityField]);
|
|
13575
|
+
rows.push({
|
|
13576
|
+
key,
|
|
13577
|
+
cells: [],
|
|
13578
|
+
fields: inlineFields(childAdmin, names, key, child, {}),
|
|
13579
|
+
url: `${prefix}/m/${inline.slug}/${key}`
|
|
13580
|
+
});
|
|
13581
|
+
}
|
|
13582
|
+
}
|
|
13583
|
+
blocks.push({
|
|
13584
|
+
label,
|
|
13585
|
+
total: children.length,
|
|
13586
|
+
columns: names.map(humanizeField),
|
|
13587
|
+
editable: true,
|
|
13588
|
+
canDelete: inline.canDelete && childAdmin.canDelete,
|
|
13589
|
+
addUrl,
|
|
13590
|
+
formAction: `${prefix}/m/${admin.slug()}/${identity}/inlines/${inline.slug}`,
|
|
13591
|
+
rows,
|
|
13592
|
+
newRow: {
|
|
13593
|
+
key: "new1",
|
|
13594
|
+
cells: [],
|
|
13595
|
+
fields: inlineFields(childAdmin, names, "new1", {}, {}),
|
|
13596
|
+
url: null
|
|
13597
|
+
},
|
|
13598
|
+
truncated: children.length > visible.length
|
|
13599
|
+
});
|
|
13600
|
+
}
|
|
13601
|
+
return blocks;
|
|
13602
|
+
}
|
|
12928
13603
|
async function buildAuditView(admin, row, dbSession) {
|
|
12929
13604
|
const fields = [];
|
|
12930
13605
|
for (const name of admin.auditFieldNames()) {
|
|
@@ -12964,6 +13639,44 @@ function makeAdminRouter(site, options) {
|
|
|
12964
13639
|
const row = await admin.repository(dbSession).first({ [admin.identityField]: identity });
|
|
12965
13640
|
return row ?? null;
|
|
12966
13641
|
}
|
|
13642
|
+
function renderImport(req, state, admin, error, created, rowErrors) {
|
|
13643
|
+
return renderImportPage(context(req, state.session, state.visible), {
|
|
13644
|
+
title: admin.verboseNamePlural(),
|
|
13645
|
+
actionUrl: `${prefix}/m/${admin.slug()}/import`,
|
|
13646
|
+
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
13647
|
+
columns: admin.editableFieldNames(),
|
|
13648
|
+
error,
|
|
13649
|
+
created,
|
|
13650
|
+
rowErrors
|
|
13651
|
+
});
|
|
13652
|
+
}
|
|
13653
|
+
async function readSubmission(req) {
|
|
13654
|
+
if (!isMultipart(req)) {
|
|
13655
|
+
return { fields: req.body ?? {}, files: [] };
|
|
13656
|
+
}
|
|
13657
|
+
const parsed = await parseMultipart(req, { maxFileBytes: maxUploadBytes });
|
|
13658
|
+
return { fields: parsed.fields, files: parsed.files };
|
|
13659
|
+
}
|
|
13660
|
+
async function applyUploads(admin, data, errors, files, creating) {
|
|
13661
|
+
if (admin.uploadFields.length === 0) return;
|
|
13662
|
+
const storage2 = admin.uploadStorage;
|
|
13663
|
+
if (storage2 === null) return;
|
|
13664
|
+
const columns = adminColumns(admin.model);
|
|
13665
|
+
for (const field of admin.uploadFields) {
|
|
13666
|
+
const file = files.find((candidate) => candidate.field === field);
|
|
13667
|
+
if (file === void 0) {
|
|
13668
|
+
const column6 = columns[field];
|
|
13669
|
+
if (creating && column6 !== void 0 && !isColumnOptional(column6)) {
|
|
13670
|
+
errors[field] = "This field is required.";
|
|
13671
|
+
}
|
|
13672
|
+
continue;
|
|
13673
|
+
}
|
|
13674
|
+
const extension = file.filename.includes(".") ? `.${file.filename.split(".").pop()}` : "";
|
|
13675
|
+
const key = `${admin.slug()}/${field}/${crypto.randomUUID()}${extension}`;
|
|
13676
|
+
const saved = await storage2.save(key, file.data, { contentType: file.contentType });
|
|
13677
|
+
data[field] = saved.key;
|
|
13678
|
+
}
|
|
13679
|
+
}
|
|
12967
13680
|
async function permittedBulkActions(admin, principal) {
|
|
12968
13681
|
const permitted = [];
|
|
12969
13682
|
for (const option of bulkActionsFor(admin)) {
|
|
@@ -13027,6 +13740,7 @@ function makeAdminRouter(site, options) {
|
|
|
13027
13740
|
filters: query.filterViews,
|
|
13028
13741
|
sort,
|
|
13029
13742
|
newUrl: await allows(state.principal, admin, AdminPermission.CREATE) ? `${prefix}/m/${admin.slug()}/new` : null,
|
|
13743
|
+
importUrl: admin.canImport && await allows(state.principal, admin, AdminPermission.CREATE) ? `${prefix}/m/${admin.slug()}/import` : null,
|
|
13030
13744
|
bulkActions: await permittedBulkActions(admin, state.principal),
|
|
13031
13745
|
bulkUrl: `${prefix}/m/${admin.slug()}/bulk`,
|
|
13032
13746
|
exportCsvUrl: exportUrl("csv"),
|
|
@@ -13151,6 +13865,7 @@ function makeAdminRouter(site, options) {
|
|
|
13151
13865
|
const columns = adminColumns(admin.model);
|
|
13152
13866
|
const options2 = {};
|
|
13153
13867
|
for (const field of Object.keys(foreignKeyFields(admin))) {
|
|
13868
|
+
if (admin.autocompleteFields.includes(field)) continue;
|
|
13154
13869
|
const column6 = columns[field];
|
|
13155
13870
|
if (column6 === void 0) continue;
|
|
13156
13871
|
const related = await relatedOptions(column6, dbSession);
|
|
@@ -13158,8 +13873,60 @@ function makeAdminRouter(site, options) {
|
|
|
13158
13873
|
}
|
|
13159
13874
|
return options2;
|
|
13160
13875
|
}
|
|
13876
|
+
function autocompleteUrlsFor(admin) {
|
|
13877
|
+
const urls = {};
|
|
13878
|
+
for (const field of admin.autocompleteFields) {
|
|
13879
|
+
urls[field] = `${prefix}/m/${admin.slug()}/autocomplete/${field}`;
|
|
13880
|
+
}
|
|
13881
|
+
return urls;
|
|
13882
|
+
}
|
|
13883
|
+
async function autocompleteLabelsFor(admin, row, dbSession) {
|
|
13884
|
+
const labels = {};
|
|
13885
|
+
if (row === null) return labels;
|
|
13886
|
+
const columns = adminColumns(admin.model);
|
|
13887
|
+
for (const field of admin.autocompleteFields) {
|
|
13888
|
+
const value = row[field];
|
|
13889
|
+
if (value === null || value === void 0 || value === "") continue;
|
|
13890
|
+
const column6 = columns[field];
|
|
13891
|
+
const table = column6 === void 0 ? null : foreignKeyTable(column6);
|
|
13892
|
+
const referenced = table === null ? null : site.get(table);
|
|
13893
|
+
if (referenced === null) continue;
|
|
13894
|
+
const related = await referenced.repository(dbSession).first({ [referenced.identityField]: value });
|
|
13895
|
+
if (related !== null) labels[field] = foreignKeyLabel(referenced, related);
|
|
13896
|
+
}
|
|
13897
|
+
return labels;
|
|
13898
|
+
}
|
|
13161
13899
|
return router;
|
|
13162
13900
|
}
|
|
13901
|
+
function groupInlineSubmission(body) {
|
|
13902
|
+
const rows = {};
|
|
13903
|
+
const deletions = /* @__PURE__ */ new Set();
|
|
13904
|
+
for (const [key, raw] of Object.entries(body)) {
|
|
13905
|
+
if (!key.startsWith("row.")) continue;
|
|
13906
|
+
const parts = key.split(".");
|
|
13907
|
+
if (parts.length !== 3) continue;
|
|
13908
|
+
const [, rowKey, field] = parts;
|
|
13909
|
+
const value = typeof raw === "string" ? raw : "";
|
|
13910
|
+
if (field === "__delete") {
|
|
13911
|
+
if (!["", "false", "off", "0", "no"].includes(value.trim().toLowerCase())) {
|
|
13912
|
+
deletions.add(rowKey);
|
|
13913
|
+
}
|
|
13914
|
+
continue;
|
|
13915
|
+
}
|
|
13916
|
+
const row = rows[rowKey] ?? {};
|
|
13917
|
+
row[field] = value;
|
|
13918
|
+
rows[rowKey] = row;
|
|
13919
|
+
}
|
|
13920
|
+
return { rows, deletions };
|
|
13921
|
+
}
|
|
13922
|
+
function inlineFieldNames(childAdmin, inline) {
|
|
13923
|
+
return childAdmin.editableFieldNames().filter(
|
|
13924
|
+
(name) => name !== inline.fkField && !childAdmin.uploadFields.includes(name) && !childAdmin.autocompleteFields.includes(name)
|
|
13925
|
+
);
|
|
13926
|
+
}
|
|
13927
|
+
function inlineFields(childAdmin, names, key, values, errors) {
|
|
13928
|
+
return buildFormFields(childAdmin, { values, errors }).filter((field) => names.includes(field.name)).map((field) => ({ ...field, name: `row.${key}.${field.name}` }));
|
|
13929
|
+
}
|
|
13163
13930
|
function flagAllows(admin, action) {
|
|
13164
13931
|
if (action === AdminPermission.CREATE) return admin.canCreate;
|
|
13165
13932
|
if (action === AdminPermission.EDIT) return admin.canEdit;
|
|
@@ -13249,6 +14016,55 @@ function bulkActionsFor(admin) {
|
|
|
13249
14016
|
}
|
|
13250
14017
|
return actions;
|
|
13251
14018
|
}
|
|
14019
|
+
function parseCsv(text) {
|
|
14020
|
+
const source = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
14021
|
+
const rows = [];
|
|
14022
|
+
let row = [];
|
|
14023
|
+
let field = "";
|
|
14024
|
+
let quoted = false;
|
|
14025
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
14026
|
+
const char = source[index];
|
|
14027
|
+
if (quoted) {
|
|
14028
|
+
if (char === '"') {
|
|
14029
|
+
if (source[index + 1] === '"') {
|
|
14030
|
+
field += '"';
|
|
14031
|
+
index += 1;
|
|
14032
|
+
} else {
|
|
14033
|
+
quoted = false;
|
|
14034
|
+
}
|
|
14035
|
+
} else {
|
|
14036
|
+
field += char;
|
|
14037
|
+
}
|
|
14038
|
+
continue;
|
|
14039
|
+
}
|
|
14040
|
+
if (char === '"') {
|
|
14041
|
+
quoted = true;
|
|
14042
|
+
} else if (char === ",") {
|
|
14043
|
+
row.push(field);
|
|
14044
|
+
field = "";
|
|
14045
|
+
} else if (char === "\n" || char === "\r") {
|
|
14046
|
+
if (char === "\r" && source[index + 1] === "\n") index += 1;
|
|
14047
|
+
row.push(field);
|
|
14048
|
+
rows.push(row);
|
|
14049
|
+
row = [];
|
|
14050
|
+
field = "";
|
|
14051
|
+
} else {
|
|
14052
|
+
field += char;
|
|
14053
|
+
}
|
|
14054
|
+
}
|
|
14055
|
+
if (field !== "" || row.length > 0) {
|
|
14056
|
+
row.push(field);
|
|
14057
|
+
rows.push(row);
|
|
14058
|
+
}
|
|
14059
|
+
const header = rows.shift();
|
|
14060
|
+
if (header === void 0 || header.length === 0) {
|
|
14061
|
+
throw new Error("The file has no header row.");
|
|
14062
|
+
}
|
|
14063
|
+
const keys = header.map((name) => name.trim());
|
|
14064
|
+
return rows.filter((entry) => entry.some((value) => value.trim() !== "")).map(
|
|
14065
|
+
(entry) => Object.fromEntries(keys.map((key, position) => [key, entry[position] ?? ""]))
|
|
14066
|
+
);
|
|
14067
|
+
}
|
|
13252
14068
|
function exportValue(value) {
|
|
13253
14069
|
if (value instanceof Date) return value.toISOString();
|
|
13254
14070
|
if (typeof value === "bigint") return value.toString();
|
|
@@ -15282,7 +16098,7 @@ async function withTestDatabase(models, fn) {
|
|
|
15282
16098
|
}
|
|
15283
16099
|
|
|
15284
16100
|
// src/version.ts
|
|
15285
|
-
var VERSION = "0.
|
|
16101
|
+
var VERSION = "0.28.0";
|
|
15286
16102
|
|
|
15287
16103
|
Object.defineProperty(exports, "OpenAPIRegistry", {
|
|
15288
16104
|
enumerable: true,
|
|
@@ -15496,6 +16312,7 @@ exports.MessageCatalog = MessageCatalog;
|
|
|
15496
16312
|
exports.MessagingHub = MessagingHub;
|
|
15497
16313
|
exports.MetricsUtils = MetricsUtils;
|
|
15498
16314
|
exports.MfaService = MfaService;
|
|
16315
|
+
exports.MultipartLimitError = MultipartLimitError;
|
|
15499
16316
|
exports.NotFoundException = NotFoundException;
|
|
15500
16317
|
exports.OAuthError = OAuthError;
|
|
15501
16318
|
exports.OIDCProvider = OIDCProvider;
|
|
@@ -15541,6 +16358,7 @@ exports.activationSchema = activationSchema;
|
|
|
15541
16358
|
exports.addLogSink = addLogSink;
|
|
15542
16359
|
exports.adminAction = adminAction;
|
|
15543
16360
|
exports.adminColumns = adminColumns;
|
|
16361
|
+
exports.adminInline = adminInline;
|
|
15544
16362
|
exports.adminLens = adminLens;
|
|
15545
16363
|
exports.adminThemeCss = adminThemeCss;
|
|
15546
16364
|
exports.attachWebSocketHub = attachWebSocketHub;
|
|
@@ -15602,12 +16420,14 @@ exports.getConditions = getConditions;
|
|
|
15602
16420
|
exports.getPaginationConditions = getPaginationConditions;
|
|
15603
16421
|
exports.getRequestId = getRequestId;
|
|
15604
16422
|
exports.getState = getState;
|
|
16423
|
+
exports.groupInlineSubmission = groupInlineSubmission;
|
|
15605
16424
|
exports.hashOpaqueToken = hashOpaqueToken;
|
|
15606
16425
|
exports.hexColorField = hexColorField;
|
|
15607
16426
|
exports.humanizeField = humanizeField;
|
|
15608
16427
|
exports.idempotencyMiddleware = idempotencyMiddleware;
|
|
15609
16428
|
exports.inboundMessageSchema = inboundMessageSchema;
|
|
15610
16429
|
exports.isColumnOptional = isColumnOptional;
|
|
16430
|
+
exports.isMultipart = isMultipart;
|
|
15611
16431
|
exports.isSearchableColumn = isSearchableColumn;
|
|
15612
16432
|
exports.isValidCep = isValidCep;
|
|
15613
16433
|
exports.isValidCity = isValidCity;
|
|
@@ -15667,7 +16487,9 @@ exports.paginationFilterSchema = paginationFilterSchema;
|
|
|
15667
16487
|
exports.paginationSchema = paginationSchema;
|
|
15668
16488
|
exports.parseAcceptLanguage = parseAcceptLanguage;
|
|
15669
16489
|
exports.parseCookies = parseCookies;
|
|
16490
|
+
exports.parseCsv = parseCsv;
|
|
15670
16491
|
exports.parseFormBody = parseFormBody;
|
|
16492
|
+
exports.parseMultipart = parseMultipart;
|
|
15671
16493
|
exports.partitionTotal = partitionTotal;
|
|
15672
16494
|
exports.passwordResetConfirmSchema = passwordResetConfirmSchema;
|
|
15673
16495
|
exports.passwordResetRequestSchema = passwordResetRequestSchema;
|
|
@@ -15689,6 +16511,7 @@ exports.renderAuthResultPage = renderAuthResultPage;
|
|
|
15689
16511
|
exports.renderDashboardPage = renderDashboardPage;
|
|
15690
16512
|
exports.renderDetailPage = renderDetailPage;
|
|
15691
16513
|
exports.renderFormPage = renderFormPage;
|
|
16514
|
+
exports.renderImportPage = renderImportPage;
|
|
15692
16515
|
exports.renderLayout = renderLayout;
|
|
15693
16516
|
exports.renderListPage = renderListPage;
|
|
15694
16517
|
exports.renderLoginPage = renderLoginPage;
|