@dyrected/core 2.16.0 → 2.18.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/dist/{app-KSDgfXsw.d.cts → app-Dd9cBbCs.d.cts} +1460 -1184
- package/dist/{app-KSDgfXsw.d.ts → app-Dd9cBbCs.d.ts} +1460 -1184
- package/dist/index.cjs +988 -6
- package/dist/index.d.cts +155 -4
- package/dist/index.d.ts +155 -4
- package/dist/index.js +969 -6
- package/dist/server.cjs +1079 -40
- package/dist/server.d.cts +20 -4
- package/dist/server.d.ts +20 -4
- package/dist/server.js +1078 -40
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -6084,6 +6084,72 @@ function createReadonlyDb(db) {
|
|
|
6084
6084
|
});
|
|
6085
6085
|
}
|
|
6086
6086
|
|
|
6087
|
+
// src/utils/errors.ts
|
|
6088
|
+
var DyrectedError = class _DyrectedError extends Error {
|
|
6089
|
+
statusCode;
|
|
6090
|
+
constructor(message, statusCode = 500) {
|
|
6091
|
+
super(message);
|
|
6092
|
+
this.name = "DyrectedError";
|
|
6093
|
+
this.statusCode = statusCode;
|
|
6094
|
+
Object.setPrototypeOf(this, _DyrectedError.prototype);
|
|
6095
|
+
}
|
|
6096
|
+
};
|
|
6097
|
+
var ValidationError = class _ValidationError extends DyrectedError {
|
|
6098
|
+
constructor(message) {
|
|
6099
|
+
super(message, 400);
|
|
6100
|
+
this.name = "ValidationError";
|
|
6101
|
+
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
6102
|
+
}
|
|
6103
|
+
};
|
|
6104
|
+
|
|
6105
|
+
// src/utils/field-integrity.ts
|
|
6106
|
+
function* storedFields(fields) {
|
|
6107
|
+
for (const field of fields ?? []) {
|
|
6108
|
+
if (field.type === "row") {
|
|
6109
|
+
yield* storedFields(field.fields);
|
|
6110
|
+
} else if (field.name) {
|
|
6111
|
+
yield field;
|
|
6112
|
+
}
|
|
6113
|
+
}
|
|
6114
|
+
}
|
|
6115
|
+
function sameValue(a, b) {
|
|
6116
|
+
if (a === b) return true;
|
|
6117
|
+
if (a === null || b === null || a === void 0 || b === void 0) return a == b;
|
|
6118
|
+
if (typeof a === "object" || typeof b === "object") {
|
|
6119
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
6120
|
+
}
|
|
6121
|
+
return false;
|
|
6122
|
+
}
|
|
6123
|
+
function assertImmutableFieldsUnchanged(fields, data, original) {
|
|
6124
|
+
for (const field of storedFields(fields)) {
|
|
6125
|
+
if (!field.immutable || data[field.name] === void 0) continue;
|
|
6126
|
+
if (!sameValue(data[field.name], original[field.name])) {
|
|
6127
|
+
throw new ValidationError(
|
|
6128
|
+
`Field "${field.label ?? field.name}" is immutable and cannot be modified.`
|
|
6129
|
+
);
|
|
6130
|
+
}
|
|
6131
|
+
}
|
|
6132
|
+
}
|
|
6133
|
+
function normalizeMoneyFields(fields, data) {
|
|
6134
|
+
let next = data;
|
|
6135
|
+
for (const field of storedFields(fields)) {
|
|
6136
|
+
if (field.type !== "money") continue;
|
|
6137
|
+
const value = data[field.name];
|
|
6138
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
6139
|
+
let minor;
|
|
6140
|
+
if (typeof value === "number") minor = value;
|
|
6141
|
+
else if (typeof value === "string" && /^-?\d+$/.test(value.trim())) minor = Number(value.trim());
|
|
6142
|
+
if (minor === void 0 || !Number.isSafeInteger(minor)) {
|
|
6143
|
+
throw new ValidationError(
|
|
6144
|
+
`Field "${field.label ?? field.name}" must be a whole number of minor units (for example kobo or cents).`
|
|
6145
|
+
);
|
|
6146
|
+
}
|
|
6147
|
+
if (next === data) next = { ...data };
|
|
6148
|
+
next[field.name] = minor;
|
|
6149
|
+
}
|
|
6150
|
+
return next;
|
|
6151
|
+
}
|
|
6152
|
+
|
|
6087
6153
|
// src/utils/upload-validation.ts
|
|
6088
6154
|
function isMimeAllowed(mimeType, allowed) {
|
|
6089
6155
|
const type = (mimeType || "").toLowerCase().trim();
|
|
@@ -6679,7 +6745,7 @@ async function dispatchLifecycleEvent(config, event) {
|
|
|
6679
6745
|
}
|
|
6680
6746
|
}
|
|
6681
6747
|
async function saveWorkflowDraft(args) {
|
|
6682
|
-
const { config, collection, id, originalDoc, data, user } = args;
|
|
6748
|
+
const { config, collection, id, originalDoc, data, user, beforeCommit } = args;
|
|
6683
6749
|
const db = config.db;
|
|
6684
6750
|
const workflow = collection.workflow;
|
|
6685
6751
|
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
@@ -6713,13 +6779,14 @@ async function saveWorkflowDraft(args) {
|
|
|
6713
6779
|
}
|
|
6714
6780
|
const updated = await tx.update({ collection: collection.slug, id, data: updateData });
|
|
6715
6781
|
await persistEvent(tx, event);
|
|
6782
|
+
await beforeCommit?.(tx, updated);
|
|
6716
6783
|
return updated;
|
|
6717
6784
|
});
|
|
6718
6785
|
void dispatchLifecycleEvent(config, event);
|
|
6719
6786
|
return { doc, event };
|
|
6720
6787
|
}
|
|
6721
6788
|
async function createWorkflowDocument(args) {
|
|
6722
|
-
const { config, collection, data, user } = args;
|
|
6789
|
+
const { config, collection, data, user, beforeCommit } = args;
|
|
6723
6790
|
const db = config.db;
|
|
6724
6791
|
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
6725
6792
|
let event;
|
|
@@ -6733,13 +6800,14 @@ async function createWorkflowDocument(args) {
|
|
|
6733
6800
|
payload: { revision: 1, previousRevision: null }
|
|
6734
6801
|
});
|
|
6735
6802
|
await persistEvent(tx, event);
|
|
6803
|
+
await beforeCommit?.(tx, created);
|
|
6736
6804
|
return created;
|
|
6737
6805
|
});
|
|
6738
6806
|
void dispatchLifecycleEvent(config, event);
|
|
6739
6807
|
return { doc, event };
|
|
6740
6808
|
}
|
|
6741
6809
|
async function transitionWorkflow(args) {
|
|
6742
|
-
const { config, collection, id, transitionName, expectedRevision, comment, user, req } = args;
|
|
6810
|
+
const { config, collection, id, transitionName, expectedRevision, comment, input, user, req } = args;
|
|
6743
6811
|
const db = config.db;
|
|
6744
6812
|
const workflow = collection.workflow;
|
|
6745
6813
|
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
@@ -6765,7 +6833,7 @@ async function transitionWorkflow(args) {
|
|
|
6765
6833
|
if (transition.requireComment && !comment?.trim()) {
|
|
6766
6834
|
throw Object.assign(new Error(`A comment is required for "${transition.label}".`), { statusCode: 400 });
|
|
6767
6835
|
}
|
|
6768
|
-
const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, req, db };
|
|
6836
|
+
const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, input, req, db };
|
|
6769
6837
|
for (const hook of workflow.hooks?.beforeTransition ?? []) await hook(hookContext);
|
|
6770
6838
|
const events = [
|
|
6771
6839
|
createLifecycleEvent({
|
|
@@ -6819,6 +6887,19 @@ async function transitionWorkflow(args) {
|
|
|
6819
6887
|
if (targetState.published) data.__published = working;
|
|
6820
6888
|
if (transition.unpublish) data.__published = null;
|
|
6821
6889
|
const next = await tx.update({ collection: collection.slug, id, data });
|
|
6890
|
+
if (transition.onTransition) {
|
|
6891
|
+
await transition.onTransition({
|
|
6892
|
+
transition,
|
|
6893
|
+
from: lockedMeta.state,
|
|
6894
|
+
to: transition.to,
|
|
6895
|
+
doc: locked,
|
|
6896
|
+
input,
|
|
6897
|
+
comment,
|
|
6898
|
+
user,
|
|
6899
|
+
req,
|
|
6900
|
+
tx
|
|
6901
|
+
});
|
|
6902
|
+
}
|
|
6822
6903
|
await tx.create({
|
|
6823
6904
|
collection: WORKFLOW_HISTORY_COLLECTION,
|
|
6824
6905
|
data: {
|
|
@@ -7458,6 +7539,7 @@ var CollectionController = class {
|
|
|
7458
7539
|
updatedBy: user?.sub ?? null
|
|
7459
7540
|
};
|
|
7460
7541
|
data = DefaultsService.apply(this.collection.fields, data);
|
|
7542
|
+
data = normalizeMoneyFields(this.collection.fields, data);
|
|
7461
7543
|
if (this.collection.workflow) {
|
|
7462
7544
|
data = initializeWorkflowDocument(data, this.collection.workflow);
|
|
7463
7545
|
}
|
|
@@ -7505,12 +7587,18 @@ var CollectionController = class {
|
|
|
7505
7587
|
if (!data.id) {
|
|
7506
7588
|
data = { ...data, id: generateDocumentId(this.collection) };
|
|
7507
7589
|
}
|
|
7590
|
+
const beforeCommit = this.buildBeforeCommit(c, "create", user);
|
|
7508
7591
|
const doc = this.collection.workflow ? (await createWorkflowDocument({
|
|
7509
7592
|
config,
|
|
7510
7593
|
collection: this.collection,
|
|
7511
7594
|
data,
|
|
7512
|
-
user
|
|
7513
|
-
|
|
7595
|
+
user,
|
|
7596
|
+
beforeCommit
|
|
7597
|
+
})).doc : await this.persistInTransaction(
|
|
7598
|
+
db,
|
|
7599
|
+
beforeCommit,
|
|
7600
|
+
(writer) => writer.create({ collection: this.collection.slug, data })
|
|
7601
|
+
);
|
|
7514
7602
|
if (this.collection.audit && db) {
|
|
7515
7603
|
AuditService.log(db, {
|
|
7516
7604
|
operation: "create",
|
|
@@ -7744,6 +7832,36 @@ var CollectionController = class {
|
|
|
7744
7832
|
const body = await c.req.json();
|
|
7745
7833
|
return this.performUpdate(c, id, body);
|
|
7746
7834
|
}
|
|
7835
|
+
/**
|
|
7836
|
+
* Builds the callback that runs the collection's `beforeCommit` hooks inside
|
|
7837
|
+
* the write transaction, or `undefined` when the collection has none.
|
|
7838
|
+
*/
|
|
7839
|
+
buildBeforeCommit(c, operation, user, previousDoc) {
|
|
7840
|
+
const hooks = this.collection.hooks?.beforeCommit;
|
|
7841
|
+
if (!hooks?.length) return void 0;
|
|
7842
|
+
return async (tx, doc) => {
|
|
7843
|
+
for (const hook of hooks) {
|
|
7844
|
+
await hook({ doc, previousDoc, req: this.toHookRequestContext(c), user, operation, tx });
|
|
7845
|
+
}
|
|
7846
|
+
};
|
|
7847
|
+
}
|
|
7848
|
+
/**
|
|
7849
|
+
* Runs a single write. When `beforeCommit` hooks exist the write and the
|
|
7850
|
+
* hooks share one transaction, so a throwing hook rolls the write back.
|
|
7851
|
+
*/
|
|
7852
|
+
async persistInTransaction(db, beforeCommit, write) {
|
|
7853
|
+
if (!beforeCommit) return write(db);
|
|
7854
|
+
if (!db.transaction) {
|
|
7855
|
+
throw new Error(
|
|
7856
|
+
`Collection "${this.collection.slug}" defines beforeCommit hooks but the configured database adapter does not support transactions.`
|
|
7857
|
+
);
|
|
7858
|
+
}
|
|
7859
|
+
return db.transaction(async (tx) => {
|
|
7860
|
+
const doc = await write(tx);
|
|
7861
|
+
await beforeCommit(tx, doc);
|
|
7862
|
+
return doc;
|
|
7863
|
+
});
|
|
7864
|
+
}
|
|
7747
7865
|
/**
|
|
7748
7866
|
* Core update pipeline shared by PATCH requests and operational actions:
|
|
7749
7867
|
* auth-field stripping, timestamps, access enforcement, field write access,
|
|
@@ -7785,6 +7903,7 @@ var CollectionController = class {
|
|
|
7785
7903
|
403
|
|
7786
7904
|
);
|
|
7787
7905
|
}
|
|
7906
|
+
data = normalizeMoneyFields(this.collection.fields, data);
|
|
7788
7907
|
let before = null;
|
|
7789
7908
|
if (this.collection.audit) {
|
|
7790
7909
|
before = originalDoc;
|
|
@@ -7818,14 +7937,21 @@ var CollectionController = class {
|
|
|
7818
7937
|
surface: "collection.beforeChange",
|
|
7819
7938
|
path: `collection:${this.collection.slug}.hooks.beforeChange`
|
|
7820
7939
|
});
|
|
7940
|
+
assertImmutableFieldsUnchanged(this.collection.fields, data, originalDoc);
|
|
7941
|
+
const beforeCommit = this.buildBeforeCommit(c, "update", user, originalDoc);
|
|
7821
7942
|
const doc = this.collection.workflow ? (await saveWorkflowDraft({
|
|
7822
7943
|
config,
|
|
7823
7944
|
collection: this.collection,
|
|
7824
7945
|
id,
|
|
7825
7946
|
originalDoc,
|
|
7826
7947
|
data,
|
|
7827
|
-
user
|
|
7828
|
-
|
|
7948
|
+
user,
|
|
7949
|
+
beforeCommit
|
|
7950
|
+
})).doc : await this.persistInTransaction(
|
|
7951
|
+
db,
|
|
7952
|
+
beforeCommit,
|
|
7953
|
+
(writer) => writer.update({ collection: this.collection.slug, id, data })
|
|
7954
|
+
);
|
|
7829
7955
|
if (this.collection.audit && db) {
|
|
7830
7956
|
AuditService.log(db, {
|
|
7831
7957
|
operation: "update",
|
|
@@ -8052,6 +8178,7 @@ var CollectionController = class {
|
|
|
8052
8178
|
transitionName,
|
|
8053
8179
|
expectedRevision: body.expectedRevision,
|
|
8054
8180
|
comment: body.comment,
|
|
8181
|
+
input: body.input,
|
|
8055
8182
|
user: c.get("user"),
|
|
8056
8183
|
req: { query: c.req.query(), headers: c.req.header(), raw: c.req.raw }
|
|
8057
8184
|
});
|
|
@@ -9383,6 +9510,20 @@ var MediaFolderController = class {
|
|
|
9383
9510
|
}
|
|
9384
9511
|
};
|
|
9385
9512
|
|
|
9513
|
+
// src/utils/admin-url.ts
|
|
9514
|
+
var DEFAULT_ADMIN_PATH = "/admin";
|
|
9515
|
+
function resolveAdminUrl(c, config, clientUrl) {
|
|
9516
|
+
const configured = config.admin?.adminUrl?.trim() || process.env.DYRECTED_ADMIN_URL?.trim() || (typeof clientUrl === "string" ? clientUrl.trim() : "") || DEFAULT_ADMIN_PATH;
|
|
9517
|
+
try {
|
|
9518
|
+
return new URL(configured, c.req.url).toString();
|
|
9519
|
+
} catch {
|
|
9520
|
+
return configured;
|
|
9521
|
+
}
|
|
9522
|
+
}
|
|
9523
|
+
function appendQueryParam(url, key, value) {
|
|
9524
|
+
return `${url}${url.includes("?") ? "&" : "?"}${key}=${encodeURIComponent(value)}`;
|
|
9525
|
+
}
|
|
9526
|
+
|
|
9386
9527
|
// src/controllers/auth.controller.ts
|
|
9387
9528
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
9388
9529
|
|
|
@@ -10058,8 +10199,7 @@ var AuthController = class {
|
|
|
10058
10199
|
},
|
|
10059
10200
|
"1h"
|
|
10060
10201
|
);
|
|
10061
|
-
const
|
|
10062
|
-
const url = resetUrl ? `${resetUrl}${resetUrl.includes("?") ? "&" : "?"}token=${encodeURIComponent(resetToken)}` : void 0;
|
|
10202
|
+
const url = appendQueryParam(resolveAdminUrl(c, config, body?.resetUrl), "token", resetToken);
|
|
10063
10203
|
try {
|
|
10064
10204
|
const { subject, html } = buildResetPasswordEmail(config, {
|
|
10065
10205
|
token: resetToken,
|
|
@@ -10191,8 +10331,7 @@ var AuthController = class {
|
|
|
10191
10331
|
},
|
|
10192
10332
|
"7d"
|
|
10193
10333
|
);
|
|
10194
|
-
const
|
|
10195
|
-
const url = inviteUrl ? `${inviteUrl}${inviteUrl.includes("?") ? "&" : "?"}inviteToken=${encodeURIComponent(inviteToken)}` : void 0;
|
|
10334
|
+
const url = appendQueryParam(resolveAdminUrl(c, config, body?.inviteUrl), "inviteToken", inviteToken);
|
|
10196
10335
|
try {
|
|
10197
10336
|
const { subject, html } = buildInviteEmail(config, {
|
|
10198
10337
|
token: inviteToken,
|
|
@@ -12471,7 +12610,12 @@ function generateOpenApi(config) {
|
|
|
12471
12610
|
type: "object",
|
|
12472
12611
|
properties: {
|
|
12473
12612
|
expectedRevision: { type: "integer", minimum: 1 },
|
|
12474
|
-
comment: { type: "string" }
|
|
12613
|
+
comment: { type: "string" },
|
|
12614
|
+
input: {
|
|
12615
|
+
type: "object",
|
|
12616
|
+
additionalProperties: true,
|
|
12617
|
+
description: "Extra data passed to the transition's onTransition handler."
|
|
12618
|
+
}
|
|
12475
12619
|
}
|
|
12476
12620
|
},
|
|
12477
12621
|
WorkflowHistoryEntry: {
|
|
@@ -13634,6 +13778,9 @@ function fieldToSchema(field) {
|
|
|
13634
13778
|
case "number":
|
|
13635
13779
|
schema = { type: "number" };
|
|
13636
13780
|
break;
|
|
13781
|
+
case "money":
|
|
13782
|
+
schema = { type: "integer", description: "Amount in minor units (for example kobo or cents)." };
|
|
13783
|
+
break;
|
|
13637
13784
|
case "boolean":
|
|
13638
13785
|
schema = { type: "boolean" };
|
|
13639
13786
|
break;
|
|
@@ -13774,6 +13921,537 @@ function getSwaggerHtml(specUrl) {
|
|
|
13774
13921
|
`;
|
|
13775
13922
|
}
|
|
13776
13923
|
|
|
13924
|
+
// src/types/navigation.ts
|
|
13925
|
+
var CURRENT_NAV_PREFERENCES_VERSION = 1;
|
|
13926
|
+
var DEFAULT_USER_NAV_PREFERENCES = {
|
|
13927
|
+
_version: CURRENT_NAV_PREFERENCES_VERSION,
|
|
13928
|
+
pinned: [],
|
|
13929
|
+
hidden: [],
|
|
13930
|
+
groupOrder: [],
|
|
13931
|
+
itemOrder: {},
|
|
13932
|
+
groups: [],
|
|
13933
|
+
items: []
|
|
13934
|
+
};
|
|
13935
|
+
|
|
13936
|
+
// src/utils/navigation-reconciler.ts
|
|
13937
|
+
var normalizeKey = (val) => (val || "").toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
13938
|
+
function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
13939
|
+
if (!baseTree) {
|
|
13940
|
+
return {
|
|
13941
|
+
groups: [],
|
|
13942
|
+
ungrouped: [],
|
|
13943
|
+
pinnedItems: []
|
|
13944
|
+
};
|
|
13945
|
+
}
|
|
13946
|
+
const includeHidden = options?.includeHidden ?? false;
|
|
13947
|
+
const effectivePrefs = prefs || DEFAULT_USER_NAV_PREFERENCES;
|
|
13948
|
+
const hiddenSet = new Set(effectivePrefs.hidden || []);
|
|
13949
|
+
const knownCollectionSlugs = new Set((schemas?.collections || []).map((c) => c.slug));
|
|
13950
|
+
const knownGlobalSlugs = new Set((schemas?.globals || []).map((g) => g.slug));
|
|
13951
|
+
const groupsList = [];
|
|
13952
|
+
const findGroup = (query) => {
|
|
13953
|
+
const qNorm = normalizeKey(query);
|
|
13954
|
+
return groupsList.find(
|
|
13955
|
+
(g) => g.id === query || g.slug === query || g.name === query || normalizeKey(g.id) === qNorm || normalizeKey(g.slug) === qNorm || normalizeKey(g.name) === qNorm
|
|
13956
|
+
);
|
|
13957
|
+
};
|
|
13958
|
+
for (const g of baseTree.groups || []) {
|
|
13959
|
+
groupsList.push({
|
|
13960
|
+
...g,
|
|
13961
|
+
items: (g.items || []).map((item) => ({ ...item }))
|
|
13962
|
+
});
|
|
13963
|
+
}
|
|
13964
|
+
for (const ug of effectivePrefs.groups || []) {
|
|
13965
|
+
const existing = findGroup(ug.slug || ug.name);
|
|
13966
|
+
if (!existing) {
|
|
13967
|
+
groupsList.push({
|
|
13968
|
+
id: ug.slug ? `group_${ug.slug}` : `group_${normalizeKey(ug.name)}`,
|
|
13969
|
+
name: ug.name,
|
|
13970
|
+
slug: ug.slug || normalizeKey(ug.name),
|
|
13971
|
+
icon: ug.icon || "Folder",
|
|
13972
|
+
defaultExpanded: ug.defaultExpanded ?? true,
|
|
13973
|
+
order: ug.order ?? 100,
|
|
13974
|
+
items: []
|
|
13975
|
+
});
|
|
13976
|
+
}
|
|
13977
|
+
}
|
|
13978
|
+
for (const itemOpt of effectivePrefs.items || []) {
|
|
13979
|
+
const targetGroup = typeof itemOpt.group === "object" ? itemOpt.group?.name : itemOpt.group;
|
|
13980
|
+
const groupKey = targetGroup || "Custom";
|
|
13981
|
+
let group = findGroup(groupKey);
|
|
13982
|
+
if (!group) {
|
|
13983
|
+
group = {
|
|
13984
|
+
id: `group_${normalizeKey(groupKey)}`,
|
|
13985
|
+
name: groupKey,
|
|
13986
|
+
slug: normalizeKey(groupKey),
|
|
13987
|
+
icon: "Folder",
|
|
13988
|
+
defaultExpanded: true,
|
|
13989
|
+
order: 99,
|
|
13990
|
+
items: []
|
|
13991
|
+
};
|
|
13992
|
+
groupsList.push(group);
|
|
13993
|
+
}
|
|
13994
|
+
const itemId = itemOpt.slug ? `workspace_${itemOpt.slug}` : itemOpt.collection ? `collection_${itemOpt.collection}` : `custom_${Math.random()}`;
|
|
13995
|
+
for (const otherGroup of groupsList) {
|
|
13996
|
+
if (otherGroup !== group) {
|
|
13997
|
+
otherGroup.items = otherGroup.items.filter((i) => i.slug !== itemOpt.slug && i.id !== itemId);
|
|
13998
|
+
}
|
|
13999
|
+
}
|
|
14000
|
+
const existingIndex = group.items.findIndex((i) => i.id === itemId || i.slug === itemOpt.slug);
|
|
14001
|
+
const existingItem = existingIndex >= 0 ? group.items[existingIndex] : void 0;
|
|
14002
|
+
const col = schemas?.collections?.find((c) => c.slug === itemOpt.collection);
|
|
14003
|
+
const colViews = col?.views || [];
|
|
14004
|
+
const baseViews = existingItem?.views || [];
|
|
14005
|
+
const userViews = itemOpt.views || [];
|
|
14006
|
+
const userViewSlugs = new Set(userViews.map((v) => v.slug));
|
|
14007
|
+
let mergedViews = [
|
|
14008
|
+
...baseViews.filter((v) => !userViewSlugs.has(v.slug)),
|
|
14009
|
+
...userViews
|
|
14010
|
+
];
|
|
14011
|
+
if (mergedViews.length === 0 && itemOpt.collection) {
|
|
14012
|
+
if (colViews.length > 0) {
|
|
14013
|
+
mergedViews = [...colViews];
|
|
14014
|
+
} else {
|
|
14015
|
+
mergedViews = [
|
|
14016
|
+
{
|
|
14017
|
+
slug: "default",
|
|
14018
|
+
label: col?.label || col?.labels?.plural || itemOpt.label || "All Records",
|
|
14019
|
+
layout: "table",
|
|
14020
|
+
collection: itemOpt.collection
|
|
14021
|
+
}
|
|
14022
|
+
];
|
|
14023
|
+
}
|
|
14024
|
+
}
|
|
14025
|
+
mergedViews = mergedViews.map((v) => ({
|
|
14026
|
+
...v,
|
|
14027
|
+
collection: v.collection || itemOpt.collection || existingItem?.collection
|
|
14028
|
+
}));
|
|
14029
|
+
const compiledItem = {
|
|
14030
|
+
id: itemId,
|
|
14031
|
+
type: itemOpt.slug ? "workspace" : itemOpt.collection ? "collection" : itemOpt.global ? "global" : "workspace",
|
|
14032
|
+
slug: itemOpt.slug || itemOpt.collection || itemOpt.global || "item",
|
|
14033
|
+
label: itemOpt.label || itemOpt.slug || "Item",
|
|
14034
|
+
icon: itemOpt.icon || existingItem?.icon || col?.admin?.icon || "Briefcase",
|
|
14035
|
+
group: group.name,
|
|
14036
|
+
order: itemOpt.order ?? existingItem?.order ?? 100,
|
|
14037
|
+
views: mergedViews,
|
|
14038
|
+
collection: itemOpt.collection || existingItem?.collection,
|
|
14039
|
+
global: itemOpt.global || existingItem?.global,
|
|
14040
|
+
badge: itemOpt.badge || existingItem?.badge,
|
|
14041
|
+
access: itemOpt.access || existingItem?.access
|
|
14042
|
+
};
|
|
14043
|
+
if (existingIndex >= 0) {
|
|
14044
|
+
group.items[existingIndex] = compiledItem;
|
|
14045
|
+
} else {
|
|
14046
|
+
group.items.push(compiledItem);
|
|
14047
|
+
}
|
|
14048
|
+
}
|
|
14049
|
+
const allAvailableItems = [];
|
|
14050
|
+
const reconciledGroups = [];
|
|
14051
|
+
for (const group of groupsList) {
|
|
14052
|
+
const isGroupHidden = hiddenSet.has(group.id) || !!group.slug && hiddenSet.has(group.slug) || hiddenSet.has(group.name);
|
|
14053
|
+
if (!includeHidden && isGroupHidden) {
|
|
14054
|
+
continue;
|
|
14055
|
+
}
|
|
14056
|
+
const visibleItems = [];
|
|
14057
|
+
for (const item of group.items) {
|
|
14058
|
+
const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
|
|
14059
|
+
if (!includeHidden && isItemHidden) {
|
|
14060
|
+
continue;
|
|
14061
|
+
}
|
|
14062
|
+
const allViews = item.views || [];
|
|
14063
|
+
const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
|
|
14064
|
+
const compoundKey = `${item.slug}_${v.slug}`;
|
|
14065
|
+
return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
|
|
14066
|
+
});
|
|
14067
|
+
let isTombstone = false;
|
|
14068
|
+
if (schemas && schemas.collections && schemas.collections.length > 0) {
|
|
14069
|
+
if (item.collection && !knownCollectionSlugs.has(item.collection)) {
|
|
14070
|
+
isTombstone = true;
|
|
14071
|
+
}
|
|
14072
|
+
}
|
|
14073
|
+
if (schemas && schemas.globals && schemas.globals.length > 0) {
|
|
14074
|
+
if (item.global && !knownGlobalSlugs.has(item.global)) {
|
|
14075
|
+
isTombstone = true;
|
|
14076
|
+
}
|
|
14077
|
+
}
|
|
14078
|
+
const processedItem = {
|
|
14079
|
+
...item,
|
|
14080
|
+
views: visibleViews,
|
|
14081
|
+
...isTombstone ? { isTombstone: true } : {}
|
|
14082
|
+
};
|
|
14083
|
+
visibleItems.push(processedItem);
|
|
14084
|
+
allAvailableItems.push(processedItem);
|
|
14085
|
+
}
|
|
14086
|
+
const groupKey = group.slug || group.name;
|
|
14087
|
+
const itemOrderList = effectivePrefs.itemOrder?.[group.id] || effectivePrefs.itemOrder?.[groupKey];
|
|
14088
|
+
if (itemOrderList && itemOrderList.length > 0) {
|
|
14089
|
+
visibleItems.sort((a, b) => {
|
|
14090
|
+
const aIdx = itemOrderList.indexOf(a.id) >= 0 ? itemOrderList.indexOf(a.id) : a.slug ? itemOrderList.indexOf(a.slug) : -1;
|
|
14091
|
+
const bIdx = itemOrderList.indexOf(b.id) >= 0 ? itemOrderList.indexOf(b.id) : b.slug ? itemOrderList.indexOf(b.slug) : -1;
|
|
14092
|
+
if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
|
|
14093
|
+
if (aIdx >= 0) return -1;
|
|
14094
|
+
if (bIdx >= 0) return 1;
|
|
14095
|
+
return (a.order ?? 100) - (b.order ?? 100);
|
|
14096
|
+
});
|
|
14097
|
+
} else {
|
|
14098
|
+
visibleItems.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
|
|
14099
|
+
}
|
|
14100
|
+
reconciledGroups.push({
|
|
14101
|
+
...group,
|
|
14102
|
+
items: visibleItems
|
|
14103
|
+
});
|
|
14104
|
+
}
|
|
14105
|
+
if (effectivePrefs.groupOrder && effectivePrefs.groupOrder.length > 0) {
|
|
14106
|
+
reconciledGroups.sort((a, b) => {
|
|
14107
|
+
const aIdx = effectivePrefs.groupOrder.indexOf(a.id) >= 0 ? effectivePrefs.groupOrder.indexOf(a.id) : a.slug ? effectivePrefs.groupOrder.indexOf(a.slug) : effectivePrefs.groupOrder.indexOf(a.name);
|
|
14108
|
+
const bIdx = effectivePrefs.groupOrder.indexOf(b.id) >= 0 ? effectivePrefs.groupOrder.indexOf(b.id) : b.slug ? effectivePrefs.groupOrder.indexOf(b.slug) : effectivePrefs.groupOrder.indexOf(b.name);
|
|
14109
|
+
if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
|
|
14110
|
+
if (aIdx >= 0) return -1;
|
|
14111
|
+
if (bIdx >= 0) return 1;
|
|
14112
|
+
return (a.order ?? 100) - (b.order ?? 100);
|
|
14113
|
+
});
|
|
14114
|
+
} else {
|
|
14115
|
+
reconciledGroups.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
|
|
14116
|
+
}
|
|
14117
|
+
const reconciledUngrouped = (baseTree.ungrouped || []).filter((item) => {
|
|
14118
|
+
const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
|
|
14119
|
+
return includeHidden || !isItemHidden;
|
|
14120
|
+
}).map((item) => {
|
|
14121
|
+
const allViews = item.views || [];
|
|
14122
|
+
const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
|
|
14123
|
+
const compoundKey = `${item.slug}_${v.slug}`;
|
|
14124
|
+
return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
|
|
14125
|
+
});
|
|
14126
|
+
const processed = { ...item, views: visibleViews };
|
|
14127
|
+
allAvailableItems.push(processed);
|
|
14128
|
+
return processed;
|
|
14129
|
+
});
|
|
14130
|
+
const pinnedItems = [];
|
|
14131
|
+
for (const pin of effectivePrefs.pinned || []) {
|
|
14132
|
+
const match = allAvailableItems.find((i) => i.slug === pin.slug || i.id === pin.slug);
|
|
14133
|
+
if (match) {
|
|
14134
|
+
pinnedItems.push({
|
|
14135
|
+
...match,
|
|
14136
|
+
id: `pin_${match.id}`,
|
|
14137
|
+
label: pin.label || match.label,
|
|
14138
|
+
icon: pin.icon || match.icon
|
|
14139
|
+
});
|
|
14140
|
+
} else {
|
|
14141
|
+
pinnedItems.push({
|
|
14142
|
+
id: `pin_${pin.slug}`,
|
|
14143
|
+
type: pin.type,
|
|
14144
|
+
slug: pin.slug,
|
|
14145
|
+
label: pin.label || pin.slug,
|
|
14146
|
+
icon: pin.icon || "Star",
|
|
14147
|
+
order: 0,
|
|
14148
|
+
views: []
|
|
14149
|
+
});
|
|
14150
|
+
}
|
|
14151
|
+
}
|
|
14152
|
+
return {
|
|
14153
|
+
groups: reconciledGroups,
|
|
14154
|
+
ungrouped: reconciledUngrouped,
|
|
14155
|
+
pinnedItems
|
|
14156
|
+
};
|
|
14157
|
+
}
|
|
14158
|
+
|
|
14159
|
+
// src/utils/navigation.ts
|
|
14160
|
+
var RESERVED_NAVIGATION_SLUGS = /* @__PURE__ */ new Set([
|
|
14161
|
+
"collections",
|
|
14162
|
+
"globals",
|
|
14163
|
+
"setup",
|
|
14164
|
+
"api",
|
|
14165
|
+
"login",
|
|
14166
|
+
"logout",
|
|
14167
|
+
"settings",
|
|
14168
|
+
"admin",
|
|
14169
|
+
"preferences",
|
|
14170
|
+
"auth"
|
|
14171
|
+
]);
|
|
14172
|
+
function assertValidNavigationSlugs(navigation) {
|
|
14173
|
+
for (const item of navigation) {
|
|
14174
|
+
if (item.slug) {
|
|
14175
|
+
const normalizedSlug = item.slug.toLowerCase().trim().replace(/^\//, "");
|
|
14176
|
+
if (RESERVED_NAVIGATION_SLUGS.has(normalizedSlug)) {
|
|
14177
|
+
throw new Error(
|
|
14178
|
+
`[Dyrected Config] The slug '${item.slug}' in navigation item '${item.label || item.slug}' is a reserved system path and cannot be used as an operational workspace slug.`
|
|
14179
|
+
);
|
|
14180
|
+
}
|
|
14181
|
+
}
|
|
14182
|
+
}
|
|
14183
|
+
}
|
|
14184
|
+
function resolveGroupMeta(groupProp, defaultGroupName = "Collections") {
|
|
14185
|
+
if (!groupProp) {
|
|
14186
|
+
return { name: defaultGroupName };
|
|
14187
|
+
}
|
|
14188
|
+
if (typeof groupProp === "string") {
|
|
14189
|
+
return { name: groupProp };
|
|
14190
|
+
}
|
|
14191
|
+
return { name: groupProp.name, meta: groupProp };
|
|
14192
|
+
}
|
|
14193
|
+
function compileNavigation(config) {
|
|
14194
|
+
const collections = config.collections || [];
|
|
14195
|
+
const globals = config.globals || [];
|
|
14196
|
+
const explicitNav = config.admin?.navigation || [];
|
|
14197
|
+
assertValidNavigationSlugs(explicitNav);
|
|
14198
|
+
const collectionsBySlug = /* @__PURE__ */ new Map();
|
|
14199
|
+
for (const col of collections) {
|
|
14200
|
+
collectionsBySlug.set(col.slug, col);
|
|
14201
|
+
}
|
|
14202
|
+
const globalsBySlug = /* @__PURE__ */ new Map();
|
|
14203
|
+
for (const gl of globals) {
|
|
14204
|
+
globalsBySlug.set(gl.slug, gl);
|
|
14205
|
+
}
|
|
14206
|
+
const mentionedCollections = /* @__PURE__ */ new Set();
|
|
14207
|
+
const mentionedGlobals = /* @__PURE__ */ new Set();
|
|
14208
|
+
const injectedViewsByCollection = /* @__PURE__ */ new Map();
|
|
14209
|
+
for (const item of explicitNav) {
|
|
14210
|
+
if (item.addToCollection) {
|
|
14211
|
+
const existing = injectedViewsByCollection.get(item.addToCollection) || [];
|
|
14212
|
+
if (item.views && item.views.length > 0) {
|
|
14213
|
+
injectedViewsByCollection.set(item.addToCollection, [...existing, ...item.views]);
|
|
14214
|
+
}
|
|
14215
|
+
}
|
|
14216
|
+
if (item.collection) mentionedCollections.add(item.collection);
|
|
14217
|
+
if (item.global) mentionedGlobals.add(item.global);
|
|
14218
|
+
}
|
|
14219
|
+
const groupMetadataMap = /* @__PURE__ */ new Map();
|
|
14220
|
+
const itemsByGroup = /* @__PURE__ */ new Map();
|
|
14221
|
+
const ungroupedItems = [];
|
|
14222
|
+
const registerItem = (groupName, item) => {
|
|
14223
|
+
if (!groupName) {
|
|
14224
|
+
ungroupedItems.push(item);
|
|
14225
|
+
} else {
|
|
14226
|
+
const list = itemsByGroup.get(groupName) || [];
|
|
14227
|
+
list.push(item);
|
|
14228
|
+
itemsByGroup.set(groupName, list);
|
|
14229
|
+
}
|
|
14230
|
+
};
|
|
14231
|
+
for (const item of explicitNav) {
|
|
14232
|
+
if (item.addToCollection && !item.slug && !item.collection && !item.global && !item.dashboard && !item.href) {
|
|
14233
|
+
continue;
|
|
14234
|
+
}
|
|
14235
|
+
const { name: groupName, meta: groupMeta } = resolveGroupMeta(item.group, void 0);
|
|
14236
|
+
if (groupName && groupMeta) {
|
|
14237
|
+
groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
|
|
14238
|
+
}
|
|
14239
|
+
if (item.dashboard) {
|
|
14240
|
+
registerItem(groupName, {
|
|
14241
|
+
id: "dashboard",
|
|
14242
|
+
type: "dashboard",
|
|
14243
|
+
slug: "dashboard",
|
|
14244
|
+
label: item.label || "Dashboard",
|
|
14245
|
+
icon: item.icon || "LayoutDashboard",
|
|
14246
|
+
group: groupName,
|
|
14247
|
+
order: item.order ?? 0,
|
|
14248
|
+
views: [],
|
|
14249
|
+
access: item.access
|
|
14250
|
+
});
|
|
14251
|
+
continue;
|
|
14252
|
+
}
|
|
14253
|
+
if (item.collection) {
|
|
14254
|
+
const col = collectionsBySlug.get(item.collection);
|
|
14255
|
+
const combinedViews = [
|
|
14256
|
+
...col?.views || [],
|
|
14257
|
+
...injectedViewsByCollection.get(item.collection) || [],
|
|
14258
|
+
...item.views || []
|
|
14259
|
+
];
|
|
14260
|
+
registerItem(groupName, {
|
|
14261
|
+
id: item.slug || `collection_${item.collection}`,
|
|
14262
|
+
type: item.slug ? "workspace" : "collection",
|
|
14263
|
+
slug: item.slug || item.collection,
|
|
14264
|
+
collection: item.collection,
|
|
14265
|
+
label: item.label || col?.labels?.plural || item.collection,
|
|
14266
|
+
icon: item.icon || col?.admin?.icon || "Folder",
|
|
14267
|
+
group: groupName,
|
|
14268
|
+
order: item.order ?? col?.admin?.order ?? 100,
|
|
14269
|
+
views: combinedViews,
|
|
14270
|
+
badge: item.badge,
|
|
14271
|
+
access: item.access || col?.access?.read
|
|
14272
|
+
});
|
|
14273
|
+
continue;
|
|
14274
|
+
}
|
|
14275
|
+
if (item.global) {
|
|
14276
|
+
const gl = globalsBySlug.get(item.global);
|
|
14277
|
+
registerItem(groupName, {
|
|
14278
|
+
id: item.slug || `global_${item.global}`,
|
|
14279
|
+
type: "global",
|
|
14280
|
+
slug: item.slug || item.global,
|
|
14281
|
+
global: item.global,
|
|
14282
|
+
label: item.label || gl?.label || item.global,
|
|
14283
|
+
icon: item.icon || "Settings",
|
|
14284
|
+
group: groupName,
|
|
14285
|
+
order: item.order ?? 100,
|
|
14286
|
+
views: [],
|
|
14287
|
+
badge: item.badge,
|
|
14288
|
+
access: item.access
|
|
14289
|
+
});
|
|
14290
|
+
continue;
|
|
14291
|
+
}
|
|
14292
|
+
if (item.href) {
|
|
14293
|
+
registerItem(groupName, {
|
|
14294
|
+
id: `link_${item.slug || item.href}`,
|
|
14295
|
+
type: "link",
|
|
14296
|
+
slug: item.slug || "link",
|
|
14297
|
+
label: item.label || "Link",
|
|
14298
|
+
href: item.href,
|
|
14299
|
+
icon: item.icon || "ExternalLink",
|
|
14300
|
+
group: groupName,
|
|
14301
|
+
order: item.order ?? 100,
|
|
14302
|
+
views: [],
|
|
14303
|
+
badge: item.badge,
|
|
14304
|
+
access: item.access
|
|
14305
|
+
});
|
|
14306
|
+
continue;
|
|
14307
|
+
}
|
|
14308
|
+
const workspaceSlug = item.slug || "workspace";
|
|
14309
|
+
registerItem(groupName, {
|
|
14310
|
+
id: `workspace_${workspaceSlug}`,
|
|
14311
|
+
type: "workspace",
|
|
14312
|
+
slug: workspaceSlug,
|
|
14313
|
+
label: item.label || workspaceSlug,
|
|
14314
|
+
icon: item.icon || "Briefcase",
|
|
14315
|
+
group: groupName,
|
|
14316
|
+
order: item.order ?? 100,
|
|
14317
|
+
views: item.views || [],
|
|
14318
|
+
badge: item.badge,
|
|
14319
|
+
access: item.access
|
|
14320
|
+
});
|
|
14321
|
+
}
|
|
14322
|
+
for (const col of collections) {
|
|
14323
|
+
if (mentionedCollections.has(col.slug)) continue;
|
|
14324
|
+
if (col.admin?.hidden) continue;
|
|
14325
|
+
const groupProp = col.admin?.group;
|
|
14326
|
+
const { name: groupName, meta: groupMeta } = resolveGroupMeta(groupProp, "Collections");
|
|
14327
|
+
if (groupMeta) {
|
|
14328
|
+
groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
|
|
14329
|
+
}
|
|
14330
|
+
const combinedViews = [
|
|
14331
|
+
...col.views || [],
|
|
14332
|
+
...injectedViewsByCollection.get(col.slug) || []
|
|
14333
|
+
];
|
|
14334
|
+
registerItem(groupName, {
|
|
14335
|
+
id: `collection_${col.slug}`,
|
|
14336
|
+
type: "collection",
|
|
14337
|
+
slug: col.slug,
|
|
14338
|
+
collection: col.slug,
|
|
14339
|
+
label: col.labels?.plural || col.slug,
|
|
14340
|
+
icon: col.admin?.icon || "Folder",
|
|
14341
|
+
group: groupName,
|
|
14342
|
+
order: col?.admin?.order ?? 100,
|
|
14343
|
+
views: combinedViews,
|
|
14344
|
+
access: col.access?.read
|
|
14345
|
+
});
|
|
14346
|
+
}
|
|
14347
|
+
for (const gl of globals) {
|
|
14348
|
+
if (mentionedGlobals.has(gl.slug)) continue;
|
|
14349
|
+
const groupName = "Configuration";
|
|
14350
|
+
registerItem(groupName, {
|
|
14351
|
+
id: `global_${gl.slug}`,
|
|
14352
|
+
type: "global",
|
|
14353
|
+
slug: gl.slug,
|
|
14354
|
+
global: gl.slug,
|
|
14355
|
+
label: gl.label || gl.slug,
|
|
14356
|
+
icon: "Settings",
|
|
14357
|
+
group: groupName,
|
|
14358
|
+
order: 100,
|
|
14359
|
+
views: []
|
|
14360
|
+
});
|
|
14361
|
+
}
|
|
14362
|
+
const sortAndSpliceItems = (items) => {
|
|
14363
|
+
const sorted = [...items].sort((a, b) => a.order - b.order);
|
|
14364
|
+
const explicitMap = /* @__PURE__ */ new Map();
|
|
14365
|
+
for (const opt of explicitNav) {
|
|
14366
|
+
if (opt.slug) explicitMap.set(opt.slug, opt);
|
|
14367
|
+
if (opt.collection) explicitMap.set(opt.collection, opt);
|
|
14368
|
+
if (opt.global) explicitMap.set(opt.global, opt);
|
|
14369
|
+
}
|
|
14370
|
+
for (const opt of explicitNav) {
|
|
14371
|
+
const itemKey = opt.slug || opt.collection || opt.global;
|
|
14372
|
+
if (!itemKey) continue;
|
|
14373
|
+
if (opt.after) {
|
|
14374
|
+
const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
|
|
14375
|
+
const targetIdx = sorted.findIndex((i) => i.slug === opt.after);
|
|
14376
|
+
if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
|
|
14377
|
+
const [moved] = sorted.splice(itemIdx, 1);
|
|
14378
|
+
const newTargetIdx = sorted.findIndex((i) => i.slug === opt.after);
|
|
14379
|
+
sorted.splice(newTargetIdx + 1, 0, moved);
|
|
14380
|
+
}
|
|
14381
|
+
} else if (opt.before) {
|
|
14382
|
+
const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
|
|
14383
|
+
const targetIdx = sorted.findIndex((i) => i.slug === opt.before);
|
|
14384
|
+
if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
|
|
14385
|
+
const [moved] = sorted.splice(itemIdx, 1);
|
|
14386
|
+
const newTargetIdx = sorted.findIndex((i) => i.slug === opt.before);
|
|
14387
|
+
sorted.splice(newTargetIdx, 0, moved);
|
|
14388
|
+
}
|
|
14389
|
+
} else if (opt.position === "first") {
|
|
14390
|
+
const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
|
|
14391
|
+
if (itemIdx > 0) {
|
|
14392
|
+
const [moved] = sorted.splice(itemIdx, 1);
|
|
14393
|
+
sorted.unshift(moved);
|
|
14394
|
+
}
|
|
14395
|
+
} else if (opt.position === "last") {
|
|
14396
|
+
const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
|
|
14397
|
+
if (itemIdx !== -1 && itemIdx < sorted.length - 1) {
|
|
14398
|
+
const [moved] = sorted.splice(itemIdx, 1);
|
|
14399
|
+
sorted.push(moved);
|
|
14400
|
+
}
|
|
14401
|
+
}
|
|
14402
|
+
}
|
|
14403
|
+
return sorted;
|
|
14404
|
+
};
|
|
14405
|
+
const compiledGroups = [];
|
|
14406
|
+
for (const [groupName, rawItems] of itemsByGroup.entries()) {
|
|
14407
|
+
const meta = groupMetadataMap.get(groupName);
|
|
14408
|
+
const sortedItems = sortAndSpliceItems(rawItems);
|
|
14409
|
+
compiledGroups.push({
|
|
14410
|
+
id: `group_${groupName.toLowerCase().replace(/\s+/g, "_")}`,
|
|
14411
|
+
name: groupName,
|
|
14412
|
+
slug: meta?.slug || groupName.toLowerCase().replace(/\s+/g, "-"),
|
|
14413
|
+
icon: meta?.icon,
|
|
14414
|
+
defaultExpanded: meta?.defaultExpanded ?? true,
|
|
14415
|
+
order: meta?.order ?? 100,
|
|
14416
|
+
items: sortedItems
|
|
14417
|
+
});
|
|
14418
|
+
}
|
|
14419
|
+
compiledGroups.sort((a, b) => a.order - b.order);
|
|
14420
|
+
return {
|
|
14421
|
+
groups: compiledGroups,
|
|
14422
|
+
ungrouped: sortAndSpliceItems(ungroupedItems)
|
|
14423
|
+
};
|
|
14424
|
+
}
|
|
14425
|
+
function pruneNavigationForUser(tree, user) {
|
|
14426
|
+
const userRoles = /* @__PURE__ */ new Set();
|
|
14427
|
+
if (user?.role) userRoles.add(user.role);
|
|
14428
|
+
if (Array.isArray(user?.roles)) {
|
|
14429
|
+
for (const r of user.roles) userRoles.add(r);
|
|
14430
|
+
}
|
|
14431
|
+
const isAllowed = (item) => {
|
|
14432
|
+
if (!item.access) return true;
|
|
14433
|
+
if (Array.isArray(item.access)) {
|
|
14434
|
+
if (item.access.length === 0) return true;
|
|
14435
|
+
return item.access.some((role) => userRoles.has(role));
|
|
14436
|
+
}
|
|
14437
|
+
return true;
|
|
14438
|
+
};
|
|
14439
|
+
const prunedGroups = [];
|
|
14440
|
+
for (const group of tree.groups) {
|
|
14441
|
+
const visibleItems = group.items.filter(isAllowed);
|
|
14442
|
+
if (visibleItems.length > 0) {
|
|
14443
|
+
prunedGroups.push({
|
|
14444
|
+
...group,
|
|
14445
|
+
items: visibleItems
|
|
14446
|
+
});
|
|
14447
|
+
}
|
|
14448
|
+
}
|
|
14449
|
+
return {
|
|
14450
|
+
groups: prunedGroups,
|
|
14451
|
+
ungrouped: tree.ungrouped.filter(isAllowed)
|
|
14452
|
+
};
|
|
14453
|
+
}
|
|
14454
|
+
|
|
13777
14455
|
// src/router.ts
|
|
13778
14456
|
init_access_control();
|
|
13779
14457
|
import { HTTPException as HTTPException5 } from "hono/http-exception";
|
|
@@ -13922,6 +14600,30 @@ function serializeDetailForApi(detail) {
|
|
|
13922
14600
|
if (!detail || !Array.isArray(detail)) return void 0;
|
|
13923
14601
|
return detail.map(serializeDetailItemForApi);
|
|
13924
14602
|
}
|
|
14603
|
+
async function serializeActionsForApi(actions, serializeAccess) {
|
|
14604
|
+
const serializeAccessConfig = async (access) => {
|
|
14605
|
+
if (!access) return void 0;
|
|
14606
|
+
const serialized = {};
|
|
14607
|
+
for (const key of ["read", "create", "update", "delete"]) {
|
|
14608
|
+
if (access[key] !== void 0) serialized[key] = await serializeAccess(access[key]);
|
|
14609
|
+
}
|
|
14610
|
+
return serialized;
|
|
14611
|
+
};
|
|
14612
|
+
return Promise.all(
|
|
14613
|
+
(actions || []).map(async (action) => ({
|
|
14614
|
+
name: action.name,
|
|
14615
|
+
label: action.label,
|
|
14616
|
+
icon: action.icon,
|
|
14617
|
+
type: action.type ?? "row",
|
|
14618
|
+
confirm: action.confirm,
|
|
14619
|
+
submitLabel: action.submitLabel,
|
|
14620
|
+
fields: action.fields?.map(serializeFieldForApi),
|
|
14621
|
+
mutation: action.mutation,
|
|
14622
|
+
// Self-hosted handlers are intentionally omitted: they never leave the server.
|
|
14623
|
+
access: action.access ? await serializeAccessConfig(action.access) : void 0
|
|
14624
|
+
}))
|
|
14625
|
+
);
|
|
14626
|
+
}
|
|
13925
14627
|
async function serializeViewForApi(view, serializeAccess) {
|
|
13926
14628
|
if (!view) return view;
|
|
13927
14629
|
const serializeAccessConfig = async (access) => {
|
|
@@ -13948,20 +14650,7 @@ async function serializeViewForApi(view, serializeAccess) {
|
|
|
13948
14650
|
features: view.features,
|
|
13949
14651
|
actionOrder: view.actionOrder,
|
|
13950
14652
|
components: view.components,
|
|
13951
|
-
actions: await
|
|
13952
|
-
(view.actions || []).map(async (action) => ({
|
|
13953
|
-
name: action.name,
|
|
13954
|
-
label: action.label,
|
|
13955
|
-
icon: action.icon,
|
|
13956
|
-
type: action.type ?? "row",
|
|
13957
|
-
confirm: action.confirm,
|
|
13958
|
-
submitLabel: action.submitLabel,
|
|
13959
|
-
fields: action.fields?.map(serializeFieldForApi),
|
|
13960
|
-
mutation: action.mutation,
|
|
13961
|
-
// Self-hosted handlers are intentionally omitted: they never leave the server.
|
|
13962
|
-
access: action.access ? await serializeAccessConfig(action.access) : void 0
|
|
13963
|
-
}))
|
|
13964
|
-
),
|
|
14653
|
+
actions: await serializeActionsForApi(view.actions, serializeAccess),
|
|
13965
14654
|
access: view.access ? await serializeAccessConfig(view.access) : void 0
|
|
13966
14655
|
};
|
|
13967
14656
|
}
|
|
@@ -14043,6 +14732,7 @@ function registerRoutes(app, config) {
|
|
|
14043
14732
|
drafts: !!col.drafts,
|
|
14044
14733
|
defaultView: col.defaultView ?? col.admin?.defaultView,
|
|
14045
14734
|
views: await Promise.all((col.views || []).map((view) => serializeViewForApi(view, serializeAccess))),
|
|
14735
|
+
actions: await serializeActionsForApi(col.actions, serializeAccess),
|
|
14046
14736
|
admin: col.admin,
|
|
14047
14737
|
detail: serializeDetailForApi(col.detail),
|
|
14048
14738
|
workflow: col.workflow ? {
|
|
@@ -14273,6 +14963,7 @@ function registerRoutes(app, config) {
|
|
|
14273
14963
|
const user = c.get("user");
|
|
14274
14964
|
const key = c.req.param("key");
|
|
14275
14965
|
const scope = c.req.query("scope");
|
|
14966
|
+
const roleParam = c.req.query("role");
|
|
14276
14967
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
14277
14968
|
if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
|
|
14278
14969
|
const getGlobalPreference = async () => {
|
|
@@ -14282,27 +14973,61 @@ function registerRoutes(app, config) {
|
|
|
14282
14973
|
});
|
|
14283
14974
|
return globalDoc ? globalDoc.value : null;
|
|
14284
14975
|
};
|
|
14285
|
-
|
|
14976
|
+
const getRolePreference = async (r) => {
|
|
14977
|
+
const roleDoc = await db.findOne({
|
|
14978
|
+
collection: "__role_preferences",
|
|
14979
|
+
id: `${r}_${key}`
|
|
14980
|
+
});
|
|
14981
|
+
return roleDoc ? roleDoc.value : null;
|
|
14982
|
+
};
|
|
14983
|
+
const getUserPreferences = async () => {
|
|
14984
|
+
if (!user?.collection || !user.sub) return null;
|
|
14985
|
+
const doc = await db.findOne({ collection: user.collection, id: user.sub });
|
|
14986
|
+
if (!doc) return null;
|
|
14987
|
+
return typeof doc.__preferences === "object" && doc.__preferences !== null ? doc.__preferences : {};
|
|
14988
|
+
};
|
|
14989
|
+
if (scope === "global") {
|
|
14286
14990
|
const globalValue2 = await getGlobalPreference();
|
|
14287
14991
|
return c.json({ key, value: globalValue2 });
|
|
14288
14992
|
}
|
|
14289
|
-
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14993
|
+
if (scope === "role") {
|
|
14994
|
+
if (roleParam) {
|
|
14995
|
+
const val = await getRolePreference(roleParam);
|
|
14996
|
+
return c.json({ key, value: val, role: roleParam });
|
|
14997
|
+
}
|
|
14998
|
+
const userRoles2 = Array.isArray(user?.roles) ? user.roles : typeof user?.role === "string" ? [user.role] : [];
|
|
14999
|
+
for (const r of userRoles2) {
|
|
15000
|
+
const val = await getRolePreference(r);
|
|
15001
|
+
if (val !== null && val !== void 0) {
|
|
15002
|
+
return c.json({ key, value: val, role: r });
|
|
15003
|
+
}
|
|
15004
|
+
}
|
|
15005
|
+
return c.json({ key, value: null });
|
|
14293
15006
|
}
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
return c.json({ key, value:
|
|
15007
|
+
if (scope === "personal") {
|
|
15008
|
+
const prefs2 = await getUserPreferences();
|
|
15009
|
+
return c.json({ key, value: prefs2 && key in prefs2 ? prefs2[key] ?? null : null });
|
|
15010
|
+
}
|
|
15011
|
+
const prefs = await getUserPreferences();
|
|
15012
|
+
if (prefs && key in prefs && prefs[key] !== null && prefs[key] !== void 0) {
|
|
15013
|
+
return c.json({ key, value: prefs[key] });
|
|
15014
|
+
}
|
|
15015
|
+
const userRoles = Array.isArray(user?.roles) ? user.roles : typeof user?.role === "string" ? [user.role] : [];
|
|
15016
|
+
for (const r of userRoles) {
|
|
15017
|
+
const val = await getRolePreference(r);
|
|
15018
|
+
if (val !== null && val !== void 0) {
|
|
15019
|
+
return c.json({ key, value: val });
|
|
15020
|
+
}
|
|
14297
15021
|
}
|
|
14298
15022
|
const globalValue = await getGlobalPreference();
|
|
14299
|
-
return c.json({ key, value: globalValue });
|
|
15023
|
+
return c.json({ key, value: globalValue ?? null });
|
|
14300
15024
|
});
|
|
14301
15025
|
app.put("/api/preferences/:key", requireAuth(config), async (c) => {
|
|
14302
15026
|
const db = config.db;
|
|
14303
15027
|
const user = c.get("user");
|
|
14304
15028
|
const key = c.req.param("key");
|
|
14305
|
-
const scope = c.req.query("scope");
|
|
15029
|
+
const scope = c.req.query("scope") || "personal";
|
|
15030
|
+
const roleParam = c.req.query("role");
|
|
14306
15031
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
14307
15032
|
if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
|
|
14308
15033
|
if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
|
|
@@ -14335,7 +15060,47 @@ function registerRoutes(app, config) {
|
|
|
14335
15060
|
data: { id: key, value: body.value }
|
|
14336
15061
|
});
|
|
14337
15062
|
}
|
|
14338
|
-
return c.json({ key, value: body.value });
|
|
15063
|
+
return c.json({ key, value: body.value, scope: "global" });
|
|
15064
|
+
}
|
|
15065
|
+
if (scope === "role") {
|
|
15066
|
+
const userCol = config.collections?.find((col) => col.slug === user?.collection);
|
|
15067
|
+
const isAdminUser = isUserAdmin(user, userCol);
|
|
15068
|
+
if (!isAdminUser) {
|
|
15069
|
+
return c.json(
|
|
15070
|
+
{
|
|
15071
|
+
error: true,
|
|
15072
|
+
message: "Only administrators can save role preferences."
|
|
15073
|
+
},
|
|
15074
|
+
403
|
|
15075
|
+
);
|
|
15076
|
+
}
|
|
15077
|
+
if (!roleParam) {
|
|
15078
|
+
return c.json(
|
|
15079
|
+
{
|
|
15080
|
+
error: true,
|
|
15081
|
+
message: "Role query parameter is required for role-scoped preferences."
|
|
15082
|
+
},
|
|
15083
|
+
400
|
|
15084
|
+
);
|
|
15085
|
+
}
|
|
15086
|
+
const roleDocId = `${roleParam}_${key}`;
|
|
15087
|
+
const existing = await db.findOne({
|
|
15088
|
+
collection: "__role_preferences",
|
|
15089
|
+
id: roleDocId
|
|
15090
|
+
});
|
|
15091
|
+
if (existing) {
|
|
15092
|
+
await db.update({
|
|
15093
|
+
collection: "__role_preferences",
|
|
15094
|
+
id: roleDocId,
|
|
15095
|
+
data: { value: body.value, role: roleParam, key }
|
|
15096
|
+
});
|
|
15097
|
+
} else {
|
|
15098
|
+
await db.create({
|
|
15099
|
+
collection: "__role_preferences",
|
|
15100
|
+
data: { id: roleDocId, role: roleParam, key, value: body.value }
|
|
15101
|
+
});
|
|
15102
|
+
}
|
|
15103
|
+
return c.json({ key, value: body.value, role: roleParam, scope: "role" });
|
|
14339
15104
|
}
|
|
14340
15105
|
const doc = await db.findOne({ collection: user.collection, id: user.sub });
|
|
14341
15106
|
if (!doc) return c.json({ error: true, message: "User not found." }, 404);
|
|
@@ -14346,13 +15111,14 @@ function registerRoutes(app, config) {
|
|
|
14346
15111
|
id: user.sub,
|
|
14347
15112
|
data: { __preferences: nextPreferences }
|
|
14348
15113
|
});
|
|
14349
|
-
return c.json({ key, value: body.value });
|
|
15114
|
+
return c.json({ key, value: body.value, scope: "personal" });
|
|
14350
15115
|
});
|
|
14351
15116
|
app.delete("/api/preferences/:key", requireAuth(config), async (c) => {
|
|
14352
15117
|
const db = config.db;
|
|
14353
15118
|
const user = c.get("user");
|
|
14354
15119
|
const key = c.req.param("key");
|
|
14355
|
-
const scope = c.req.query("scope");
|
|
15120
|
+
const scope = c.req.query("scope") || "personal";
|
|
15121
|
+
const roleParam = c.req.query("role");
|
|
14356
15122
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
14357
15123
|
if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
|
|
14358
15124
|
if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
|
|
@@ -14371,6 +15137,30 @@ function registerRoutes(app, config) {
|
|
|
14371
15137
|
await db.delete({ collection: "__global_preferences", id: key });
|
|
14372
15138
|
return c.json({ success: true });
|
|
14373
15139
|
}
|
|
15140
|
+
if (scope === "role") {
|
|
15141
|
+
const userCol = config.collections?.find((col) => col.slug === user?.collection);
|
|
15142
|
+
const isAdminUser = isUserAdmin(user, userCol);
|
|
15143
|
+
if (!isAdminUser) {
|
|
15144
|
+
return c.json(
|
|
15145
|
+
{
|
|
15146
|
+
error: true,
|
|
15147
|
+
message: "Only administrators can delete role preferences."
|
|
15148
|
+
},
|
|
15149
|
+
403
|
|
15150
|
+
);
|
|
15151
|
+
}
|
|
15152
|
+
if (!roleParam) {
|
|
15153
|
+
return c.json(
|
|
15154
|
+
{
|
|
15155
|
+
error: true,
|
|
15156
|
+
message: "Role query parameter is required for role-scoped preferences."
|
|
15157
|
+
},
|
|
15158
|
+
400
|
|
15159
|
+
);
|
|
15160
|
+
}
|
|
15161
|
+
await db.delete({ collection: "__role_preferences", id: `${roleParam}_${key}` });
|
|
15162
|
+
return c.json({ success: true });
|
|
15163
|
+
}
|
|
14374
15164
|
const doc = await db.findOne({ collection: user.collection, id: user.sub });
|
|
14375
15165
|
if (!doc) return c.json({ error: true, message: "User not found." }, 404);
|
|
14376
15166
|
const preferences = typeof doc.__preferences === "object" && doc.__preferences !== null ? { ...doc.__preferences } : {};
|
|
@@ -14382,6 +15172,198 @@ function registerRoutes(app, config) {
|
|
|
14382
15172
|
});
|
|
14383
15173
|
return c.json({ success: true });
|
|
14384
15174
|
});
|
|
15175
|
+
app.get("/api/admin/navigation", optionalAuth(config), async (c) => {
|
|
15176
|
+
const user = c.get("user");
|
|
15177
|
+
const compiled = compileNavigation(config);
|
|
15178
|
+
const pruned = pruneNavigationForUser(compiled, user);
|
|
15179
|
+
return c.json(pruned);
|
|
15180
|
+
});
|
|
15181
|
+
app.on(["GET", "POST"], "/api/admin/navigation/badges", optionalAuth(config), async (c) => {
|
|
15182
|
+
const db = config.db;
|
|
15183
|
+
if (!db) return c.json({ badges: {} });
|
|
15184
|
+
const user = c.get("user");
|
|
15185
|
+
const compiled = compileNavigation(config);
|
|
15186
|
+
let userNavPrefs = null;
|
|
15187
|
+
const prefsQuery = c.req.query("prefs");
|
|
15188
|
+
if (prefsQuery) {
|
|
15189
|
+
try {
|
|
15190
|
+
userNavPrefs = JSON.parse(decodeURIComponent(prefsQuery));
|
|
15191
|
+
} catch {
|
|
15192
|
+
}
|
|
15193
|
+
}
|
|
15194
|
+
if (!userNavPrefs && c.req.method === "POST") {
|
|
15195
|
+
try {
|
|
15196
|
+
const body = await c.req.json();
|
|
15197
|
+
if (body?.preferences) userNavPrefs = body.preferences;
|
|
15198
|
+
} catch {
|
|
15199
|
+
}
|
|
15200
|
+
}
|
|
15201
|
+
if (!userNavPrefs && user?.collection && user?.sub) {
|
|
15202
|
+
try {
|
|
15203
|
+
const userDoc = await db.findOne({ collection: user.collection, id: user.sub });
|
|
15204
|
+
if (userDoc?.__preferences && typeof userDoc.__preferences === "object") {
|
|
15205
|
+
userNavPrefs = userDoc.__preferences["admin:navigation"];
|
|
15206
|
+
}
|
|
15207
|
+
} catch {
|
|
15208
|
+
}
|
|
15209
|
+
}
|
|
15210
|
+
if (!userNavPrefs) {
|
|
15211
|
+
try {
|
|
15212
|
+
const globalDoc = await db.findOne({
|
|
15213
|
+
collection: "__global_preferences",
|
|
15214
|
+
id: "admin:navigation"
|
|
15215
|
+
});
|
|
15216
|
+
if (globalDoc?.value) {
|
|
15217
|
+
userNavPrefs = globalDoc.value;
|
|
15218
|
+
}
|
|
15219
|
+
} catch {
|
|
15220
|
+
}
|
|
15221
|
+
}
|
|
15222
|
+
let allItems;
|
|
15223
|
+
if (userNavPrefs) {
|
|
15224
|
+
const reconciled = reconcileNavigation(compiled, userNavPrefs, {
|
|
15225
|
+
collections: config.collections,
|
|
15226
|
+
globals: config.globals
|
|
15227
|
+
});
|
|
15228
|
+
allItems = [
|
|
15229
|
+
...reconciled.ungrouped || [],
|
|
15230
|
+
...reconciled.pinnedItems || [],
|
|
15231
|
+
...(reconciled.groups || []).flatMap((g) => g.items || [])
|
|
15232
|
+
];
|
|
15233
|
+
} else {
|
|
15234
|
+
const pruned = pruneNavigationForUser(compiled, user);
|
|
15235
|
+
allItems = [
|
|
15236
|
+
...pruned.ungrouped,
|
|
15237
|
+
...pruned.groups.flatMap((g) => g.items)
|
|
15238
|
+
];
|
|
15239
|
+
}
|
|
15240
|
+
const badges = {};
|
|
15241
|
+
const requestsByCollection = /* @__PURE__ */ new Map();
|
|
15242
|
+
let reqCounter = 0;
|
|
15243
|
+
for (const item of allItems) {
|
|
15244
|
+
if (item.badge) {
|
|
15245
|
+
const itemBadge = item.badge;
|
|
15246
|
+
const badgeKey = item.slug || item.collection || item.global || item.id;
|
|
15247
|
+
if (typeof itemBadge === "string") {
|
|
15248
|
+
badges[badgeKey] = { count: itemBadge, variant: "default" };
|
|
15249
|
+
} else {
|
|
15250
|
+
const badgeConfig = itemBadge;
|
|
15251
|
+
const variant = badgeConfig.variant ?? "default";
|
|
15252
|
+
if (badgeConfig.text) {
|
|
15253
|
+
badges[badgeKey] = { count: badgeConfig.text, variant };
|
|
15254
|
+
} else {
|
|
15255
|
+
let targetCol;
|
|
15256
|
+
let targetWhere;
|
|
15257
|
+
if (badgeConfig.aggregate) {
|
|
15258
|
+
targetCol = badgeConfig.aggregate.collection;
|
|
15259
|
+
targetWhere = badgeConfig.aggregate.where;
|
|
15260
|
+
} else if (badgeConfig.count) {
|
|
15261
|
+
targetCol = item.collection || item.views?.[0]?.collection;
|
|
15262
|
+
const primaryView = item.views?.[0];
|
|
15263
|
+
if (primaryView?.filter) {
|
|
15264
|
+
targetWhere = primaryView.filter;
|
|
15265
|
+
}
|
|
15266
|
+
}
|
|
15267
|
+
if (targetCol) {
|
|
15268
|
+
const reqId = `agg_${reqCounter++}`;
|
|
15269
|
+
const list = requestsByCollection.get(targetCol) || [];
|
|
15270
|
+
list.push({
|
|
15271
|
+
id: reqId,
|
|
15272
|
+
where: targetWhere,
|
|
15273
|
+
onResult: (count2) => {
|
|
15274
|
+
badges[badgeKey] = { count: count2, variant };
|
|
15275
|
+
}
|
|
15276
|
+
});
|
|
15277
|
+
requestsByCollection.set(targetCol, list);
|
|
15278
|
+
}
|
|
15279
|
+
}
|
|
15280
|
+
}
|
|
15281
|
+
}
|
|
15282
|
+
if (item.views && item.views.length > 0) {
|
|
15283
|
+
for (const view of item.views) {
|
|
15284
|
+
if (!view.badge) continue;
|
|
15285
|
+
const viewBadge = view.badge;
|
|
15286
|
+
const viewKey = `${item.slug}:${view.slug}`;
|
|
15287
|
+
const altKey = `${item.slug}_${view.slug}`;
|
|
15288
|
+
const plainKey = view.slug;
|
|
15289
|
+
const setBadgeResult = (res) => {
|
|
15290
|
+
badges[viewKey] = res;
|
|
15291
|
+
badges[altKey] = res;
|
|
15292
|
+
if (!badges[plainKey]) {
|
|
15293
|
+
badges[plainKey] = res;
|
|
15294
|
+
}
|
|
15295
|
+
if (view.collection) {
|
|
15296
|
+
badges[`${view.collection}:${view.slug}`] = res;
|
|
15297
|
+
badges[`${view.collection}_${view.slug}`] = res;
|
|
15298
|
+
}
|
|
15299
|
+
};
|
|
15300
|
+
if (typeof viewBadge === "string") {
|
|
15301
|
+
setBadgeResult({ count: viewBadge, variant: "default" });
|
|
15302
|
+
continue;
|
|
15303
|
+
}
|
|
15304
|
+
const badgeConfig = viewBadge;
|
|
15305
|
+
const variant = badgeConfig.variant ?? "default";
|
|
15306
|
+
if (badgeConfig.text) {
|
|
15307
|
+
setBadgeResult({ count: badgeConfig.text, variant });
|
|
15308
|
+
continue;
|
|
15309
|
+
}
|
|
15310
|
+
const targetCol = badgeConfig.aggregate?.collection || view.collection || item.collection || (item.type === "collection" ? item.slug : void 0);
|
|
15311
|
+
const targetWhere = badgeConfig.aggregate?.where || (typeof view.filter === "object" ? view.filter : void 0);
|
|
15312
|
+
if (!targetCol) continue;
|
|
15313
|
+
const reqId = `agg_${reqCounter++}`;
|
|
15314
|
+
const list = requestsByCollection.get(targetCol) || [];
|
|
15315
|
+
list.push({
|
|
15316
|
+
id: reqId,
|
|
15317
|
+
where: targetWhere,
|
|
15318
|
+
onResult: (count2) => {
|
|
15319
|
+
setBadgeResult({ count: count2, variant });
|
|
15320
|
+
}
|
|
15321
|
+
});
|
|
15322
|
+
requestsByCollection.set(targetCol, list);
|
|
15323
|
+
}
|
|
15324
|
+
}
|
|
15325
|
+
}
|
|
15326
|
+
const collectionTasks = Array.from(requestsByCollection.entries()).map(async ([col, reqs]) => {
|
|
15327
|
+
if (typeof db.aggregate === "function") {
|
|
15328
|
+
try {
|
|
15329
|
+
const aggregates = {};
|
|
15330
|
+
for (const req of reqs) {
|
|
15331
|
+
aggregates[req.id] = { count: "*", where: req.where };
|
|
15332
|
+
}
|
|
15333
|
+
const agg = await db.aggregate({
|
|
15334
|
+
collection: col,
|
|
15335
|
+
aggregates
|
|
15336
|
+
});
|
|
15337
|
+
for (const req of reqs) {
|
|
15338
|
+
const val = agg?.[req.id];
|
|
15339
|
+
req.onResult(typeof val === "number" ? val : 0);
|
|
15340
|
+
}
|
|
15341
|
+
} catch {
|
|
15342
|
+
for (const req of reqs) {
|
|
15343
|
+
req.onResult(0);
|
|
15344
|
+
}
|
|
15345
|
+
}
|
|
15346
|
+
} else {
|
|
15347
|
+
await Promise.allSettled(
|
|
15348
|
+
reqs.map(async (req) => {
|
|
15349
|
+
try {
|
|
15350
|
+
const res = await db.find({
|
|
15351
|
+
collection: col,
|
|
15352
|
+
where: req.where,
|
|
15353
|
+
limit: 1
|
|
15354
|
+
});
|
|
15355
|
+
const total = res?.totalDocs ?? res?.total ?? 0;
|
|
15356
|
+
req.onResult(total);
|
|
15357
|
+
} catch {
|
|
15358
|
+
req.onResult(0);
|
|
15359
|
+
}
|
|
15360
|
+
})
|
|
15361
|
+
);
|
|
15362
|
+
}
|
|
15363
|
+
});
|
|
15364
|
+
await Promise.allSettled(collectionTasks);
|
|
15365
|
+
return c.json({ badges });
|
|
15366
|
+
});
|
|
14385
15367
|
app.get("/api/media/folders", async (c) => {
|
|
14386
15368
|
const folderController = new MediaFolderController("media");
|
|
14387
15369
|
return folderController.list(c);
|
|
@@ -14631,6 +15613,31 @@ function registerRoutes(app, config) {
|
|
|
14631
15613
|
});
|
|
14632
15614
|
}
|
|
14633
15615
|
|
|
15616
|
+
// src/tasks.ts
|
|
15617
|
+
var TASK_LOCKS_COLLECTION = "__task_locks";
|
|
15618
|
+
var TASK_LOCKS_COLLECTION_CONFIG = {
|
|
15619
|
+
slug: TASK_LOCKS_COLLECTION,
|
|
15620
|
+
labels: { singular: "Task lock", plural: "Task locks" },
|
|
15621
|
+
fields: [
|
|
15622
|
+
{ name: "name", type: "text", required: true },
|
|
15623
|
+
// Numeric and promoted so the lock compare-and-set is a real column comparison.
|
|
15624
|
+
{ name: "lockedUntil", type: "number", required: true, promoted: true },
|
|
15625
|
+
{ name: "lockedBy", type: "text" },
|
|
15626
|
+
{ name: "lastScheduledFor", type: "date" },
|
|
15627
|
+
{ name: "lastStartedAt", type: "date" },
|
|
15628
|
+
{ name: "lastFinishedAt", type: "date" },
|
|
15629
|
+
{ name: "lastStatus", type: "text" },
|
|
15630
|
+
{ name: "lastError", type: "textarea" }
|
|
15631
|
+
],
|
|
15632
|
+
access: {
|
|
15633
|
+
read: ({ user }) => !!user?.roles?.includes("admin"),
|
|
15634
|
+
create: () => false,
|
|
15635
|
+
update: () => false,
|
|
15636
|
+
delete: () => false
|
|
15637
|
+
},
|
|
15638
|
+
admin: { hidden: true }
|
|
15639
|
+
};
|
|
15640
|
+
|
|
14634
15641
|
// src/utils/config.ts
|
|
14635
15642
|
var AUDIT_COLLECTION_SLUG = "__audit";
|
|
14636
15643
|
var SYSTEM_FIELDS = [
|
|
@@ -14782,6 +15789,7 @@ function normalizeConfig(config) {
|
|
|
14782
15789
|
const globals = schemaAwareConfig?.globals || [];
|
|
14783
15790
|
const needsAudit = collections.some((col) => col.audit);
|
|
14784
15791
|
const needsWorkflow = collections.some((col) => col.workflow || col.drafts);
|
|
15792
|
+
const needsTasks = (config.tasks?.length ?? 0) > 0;
|
|
14785
15793
|
const needsAuthSessions = collections.some((col) => !!col.auth);
|
|
14786
15794
|
const adminAuthCollectionSlug = getAdminAuthCollection({
|
|
14787
15795
|
collections,
|
|
@@ -14861,7 +15869,33 @@ function normalizeConfig(config) {
|
|
|
14861
15869
|
}
|
|
14862
15870
|
];
|
|
14863
15871
|
}
|
|
15872
|
+
if (!existingFieldNames.has("__preferences")) {
|
|
15873
|
+
fields = [
|
|
15874
|
+
...fields,
|
|
15875
|
+
{
|
|
15876
|
+
name: "__preferences",
|
|
15877
|
+
type: "json",
|
|
15878
|
+
label: "Preferences",
|
|
15879
|
+
promoted: true,
|
|
15880
|
+
admin: {
|
|
15881
|
+
hidden: true,
|
|
15882
|
+
readOnly: true
|
|
15883
|
+
}
|
|
15884
|
+
}
|
|
15885
|
+
];
|
|
15886
|
+
}
|
|
14864
15887
|
fields = fields.map((field) => {
|
|
15888
|
+
if (field.name === "__preferences") {
|
|
15889
|
+
return {
|
|
15890
|
+
...field,
|
|
15891
|
+
promoted: true,
|
|
15892
|
+
admin: {
|
|
15893
|
+
...field.admin || {},
|
|
15894
|
+
hidden: true,
|
|
15895
|
+
readOnly: true
|
|
15896
|
+
}
|
|
15897
|
+
};
|
|
15898
|
+
}
|
|
14865
15899
|
if (field.name === "email") {
|
|
14866
15900
|
return {
|
|
14867
15901
|
...field,
|
|
@@ -14988,6 +16022,9 @@ function normalizeConfig(config) {
|
|
|
14988
16022
|
if (needsWorkflow && !normalizedCollections.some((col) => col.slug === LIFECYCLE_EVENTS_COLLECTION)) {
|
|
14989
16023
|
systemCollections.push(LIFECYCLE_EVENTS_COLLECTION_CONFIG);
|
|
14990
16024
|
}
|
|
16025
|
+
if (needsTasks && !normalizedCollections.some((col) => col.slug === TASK_LOCKS_COLLECTION)) {
|
|
16026
|
+
systemCollections.push(TASK_LOCKS_COLLECTION_CONFIG);
|
|
16027
|
+
}
|
|
14991
16028
|
if (needsAuthSessions && !normalizedCollections.some((col) => col.slug === AUTH_SESSIONS_COLLECTION)) {
|
|
14992
16029
|
systemCollections.push(AUTH_SESSIONS_COLLECTION_CONFIG);
|
|
14993
16030
|
}
|
|
@@ -15357,6 +16394,7 @@ export {
|
|
|
15357
16394
|
revokeAllAuthSessions,
|
|
15358
16395
|
revokeAuthSession,
|
|
15359
16396
|
sendEmail,
|
|
16397
|
+
serializeActionsForApi,
|
|
15360
16398
|
serializeDetailForApi,
|
|
15361
16399
|
serializeViewForApi,
|
|
15362
16400
|
shouldSampleRequest,
|