@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.cjs
CHANGED
|
@@ -4013,6 +4013,7 @@ __export(server_exports, {
|
|
|
4013
4013
|
revokeAllAuthSessions: () => revokeAllAuthSessions,
|
|
4014
4014
|
revokeAuthSession: () => revokeAuthSession,
|
|
4015
4015
|
sendEmail: () => sendEmail,
|
|
4016
|
+
serializeActionsForApi: () => serializeActionsForApi,
|
|
4016
4017
|
serializeDetailForApi: () => serializeDetailForApi,
|
|
4017
4018
|
serializeViewForApi: () => serializeViewForApi,
|
|
4018
4019
|
shouldSampleRequest: () => shouldSampleRequest,
|
|
@@ -6144,6 +6145,72 @@ function createReadonlyDb(db) {
|
|
|
6144
6145
|
});
|
|
6145
6146
|
}
|
|
6146
6147
|
|
|
6148
|
+
// src/utils/errors.ts
|
|
6149
|
+
var DyrectedError = class _DyrectedError extends Error {
|
|
6150
|
+
statusCode;
|
|
6151
|
+
constructor(message, statusCode = 500) {
|
|
6152
|
+
super(message);
|
|
6153
|
+
this.name = "DyrectedError";
|
|
6154
|
+
this.statusCode = statusCode;
|
|
6155
|
+
Object.setPrototypeOf(this, _DyrectedError.prototype);
|
|
6156
|
+
}
|
|
6157
|
+
};
|
|
6158
|
+
var ValidationError = class _ValidationError extends DyrectedError {
|
|
6159
|
+
constructor(message) {
|
|
6160
|
+
super(message, 400);
|
|
6161
|
+
this.name = "ValidationError";
|
|
6162
|
+
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
6163
|
+
}
|
|
6164
|
+
};
|
|
6165
|
+
|
|
6166
|
+
// src/utils/field-integrity.ts
|
|
6167
|
+
function* storedFields(fields) {
|
|
6168
|
+
for (const field of fields ?? []) {
|
|
6169
|
+
if (field.type === "row") {
|
|
6170
|
+
yield* storedFields(field.fields);
|
|
6171
|
+
} else if (field.name) {
|
|
6172
|
+
yield field;
|
|
6173
|
+
}
|
|
6174
|
+
}
|
|
6175
|
+
}
|
|
6176
|
+
function sameValue(a, b) {
|
|
6177
|
+
if (a === b) return true;
|
|
6178
|
+
if (a === null || b === null || a === void 0 || b === void 0) return a == b;
|
|
6179
|
+
if (typeof a === "object" || typeof b === "object") {
|
|
6180
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
6181
|
+
}
|
|
6182
|
+
return false;
|
|
6183
|
+
}
|
|
6184
|
+
function assertImmutableFieldsUnchanged(fields, data, original) {
|
|
6185
|
+
for (const field of storedFields(fields)) {
|
|
6186
|
+
if (!field.immutable || data[field.name] === void 0) continue;
|
|
6187
|
+
if (!sameValue(data[field.name], original[field.name])) {
|
|
6188
|
+
throw new ValidationError(
|
|
6189
|
+
`Field "${field.label ?? field.name}" is immutable and cannot be modified.`
|
|
6190
|
+
);
|
|
6191
|
+
}
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
function normalizeMoneyFields(fields, data) {
|
|
6195
|
+
let next = data;
|
|
6196
|
+
for (const field of storedFields(fields)) {
|
|
6197
|
+
if (field.type !== "money") continue;
|
|
6198
|
+
const value = data[field.name];
|
|
6199
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
6200
|
+
let minor;
|
|
6201
|
+
if (typeof value === "number") minor = value;
|
|
6202
|
+
else if (typeof value === "string" && /^-?\d+$/.test(value.trim())) minor = Number(value.trim());
|
|
6203
|
+
if (minor === void 0 || !Number.isSafeInteger(minor)) {
|
|
6204
|
+
throw new ValidationError(
|
|
6205
|
+
`Field "${field.label ?? field.name}" must be a whole number of minor units (for example kobo or cents).`
|
|
6206
|
+
);
|
|
6207
|
+
}
|
|
6208
|
+
if (next === data) next = { ...data };
|
|
6209
|
+
next[field.name] = minor;
|
|
6210
|
+
}
|
|
6211
|
+
return next;
|
|
6212
|
+
}
|
|
6213
|
+
|
|
6147
6214
|
// src/utils/upload-validation.ts
|
|
6148
6215
|
function isMimeAllowed(mimeType, allowed) {
|
|
6149
6216
|
const type = (mimeType || "").toLowerCase().trim();
|
|
@@ -6739,7 +6806,7 @@ async function dispatchLifecycleEvent(config, event) {
|
|
|
6739
6806
|
}
|
|
6740
6807
|
}
|
|
6741
6808
|
async function saveWorkflowDraft(args) {
|
|
6742
|
-
const { config, collection, id, originalDoc, data, user } = args;
|
|
6809
|
+
const { config, collection, id, originalDoc, data, user, beforeCommit } = args;
|
|
6743
6810
|
const db = config.db;
|
|
6744
6811
|
const workflow = collection.workflow;
|
|
6745
6812
|
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
@@ -6773,13 +6840,14 @@ async function saveWorkflowDraft(args) {
|
|
|
6773
6840
|
}
|
|
6774
6841
|
const updated = await tx.update({ collection: collection.slug, id, data: updateData });
|
|
6775
6842
|
await persistEvent(tx, event);
|
|
6843
|
+
await beforeCommit?.(tx, updated);
|
|
6776
6844
|
return updated;
|
|
6777
6845
|
});
|
|
6778
6846
|
void dispatchLifecycleEvent(config, event);
|
|
6779
6847
|
return { doc, event };
|
|
6780
6848
|
}
|
|
6781
6849
|
async function createWorkflowDocument(args) {
|
|
6782
|
-
const { config, collection, data, user } = args;
|
|
6850
|
+
const { config, collection, data, user, beforeCommit } = args;
|
|
6783
6851
|
const db = config.db;
|
|
6784
6852
|
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
6785
6853
|
let event;
|
|
@@ -6793,13 +6861,14 @@ async function createWorkflowDocument(args) {
|
|
|
6793
6861
|
payload: { revision: 1, previousRevision: null }
|
|
6794
6862
|
});
|
|
6795
6863
|
await persistEvent(tx, event);
|
|
6864
|
+
await beforeCommit?.(tx, created);
|
|
6796
6865
|
return created;
|
|
6797
6866
|
});
|
|
6798
6867
|
void dispatchLifecycleEvent(config, event);
|
|
6799
6868
|
return { doc, event };
|
|
6800
6869
|
}
|
|
6801
6870
|
async function transitionWorkflow(args) {
|
|
6802
|
-
const { config, collection, id, transitionName, expectedRevision, comment, user, req } = args;
|
|
6871
|
+
const { config, collection, id, transitionName, expectedRevision, comment, input, user, req } = args;
|
|
6803
6872
|
const db = config.db;
|
|
6804
6873
|
const workflow = collection.workflow;
|
|
6805
6874
|
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
@@ -6825,7 +6894,7 @@ async function transitionWorkflow(args) {
|
|
|
6825
6894
|
if (transition.requireComment && !comment?.trim()) {
|
|
6826
6895
|
throw Object.assign(new Error(`A comment is required for "${transition.label}".`), { statusCode: 400 });
|
|
6827
6896
|
}
|
|
6828
|
-
const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, req, db };
|
|
6897
|
+
const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, input, req, db };
|
|
6829
6898
|
for (const hook of workflow.hooks?.beforeTransition ?? []) await hook(hookContext);
|
|
6830
6899
|
const events = [
|
|
6831
6900
|
createLifecycleEvent({
|
|
@@ -6879,6 +6948,19 @@ async function transitionWorkflow(args) {
|
|
|
6879
6948
|
if (targetState.published) data.__published = working;
|
|
6880
6949
|
if (transition.unpublish) data.__published = null;
|
|
6881
6950
|
const next = await tx.update({ collection: collection.slug, id, data });
|
|
6951
|
+
if (transition.onTransition) {
|
|
6952
|
+
await transition.onTransition({
|
|
6953
|
+
transition,
|
|
6954
|
+
from: lockedMeta.state,
|
|
6955
|
+
to: transition.to,
|
|
6956
|
+
doc: locked,
|
|
6957
|
+
input,
|
|
6958
|
+
comment,
|
|
6959
|
+
user,
|
|
6960
|
+
req,
|
|
6961
|
+
tx
|
|
6962
|
+
});
|
|
6963
|
+
}
|
|
6882
6964
|
await tx.create({
|
|
6883
6965
|
collection: WORKFLOW_HISTORY_COLLECTION,
|
|
6884
6966
|
data: {
|
|
@@ -7518,6 +7600,7 @@ var CollectionController = class {
|
|
|
7518
7600
|
updatedBy: user?.sub ?? null
|
|
7519
7601
|
};
|
|
7520
7602
|
data = DefaultsService.apply(this.collection.fields, data);
|
|
7603
|
+
data = normalizeMoneyFields(this.collection.fields, data);
|
|
7521
7604
|
if (this.collection.workflow) {
|
|
7522
7605
|
data = initializeWorkflowDocument(data, this.collection.workflow);
|
|
7523
7606
|
}
|
|
@@ -7565,12 +7648,18 @@ var CollectionController = class {
|
|
|
7565
7648
|
if (!data.id) {
|
|
7566
7649
|
data = { ...data, id: generateDocumentId(this.collection) };
|
|
7567
7650
|
}
|
|
7651
|
+
const beforeCommit = this.buildBeforeCommit(c, "create", user);
|
|
7568
7652
|
const doc = this.collection.workflow ? (await createWorkflowDocument({
|
|
7569
7653
|
config,
|
|
7570
7654
|
collection: this.collection,
|
|
7571
7655
|
data,
|
|
7572
|
-
user
|
|
7573
|
-
|
|
7656
|
+
user,
|
|
7657
|
+
beforeCommit
|
|
7658
|
+
})).doc : await this.persistInTransaction(
|
|
7659
|
+
db,
|
|
7660
|
+
beforeCommit,
|
|
7661
|
+
(writer) => writer.create({ collection: this.collection.slug, data })
|
|
7662
|
+
);
|
|
7574
7663
|
if (this.collection.audit && db) {
|
|
7575
7664
|
AuditService.log(db, {
|
|
7576
7665
|
operation: "create",
|
|
@@ -7804,6 +7893,36 @@ var CollectionController = class {
|
|
|
7804
7893
|
const body = await c.req.json();
|
|
7805
7894
|
return this.performUpdate(c, id, body);
|
|
7806
7895
|
}
|
|
7896
|
+
/**
|
|
7897
|
+
* Builds the callback that runs the collection's `beforeCommit` hooks inside
|
|
7898
|
+
* the write transaction, or `undefined` when the collection has none.
|
|
7899
|
+
*/
|
|
7900
|
+
buildBeforeCommit(c, operation, user, previousDoc) {
|
|
7901
|
+
const hooks = this.collection.hooks?.beforeCommit;
|
|
7902
|
+
if (!hooks?.length) return void 0;
|
|
7903
|
+
return async (tx, doc) => {
|
|
7904
|
+
for (const hook of hooks) {
|
|
7905
|
+
await hook({ doc, previousDoc, req: this.toHookRequestContext(c), user, operation, tx });
|
|
7906
|
+
}
|
|
7907
|
+
};
|
|
7908
|
+
}
|
|
7909
|
+
/**
|
|
7910
|
+
* Runs a single write. When `beforeCommit` hooks exist the write and the
|
|
7911
|
+
* hooks share one transaction, so a throwing hook rolls the write back.
|
|
7912
|
+
*/
|
|
7913
|
+
async persistInTransaction(db, beforeCommit, write) {
|
|
7914
|
+
if (!beforeCommit) return write(db);
|
|
7915
|
+
if (!db.transaction) {
|
|
7916
|
+
throw new Error(
|
|
7917
|
+
`Collection "${this.collection.slug}" defines beforeCommit hooks but the configured database adapter does not support transactions.`
|
|
7918
|
+
);
|
|
7919
|
+
}
|
|
7920
|
+
return db.transaction(async (tx) => {
|
|
7921
|
+
const doc = await write(tx);
|
|
7922
|
+
await beforeCommit(tx, doc);
|
|
7923
|
+
return doc;
|
|
7924
|
+
});
|
|
7925
|
+
}
|
|
7807
7926
|
/**
|
|
7808
7927
|
* Core update pipeline shared by PATCH requests and operational actions:
|
|
7809
7928
|
* auth-field stripping, timestamps, access enforcement, field write access,
|
|
@@ -7845,6 +7964,7 @@ var CollectionController = class {
|
|
|
7845
7964
|
403
|
|
7846
7965
|
);
|
|
7847
7966
|
}
|
|
7967
|
+
data = normalizeMoneyFields(this.collection.fields, data);
|
|
7848
7968
|
let before = null;
|
|
7849
7969
|
if (this.collection.audit) {
|
|
7850
7970
|
before = originalDoc;
|
|
@@ -7878,14 +7998,21 @@ var CollectionController = class {
|
|
|
7878
7998
|
surface: "collection.beforeChange",
|
|
7879
7999
|
path: `collection:${this.collection.slug}.hooks.beforeChange`
|
|
7880
8000
|
});
|
|
8001
|
+
assertImmutableFieldsUnchanged(this.collection.fields, data, originalDoc);
|
|
8002
|
+
const beforeCommit = this.buildBeforeCommit(c, "update", user, originalDoc);
|
|
7881
8003
|
const doc = this.collection.workflow ? (await saveWorkflowDraft({
|
|
7882
8004
|
config,
|
|
7883
8005
|
collection: this.collection,
|
|
7884
8006
|
id,
|
|
7885
8007
|
originalDoc,
|
|
7886
8008
|
data,
|
|
7887
|
-
user
|
|
7888
|
-
|
|
8009
|
+
user,
|
|
8010
|
+
beforeCommit
|
|
8011
|
+
})).doc : await this.persistInTransaction(
|
|
8012
|
+
db,
|
|
8013
|
+
beforeCommit,
|
|
8014
|
+
(writer) => writer.update({ collection: this.collection.slug, id, data })
|
|
8015
|
+
);
|
|
7889
8016
|
if (this.collection.audit && db) {
|
|
7890
8017
|
AuditService.log(db, {
|
|
7891
8018
|
operation: "update",
|
|
@@ -8112,6 +8239,7 @@ var CollectionController = class {
|
|
|
8112
8239
|
transitionName,
|
|
8113
8240
|
expectedRevision: body.expectedRevision,
|
|
8114
8241
|
comment: body.comment,
|
|
8242
|
+
input: body.input,
|
|
8115
8243
|
user: c.get("user"),
|
|
8116
8244
|
req: { query: c.req.query(), headers: c.req.header(), raw: c.req.raw }
|
|
8117
8245
|
});
|
|
@@ -9443,6 +9571,20 @@ var MediaFolderController = class {
|
|
|
9443
9571
|
}
|
|
9444
9572
|
};
|
|
9445
9573
|
|
|
9574
|
+
// src/utils/admin-url.ts
|
|
9575
|
+
var DEFAULT_ADMIN_PATH = "/admin";
|
|
9576
|
+
function resolveAdminUrl(c, config, clientUrl) {
|
|
9577
|
+
const configured = config.admin?.adminUrl?.trim() || process.env.DYRECTED_ADMIN_URL?.trim() || (typeof clientUrl === "string" ? clientUrl.trim() : "") || DEFAULT_ADMIN_PATH;
|
|
9578
|
+
try {
|
|
9579
|
+
return new URL(configured, c.req.url).toString();
|
|
9580
|
+
} catch {
|
|
9581
|
+
return configured;
|
|
9582
|
+
}
|
|
9583
|
+
}
|
|
9584
|
+
function appendQueryParam(url, key, value) {
|
|
9585
|
+
return `${url}${url.includes("?") ? "&" : "?"}${key}=${encodeURIComponent(value)}`;
|
|
9586
|
+
}
|
|
9587
|
+
|
|
9446
9588
|
// src/controllers/auth.controller.ts
|
|
9447
9589
|
var import_node_crypto3 = require("crypto");
|
|
9448
9590
|
|
|
@@ -10118,8 +10260,7 @@ var AuthController = class {
|
|
|
10118
10260
|
},
|
|
10119
10261
|
"1h"
|
|
10120
10262
|
);
|
|
10121
|
-
const
|
|
10122
|
-
const url = resetUrl ? `${resetUrl}${resetUrl.includes("?") ? "&" : "?"}token=${encodeURIComponent(resetToken)}` : void 0;
|
|
10263
|
+
const url = appendQueryParam(resolveAdminUrl(c, config, body?.resetUrl), "token", resetToken);
|
|
10123
10264
|
try {
|
|
10124
10265
|
const { subject, html } = buildResetPasswordEmail(config, {
|
|
10125
10266
|
token: resetToken,
|
|
@@ -10251,8 +10392,7 @@ var AuthController = class {
|
|
|
10251
10392
|
},
|
|
10252
10393
|
"7d"
|
|
10253
10394
|
);
|
|
10254
|
-
const
|
|
10255
|
-
const url = inviteUrl ? `${inviteUrl}${inviteUrl.includes("?") ? "&" : "?"}inviteToken=${encodeURIComponent(inviteToken)}` : void 0;
|
|
10395
|
+
const url = appendQueryParam(resolveAdminUrl(c, config, body?.inviteUrl), "inviteToken", inviteToken);
|
|
10256
10396
|
try {
|
|
10257
10397
|
const { subject, html } = buildInviteEmail(config, {
|
|
10258
10398
|
token: inviteToken,
|
|
@@ -12526,7 +12666,12 @@ function generateOpenApi(config) {
|
|
|
12526
12666
|
type: "object",
|
|
12527
12667
|
properties: {
|
|
12528
12668
|
expectedRevision: { type: "integer", minimum: 1 },
|
|
12529
|
-
comment: { type: "string" }
|
|
12669
|
+
comment: { type: "string" },
|
|
12670
|
+
input: {
|
|
12671
|
+
type: "object",
|
|
12672
|
+
additionalProperties: true,
|
|
12673
|
+
description: "Extra data passed to the transition's onTransition handler."
|
|
12674
|
+
}
|
|
12530
12675
|
}
|
|
12531
12676
|
},
|
|
12532
12677
|
WorkflowHistoryEntry: {
|
|
@@ -13689,6 +13834,9 @@ function fieldToSchema(field) {
|
|
|
13689
13834
|
case "number":
|
|
13690
13835
|
schema = { type: "number" };
|
|
13691
13836
|
break;
|
|
13837
|
+
case "money":
|
|
13838
|
+
schema = { type: "integer", description: "Amount in minor units (for example kobo or cents)." };
|
|
13839
|
+
break;
|
|
13692
13840
|
case "boolean":
|
|
13693
13841
|
schema = { type: "boolean" };
|
|
13694
13842
|
break;
|
|
@@ -13829,6 +13977,537 @@ function getSwaggerHtml(specUrl) {
|
|
|
13829
13977
|
`;
|
|
13830
13978
|
}
|
|
13831
13979
|
|
|
13980
|
+
// src/types/navigation.ts
|
|
13981
|
+
var CURRENT_NAV_PREFERENCES_VERSION = 1;
|
|
13982
|
+
var DEFAULT_USER_NAV_PREFERENCES = {
|
|
13983
|
+
_version: CURRENT_NAV_PREFERENCES_VERSION,
|
|
13984
|
+
pinned: [],
|
|
13985
|
+
hidden: [],
|
|
13986
|
+
groupOrder: [],
|
|
13987
|
+
itemOrder: {},
|
|
13988
|
+
groups: [],
|
|
13989
|
+
items: []
|
|
13990
|
+
};
|
|
13991
|
+
|
|
13992
|
+
// src/utils/navigation-reconciler.ts
|
|
13993
|
+
var normalizeKey = (val) => (val || "").toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
13994
|
+
function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
13995
|
+
if (!baseTree) {
|
|
13996
|
+
return {
|
|
13997
|
+
groups: [],
|
|
13998
|
+
ungrouped: [],
|
|
13999
|
+
pinnedItems: []
|
|
14000
|
+
};
|
|
14001
|
+
}
|
|
14002
|
+
const includeHidden = options?.includeHidden ?? false;
|
|
14003
|
+
const effectivePrefs = prefs || DEFAULT_USER_NAV_PREFERENCES;
|
|
14004
|
+
const hiddenSet = new Set(effectivePrefs.hidden || []);
|
|
14005
|
+
const knownCollectionSlugs = new Set((schemas?.collections || []).map((c) => c.slug));
|
|
14006
|
+
const knownGlobalSlugs = new Set((schemas?.globals || []).map((g) => g.slug));
|
|
14007
|
+
const groupsList = [];
|
|
14008
|
+
const findGroup = (query) => {
|
|
14009
|
+
const qNorm = normalizeKey(query);
|
|
14010
|
+
return groupsList.find(
|
|
14011
|
+
(g) => g.id === query || g.slug === query || g.name === query || normalizeKey(g.id) === qNorm || normalizeKey(g.slug) === qNorm || normalizeKey(g.name) === qNorm
|
|
14012
|
+
);
|
|
14013
|
+
};
|
|
14014
|
+
for (const g of baseTree.groups || []) {
|
|
14015
|
+
groupsList.push({
|
|
14016
|
+
...g,
|
|
14017
|
+
items: (g.items || []).map((item) => ({ ...item }))
|
|
14018
|
+
});
|
|
14019
|
+
}
|
|
14020
|
+
for (const ug of effectivePrefs.groups || []) {
|
|
14021
|
+
const existing = findGroup(ug.slug || ug.name);
|
|
14022
|
+
if (!existing) {
|
|
14023
|
+
groupsList.push({
|
|
14024
|
+
id: ug.slug ? `group_${ug.slug}` : `group_${normalizeKey(ug.name)}`,
|
|
14025
|
+
name: ug.name,
|
|
14026
|
+
slug: ug.slug || normalizeKey(ug.name),
|
|
14027
|
+
icon: ug.icon || "Folder",
|
|
14028
|
+
defaultExpanded: ug.defaultExpanded ?? true,
|
|
14029
|
+
order: ug.order ?? 100,
|
|
14030
|
+
items: []
|
|
14031
|
+
});
|
|
14032
|
+
}
|
|
14033
|
+
}
|
|
14034
|
+
for (const itemOpt of effectivePrefs.items || []) {
|
|
14035
|
+
const targetGroup = typeof itemOpt.group === "object" ? itemOpt.group?.name : itemOpt.group;
|
|
14036
|
+
const groupKey = targetGroup || "Custom";
|
|
14037
|
+
let group = findGroup(groupKey);
|
|
14038
|
+
if (!group) {
|
|
14039
|
+
group = {
|
|
14040
|
+
id: `group_${normalizeKey(groupKey)}`,
|
|
14041
|
+
name: groupKey,
|
|
14042
|
+
slug: normalizeKey(groupKey),
|
|
14043
|
+
icon: "Folder",
|
|
14044
|
+
defaultExpanded: true,
|
|
14045
|
+
order: 99,
|
|
14046
|
+
items: []
|
|
14047
|
+
};
|
|
14048
|
+
groupsList.push(group);
|
|
14049
|
+
}
|
|
14050
|
+
const itemId = itemOpt.slug ? `workspace_${itemOpt.slug}` : itemOpt.collection ? `collection_${itemOpt.collection}` : `custom_${Math.random()}`;
|
|
14051
|
+
for (const otherGroup of groupsList) {
|
|
14052
|
+
if (otherGroup !== group) {
|
|
14053
|
+
otherGroup.items = otherGroup.items.filter((i) => i.slug !== itemOpt.slug && i.id !== itemId);
|
|
14054
|
+
}
|
|
14055
|
+
}
|
|
14056
|
+
const existingIndex = group.items.findIndex((i) => i.id === itemId || i.slug === itemOpt.slug);
|
|
14057
|
+
const existingItem = existingIndex >= 0 ? group.items[existingIndex] : void 0;
|
|
14058
|
+
const col = schemas?.collections?.find((c) => c.slug === itemOpt.collection);
|
|
14059
|
+
const colViews = col?.views || [];
|
|
14060
|
+
const baseViews = existingItem?.views || [];
|
|
14061
|
+
const userViews = itemOpt.views || [];
|
|
14062
|
+
const userViewSlugs = new Set(userViews.map((v) => v.slug));
|
|
14063
|
+
let mergedViews = [
|
|
14064
|
+
...baseViews.filter((v) => !userViewSlugs.has(v.slug)),
|
|
14065
|
+
...userViews
|
|
14066
|
+
];
|
|
14067
|
+
if (mergedViews.length === 0 && itemOpt.collection) {
|
|
14068
|
+
if (colViews.length > 0) {
|
|
14069
|
+
mergedViews = [...colViews];
|
|
14070
|
+
} else {
|
|
14071
|
+
mergedViews = [
|
|
14072
|
+
{
|
|
14073
|
+
slug: "default",
|
|
14074
|
+
label: col?.label || col?.labels?.plural || itemOpt.label || "All Records",
|
|
14075
|
+
layout: "table",
|
|
14076
|
+
collection: itemOpt.collection
|
|
14077
|
+
}
|
|
14078
|
+
];
|
|
14079
|
+
}
|
|
14080
|
+
}
|
|
14081
|
+
mergedViews = mergedViews.map((v) => ({
|
|
14082
|
+
...v,
|
|
14083
|
+
collection: v.collection || itemOpt.collection || existingItem?.collection
|
|
14084
|
+
}));
|
|
14085
|
+
const compiledItem = {
|
|
14086
|
+
id: itemId,
|
|
14087
|
+
type: itemOpt.slug ? "workspace" : itemOpt.collection ? "collection" : itemOpt.global ? "global" : "workspace",
|
|
14088
|
+
slug: itemOpt.slug || itemOpt.collection || itemOpt.global || "item",
|
|
14089
|
+
label: itemOpt.label || itemOpt.slug || "Item",
|
|
14090
|
+
icon: itemOpt.icon || existingItem?.icon || col?.admin?.icon || "Briefcase",
|
|
14091
|
+
group: group.name,
|
|
14092
|
+
order: itemOpt.order ?? existingItem?.order ?? 100,
|
|
14093
|
+
views: mergedViews,
|
|
14094
|
+
collection: itemOpt.collection || existingItem?.collection,
|
|
14095
|
+
global: itemOpt.global || existingItem?.global,
|
|
14096
|
+
badge: itemOpt.badge || existingItem?.badge,
|
|
14097
|
+
access: itemOpt.access || existingItem?.access
|
|
14098
|
+
};
|
|
14099
|
+
if (existingIndex >= 0) {
|
|
14100
|
+
group.items[existingIndex] = compiledItem;
|
|
14101
|
+
} else {
|
|
14102
|
+
group.items.push(compiledItem);
|
|
14103
|
+
}
|
|
14104
|
+
}
|
|
14105
|
+
const allAvailableItems = [];
|
|
14106
|
+
const reconciledGroups = [];
|
|
14107
|
+
for (const group of groupsList) {
|
|
14108
|
+
const isGroupHidden = hiddenSet.has(group.id) || !!group.slug && hiddenSet.has(group.slug) || hiddenSet.has(group.name);
|
|
14109
|
+
if (!includeHidden && isGroupHidden) {
|
|
14110
|
+
continue;
|
|
14111
|
+
}
|
|
14112
|
+
const visibleItems = [];
|
|
14113
|
+
for (const item of group.items) {
|
|
14114
|
+
const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
|
|
14115
|
+
if (!includeHidden && isItemHidden) {
|
|
14116
|
+
continue;
|
|
14117
|
+
}
|
|
14118
|
+
const allViews = item.views || [];
|
|
14119
|
+
const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
|
|
14120
|
+
const compoundKey = `${item.slug}_${v.slug}`;
|
|
14121
|
+
return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
|
|
14122
|
+
});
|
|
14123
|
+
let isTombstone = false;
|
|
14124
|
+
if (schemas && schemas.collections && schemas.collections.length > 0) {
|
|
14125
|
+
if (item.collection && !knownCollectionSlugs.has(item.collection)) {
|
|
14126
|
+
isTombstone = true;
|
|
14127
|
+
}
|
|
14128
|
+
}
|
|
14129
|
+
if (schemas && schemas.globals && schemas.globals.length > 0) {
|
|
14130
|
+
if (item.global && !knownGlobalSlugs.has(item.global)) {
|
|
14131
|
+
isTombstone = true;
|
|
14132
|
+
}
|
|
14133
|
+
}
|
|
14134
|
+
const processedItem = {
|
|
14135
|
+
...item,
|
|
14136
|
+
views: visibleViews,
|
|
14137
|
+
...isTombstone ? { isTombstone: true } : {}
|
|
14138
|
+
};
|
|
14139
|
+
visibleItems.push(processedItem);
|
|
14140
|
+
allAvailableItems.push(processedItem);
|
|
14141
|
+
}
|
|
14142
|
+
const groupKey = group.slug || group.name;
|
|
14143
|
+
const itemOrderList = effectivePrefs.itemOrder?.[group.id] || effectivePrefs.itemOrder?.[groupKey];
|
|
14144
|
+
if (itemOrderList && itemOrderList.length > 0) {
|
|
14145
|
+
visibleItems.sort((a, b) => {
|
|
14146
|
+
const aIdx = itemOrderList.indexOf(a.id) >= 0 ? itemOrderList.indexOf(a.id) : a.slug ? itemOrderList.indexOf(a.slug) : -1;
|
|
14147
|
+
const bIdx = itemOrderList.indexOf(b.id) >= 0 ? itemOrderList.indexOf(b.id) : b.slug ? itemOrderList.indexOf(b.slug) : -1;
|
|
14148
|
+
if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
|
|
14149
|
+
if (aIdx >= 0) return -1;
|
|
14150
|
+
if (bIdx >= 0) return 1;
|
|
14151
|
+
return (a.order ?? 100) - (b.order ?? 100);
|
|
14152
|
+
});
|
|
14153
|
+
} else {
|
|
14154
|
+
visibleItems.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
|
|
14155
|
+
}
|
|
14156
|
+
reconciledGroups.push({
|
|
14157
|
+
...group,
|
|
14158
|
+
items: visibleItems
|
|
14159
|
+
});
|
|
14160
|
+
}
|
|
14161
|
+
if (effectivePrefs.groupOrder && effectivePrefs.groupOrder.length > 0) {
|
|
14162
|
+
reconciledGroups.sort((a, b) => {
|
|
14163
|
+
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);
|
|
14164
|
+
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);
|
|
14165
|
+
if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
|
|
14166
|
+
if (aIdx >= 0) return -1;
|
|
14167
|
+
if (bIdx >= 0) return 1;
|
|
14168
|
+
return (a.order ?? 100) - (b.order ?? 100);
|
|
14169
|
+
});
|
|
14170
|
+
} else {
|
|
14171
|
+
reconciledGroups.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
|
|
14172
|
+
}
|
|
14173
|
+
const reconciledUngrouped = (baseTree.ungrouped || []).filter((item) => {
|
|
14174
|
+
const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
|
|
14175
|
+
return includeHidden || !isItemHidden;
|
|
14176
|
+
}).map((item) => {
|
|
14177
|
+
const allViews = item.views || [];
|
|
14178
|
+
const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
|
|
14179
|
+
const compoundKey = `${item.slug}_${v.slug}`;
|
|
14180
|
+
return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
|
|
14181
|
+
});
|
|
14182
|
+
const processed = { ...item, views: visibleViews };
|
|
14183
|
+
allAvailableItems.push(processed);
|
|
14184
|
+
return processed;
|
|
14185
|
+
});
|
|
14186
|
+
const pinnedItems = [];
|
|
14187
|
+
for (const pin of effectivePrefs.pinned || []) {
|
|
14188
|
+
const match = allAvailableItems.find((i) => i.slug === pin.slug || i.id === pin.slug);
|
|
14189
|
+
if (match) {
|
|
14190
|
+
pinnedItems.push({
|
|
14191
|
+
...match,
|
|
14192
|
+
id: `pin_${match.id}`,
|
|
14193
|
+
label: pin.label || match.label,
|
|
14194
|
+
icon: pin.icon || match.icon
|
|
14195
|
+
});
|
|
14196
|
+
} else {
|
|
14197
|
+
pinnedItems.push({
|
|
14198
|
+
id: `pin_${pin.slug}`,
|
|
14199
|
+
type: pin.type,
|
|
14200
|
+
slug: pin.slug,
|
|
14201
|
+
label: pin.label || pin.slug,
|
|
14202
|
+
icon: pin.icon || "Star",
|
|
14203
|
+
order: 0,
|
|
14204
|
+
views: []
|
|
14205
|
+
});
|
|
14206
|
+
}
|
|
14207
|
+
}
|
|
14208
|
+
return {
|
|
14209
|
+
groups: reconciledGroups,
|
|
14210
|
+
ungrouped: reconciledUngrouped,
|
|
14211
|
+
pinnedItems
|
|
14212
|
+
};
|
|
14213
|
+
}
|
|
14214
|
+
|
|
14215
|
+
// src/utils/navigation.ts
|
|
14216
|
+
var RESERVED_NAVIGATION_SLUGS = /* @__PURE__ */ new Set([
|
|
14217
|
+
"collections",
|
|
14218
|
+
"globals",
|
|
14219
|
+
"setup",
|
|
14220
|
+
"api",
|
|
14221
|
+
"login",
|
|
14222
|
+
"logout",
|
|
14223
|
+
"settings",
|
|
14224
|
+
"admin",
|
|
14225
|
+
"preferences",
|
|
14226
|
+
"auth"
|
|
14227
|
+
]);
|
|
14228
|
+
function assertValidNavigationSlugs(navigation) {
|
|
14229
|
+
for (const item of navigation) {
|
|
14230
|
+
if (item.slug) {
|
|
14231
|
+
const normalizedSlug = item.slug.toLowerCase().trim().replace(/^\//, "");
|
|
14232
|
+
if (RESERVED_NAVIGATION_SLUGS.has(normalizedSlug)) {
|
|
14233
|
+
throw new Error(
|
|
14234
|
+
`[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.`
|
|
14235
|
+
);
|
|
14236
|
+
}
|
|
14237
|
+
}
|
|
14238
|
+
}
|
|
14239
|
+
}
|
|
14240
|
+
function resolveGroupMeta(groupProp, defaultGroupName = "Collections") {
|
|
14241
|
+
if (!groupProp) {
|
|
14242
|
+
return { name: defaultGroupName };
|
|
14243
|
+
}
|
|
14244
|
+
if (typeof groupProp === "string") {
|
|
14245
|
+
return { name: groupProp };
|
|
14246
|
+
}
|
|
14247
|
+
return { name: groupProp.name, meta: groupProp };
|
|
14248
|
+
}
|
|
14249
|
+
function compileNavigation(config) {
|
|
14250
|
+
const collections = config.collections || [];
|
|
14251
|
+
const globals = config.globals || [];
|
|
14252
|
+
const explicitNav = config.admin?.navigation || [];
|
|
14253
|
+
assertValidNavigationSlugs(explicitNav);
|
|
14254
|
+
const collectionsBySlug = /* @__PURE__ */ new Map();
|
|
14255
|
+
for (const col of collections) {
|
|
14256
|
+
collectionsBySlug.set(col.slug, col);
|
|
14257
|
+
}
|
|
14258
|
+
const globalsBySlug = /* @__PURE__ */ new Map();
|
|
14259
|
+
for (const gl of globals) {
|
|
14260
|
+
globalsBySlug.set(gl.slug, gl);
|
|
14261
|
+
}
|
|
14262
|
+
const mentionedCollections = /* @__PURE__ */ new Set();
|
|
14263
|
+
const mentionedGlobals = /* @__PURE__ */ new Set();
|
|
14264
|
+
const injectedViewsByCollection = /* @__PURE__ */ new Map();
|
|
14265
|
+
for (const item of explicitNav) {
|
|
14266
|
+
if (item.addToCollection) {
|
|
14267
|
+
const existing = injectedViewsByCollection.get(item.addToCollection) || [];
|
|
14268
|
+
if (item.views && item.views.length > 0) {
|
|
14269
|
+
injectedViewsByCollection.set(item.addToCollection, [...existing, ...item.views]);
|
|
14270
|
+
}
|
|
14271
|
+
}
|
|
14272
|
+
if (item.collection) mentionedCollections.add(item.collection);
|
|
14273
|
+
if (item.global) mentionedGlobals.add(item.global);
|
|
14274
|
+
}
|
|
14275
|
+
const groupMetadataMap = /* @__PURE__ */ new Map();
|
|
14276
|
+
const itemsByGroup = /* @__PURE__ */ new Map();
|
|
14277
|
+
const ungroupedItems = [];
|
|
14278
|
+
const registerItem = (groupName, item) => {
|
|
14279
|
+
if (!groupName) {
|
|
14280
|
+
ungroupedItems.push(item);
|
|
14281
|
+
} else {
|
|
14282
|
+
const list = itemsByGroup.get(groupName) || [];
|
|
14283
|
+
list.push(item);
|
|
14284
|
+
itemsByGroup.set(groupName, list);
|
|
14285
|
+
}
|
|
14286
|
+
};
|
|
14287
|
+
for (const item of explicitNav) {
|
|
14288
|
+
if (item.addToCollection && !item.slug && !item.collection && !item.global && !item.dashboard && !item.href) {
|
|
14289
|
+
continue;
|
|
14290
|
+
}
|
|
14291
|
+
const { name: groupName, meta: groupMeta } = resolveGroupMeta(item.group, void 0);
|
|
14292
|
+
if (groupName && groupMeta) {
|
|
14293
|
+
groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
|
|
14294
|
+
}
|
|
14295
|
+
if (item.dashboard) {
|
|
14296
|
+
registerItem(groupName, {
|
|
14297
|
+
id: "dashboard",
|
|
14298
|
+
type: "dashboard",
|
|
14299
|
+
slug: "dashboard",
|
|
14300
|
+
label: item.label || "Dashboard",
|
|
14301
|
+
icon: item.icon || "LayoutDashboard",
|
|
14302
|
+
group: groupName,
|
|
14303
|
+
order: item.order ?? 0,
|
|
14304
|
+
views: [],
|
|
14305
|
+
access: item.access
|
|
14306
|
+
});
|
|
14307
|
+
continue;
|
|
14308
|
+
}
|
|
14309
|
+
if (item.collection) {
|
|
14310
|
+
const col = collectionsBySlug.get(item.collection);
|
|
14311
|
+
const combinedViews = [
|
|
14312
|
+
...col?.views || [],
|
|
14313
|
+
...injectedViewsByCollection.get(item.collection) || [],
|
|
14314
|
+
...item.views || []
|
|
14315
|
+
];
|
|
14316
|
+
registerItem(groupName, {
|
|
14317
|
+
id: item.slug || `collection_${item.collection}`,
|
|
14318
|
+
type: item.slug ? "workspace" : "collection",
|
|
14319
|
+
slug: item.slug || item.collection,
|
|
14320
|
+
collection: item.collection,
|
|
14321
|
+
label: item.label || col?.labels?.plural || item.collection,
|
|
14322
|
+
icon: item.icon || col?.admin?.icon || "Folder",
|
|
14323
|
+
group: groupName,
|
|
14324
|
+
order: item.order ?? col?.admin?.order ?? 100,
|
|
14325
|
+
views: combinedViews,
|
|
14326
|
+
badge: item.badge,
|
|
14327
|
+
access: item.access || col?.access?.read
|
|
14328
|
+
});
|
|
14329
|
+
continue;
|
|
14330
|
+
}
|
|
14331
|
+
if (item.global) {
|
|
14332
|
+
const gl = globalsBySlug.get(item.global);
|
|
14333
|
+
registerItem(groupName, {
|
|
14334
|
+
id: item.slug || `global_${item.global}`,
|
|
14335
|
+
type: "global",
|
|
14336
|
+
slug: item.slug || item.global,
|
|
14337
|
+
global: item.global,
|
|
14338
|
+
label: item.label || gl?.label || item.global,
|
|
14339
|
+
icon: item.icon || "Settings",
|
|
14340
|
+
group: groupName,
|
|
14341
|
+
order: item.order ?? 100,
|
|
14342
|
+
views: [],
|
|
14343
|
+
badge: item.badge,
|
|
14344
|
+
access: item.access
|
|
14345
|
+
});
|
|
14346
|
+
continue;
|
|
14347
|
+
}
|
|
14348
|
+
if (item.href) {
|
|
14349
|
+
registerItem(groupName, {
|
|
14350
|
+
id: `link_${item.slug || item.href}`,
|
|
14351
|
+
type: "link",
|
|
14352
|
+
slug: item.slug || "link",
|
|
14353
|
+
label: item.label || "Link",
|
|
14354
|
+
href: item.href,
|
|
14355
|
+
icon: item.icon || "ExternalLink",
|
|
14356
|
+
group: groupName,
|
|
14357
|
+
order: item.order ?? 100,
|
|
14358
|
+
views: [],
|
|
14359
|
+
badge: item.badge,
|
|
14360
|
+
access: item.access
|
|
14361
|
+
});
|
|
14362
|
+
continue;
|
|
14363
|
+
}
|
|
14364
|
+
const workspaceSlug = item.slug || "workspace";
|
|
14365
|
+
registerItem(groupName, {
|
|
14366
|
+
id: `workspace_${workspaceSlug}`,
|
|
14367
|
+
type: "workspace",
|
|
14368
|
+
slug: workspaceSlug,
|
|
14369
|
+
label: item.label || workspaceSlug,
|
|
14370
|
+
icon: item.icon || "Briefcase",
|
|
14371
|
+
group: groupName,
|
|
14372
|
+
order: item.order ?? 100,
|
|
14373
|
+
views: item.views || [],
|
|
14374
|
+
badge: item.badge,
|
|
14375
|
+
access: item.access
|
|
14376
|
+
});
|
|
14377
|
+
}
|
|
14378
|
+
for (const col of collections) {
|
|
14379
|
+
if (mentionedCollections.has(col.slug)) continue;
|
|
14380
|
+
if (col.admin?.hidden) continue;
|
|
14381
|
+
const groupProp = col.admin?.group;
|
|
14382
|
+
const { name: groupName, meta: groupMeta } = resolveGroupMeta(groupProp, "Collections");
|
|
14383
|
+
if (groupMeta) {
|
|
14384
|
+
groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
|
|
14385
|
+
}
|
|
14386
|
+
const combinedViews = [
|
|
14387
|
+
...col.views || [],
|
|
14388
|
+
...injectedViewsByCollection.get(col.slug) || []
|
|
14389
|
+
];
|
|
14390
|
+
registerItem(groupName, {
|
|
14391
|
+
id: `collection_${col.slug}`,
|
|
14392
|
+
type: "collection",
|
|
14393
|
+
slug: col.slug,
|
|
14394
|
+
collection: col.slug,
|
|
14395
|
+
label: col.labels?.plural || col.slug,
|
|
14396
|
+
icon: col.admin?.icon || "Folder",
|
|
14397
|
+
group: groupName,
|
|
14398
|
+
order: col?.admin?.order ?? 100,
|
|
14399
|
+
views: combinedViews,
|
|
14400
|
+
access: col.access?.read
|
|
14401
|
+
});
|
|
14402
|
+
}
|
|
14403
|
+
for (const gl of globals) {
|
|
14404
|
+
if (mentionedGlobals.has(gl.slug)) continue;
|
|
14405
|
+
const groupName = "Configuration";
|
|
14406
|
+
registerItem(groupName, {
|
|
14407
|
+
id: `global_${gl.slug}`,
|
|
14408
|
+
type: "global",
|
|
14409
|
+
slug: gl.slug,
|
|
14410
|
+
global: gl.slug,
|
|
14411
|
+
label: gl.label || gl.slug,
|
|
14412
|
+
icon: "Settings",
|
|
14413
|
+
group: groupName,
|
|
14414
|
+
order: 100,
|
|
14415
|
+
views: []
|
|
14416
|
+
});
|
|
14417
|
+
}
|
|
14418
|
+
const sortAndSpliceItems = (items) => {
|
|
14419
|
+
const sorted = [...items].sort((a, b) => a.order - b.order);
|
|
14420
|
+
const explicitMap = /* @__PURE__ */ new Map();
|
|
14421
|
+
for (const opt of explicitNav) {
|
|
14422
|
+
if (opt.slug) explicitMap.set(opt.slug, opt);
|
|
14423
|
+
if (opt.collection) explicitMap.set(opt.collection, opt);
|
|
14424
|
+
if (opt.global) explicitMap.set(opt.global, opt);
|
|
14425
|
+
}
|
|
14426
|
+
for (const opt of explicitNav) {
|
|
14427
|
+
const itemKey = opt.slug || opt.collection || opt.global;
|
|
14428
|
+
if (!itemKey) continue;
|
|
14429
|
+
if (opt.after) {
|
|
14430
|
+
const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
|
|
14431
|
+
const targetIdx = sorted.findIndex((i) => i.slug === opt.after);
|
|
14432
|
+
if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
|
|
14433
|
+
const [moved] = sorted.splice(itemIdx, 1);
|
|
14434
|
+
const newTargetIdx = sorted.findIndex((i) => i.slug === opt.after);
|
|
14435
|
+
sorted.splice(newTargetIdx + 1, 0, moved);
|
|
14436
|
+
}
|
|
14437
|
+
} else if (opt.before) {
|
|
14438
|
+
const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
|
|
14439
|
+
const targetIdx = sorted.findIndex((i) => i.slug === opt.before);
|
|
14440
|
+
if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
|
|
14441
|
+
const [moved] = sorted.splice(itemIdx, 1);
|
|
14442
|
+
const newTargetIdx = sorted.findIndex((i) => i.slug === opt.before);
|
|
14443
|
+
sorted.splice(newTargetIdx, 0, moved);
|
|
14444
|
+
}
|
|
14445
|
+
} else if (opt.position === "first") {
|
|
14446
|
+
const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
|
|
14447
|
+
if (itemIdx > 0) {
|
|
14448
|
+
const [moved] = sorted.splice(itemIdx, 1);
|
|
14449
|
+
sorted.unshift(moved);
|
|
14450
|
+
}
|
|
14451
|
+
} else if (opt.position === "last") {
|
|
14452
|
+
const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
|
|
14453
|
+
if (itemIdx !== -1 && itemIdx < sorted.length - 1) {
|
|
14454
|
+
const [moved] = sorted.splice(itemIdx, 1);
|
|
14455
|
+
sorted.push(moved);
|
|
14456
|
+
}
|
|
14457
|
+
}
|
|
14458
|
+
}
|
|
14459
|
+
return sorted;
|
|
14460
|
+
};
|
|
14461
|
+
const compiledGroups = [];
|
|
14462
|
+
for (const [groupName, rawItems] of itemsByGroup.entries()) {
|
|
14463
|
+
const meta = groupMetadataMap.get(groupName);
|
|
14464
|
+
const sortedItems = sortAndSpliceItems(rawItems);
|
|
14465
|
+
compiledGroups.push({
|
|
14466
|
+
id: `group_${groupName.toLowerCase().replace(/\s+/g, "_")}`,
|
|
14467
|
+
name: groupName,
|
|
14468
|
+
slug: meta?.slug || groupName.toLowerCase().replace(/\s+/g, "-"),
|
|
14469
|
+
icon: meta?.icon,
|
|
14470
|
+
defaultExpanded: meta?.defaultExpanded ?? true,
|
|
14471
|
+
order: meta?.order ?? 100,
|
|
14472
|
+
items: sortedItems
|
|
14473
|
+
});
|
|
14474
|
+
}
|
|
14475
|
+
compiledGroups.sort((a, b) => a.order - b.order);
|
|
14476
|
+
return {
|
|
14477
|
+
groups: compiledGroups,
|
|
14478
|
+
ungrouped: sortAndSpliceItems(ungroupedItems)
|
|
14479
|
+
};
|
|
14480
|
+
}
|
|
14481
|
+
function pruneNavigationForUser(tree, user) {
|
|
14482
|
+
const userRoles = /* @__PURE__ */ new Set();
|
|
14483
|
+
if (user?.role) userRoles.add(user.role);
|
|
14484
|
+
if (Array.isArray(user?.roles)) {
|
|
14485
|
+
for (const r of user.roles) userRoles.add(r);
|
|
14486
|
+
}
|
|
14487
|
+
const isAllowed = (item) => {
|
|
14488
|
+
if (!item.access) return true;
|
|
14489
|
+
if (Array.isArray(item.access)) {
|
|
14490
|
+
if (item.access.length === 0) return true;
|
|
14491
|
+
return item.access.some((role) => userRoles.has(role));
|
|
14492
|
+
}
|
|
14493
|
+
return true;
|
|
14494
|
+
};
|
|
14495
|
+
const prunedGroups = [];
|
|
14496
|
+
for (const group of tree.groups) {
|
|
14497
|
+
const visibleItems = group.items.filter(isAllowed);
|
|
14498
|
+
if (visibleItems.length > 0) {
|
|
14499
|
+
prunedGroups.push({
|
|
14500
|
+
...group,
|
|
14501
|
+
items: visibleItems
|
|
14502
|
+
});
|
|
14503
|
+
}
|
|
14504
|
+
}
|
|
14505
|
+
return {
|
|
14506
|
+
groups: prunedGroups,
|
|
14507
|
+
ungrouped: tree.ungrouped.filter(isAllowed)
|
|
14508
|
+
};
|
|
14509
|
+
}
|
|
14510
|
+
|
|
13832
14511
|
// src/router.ts
|
|
13833
14512
|
init_access_control();
|
|
13834
14513
|
var import_http_exception5 = require("hono/http-exception");
|
|
@@ -13977,6 +14656,30 @@ function serializeDetailForApi(detail) {
|
|
|
13977
14656
|
if (!detail || !Array.isArray(detail)) return void 0;
|
|
13978
14657
|
return detail.map(serializeDetailItemForApi);
|
|
13979
14658
|
}
|
|
14659
|
+
async function serializeActionsForApi(actions, serializeAccess) {
|
|
14660
|
+
const serializeAccessConfig = async (access) => {
|
|
14661
|
+
if (!access) return void 0;
|
|
14662
|
+
const serialized = {};
|
|
14663
|
+
for (const key of ["read", "create", "update", "delete"]) {
|
|
14664
|
+
if (access[key] !== void 0) serialized[key] = await serializeAccess(access[key]);
|
|
14665
|
+
}
|
|
14666
|
+
return serialized;
|
|
14667
|
+
};
|
|
14668
|
+
return Promise.all(
|
|
14669
|
+
(actions || []).map(async (action) => ({
|
|
14670
|
+
name: action.name,
|
|
14671
|
+
label: action.label,
|
|
14672
|
+
icon: action.icon,
|
|
14673
|
+
type: action.type ?? "row",
|
|
14674
|
+
confirm: action.confirm,
|
|
14675
|
+
submitLabel: action.submitLabel,
|
|
14676
|
+
fields: action.fields?.map(serializeFieldForApi),
|
|
14677
|
+
mutation: action.mutation,
|
|
14678
|
+
// Self-hosted handlers are intentionally omitted: they never leave the server.
|
|
14679
|
+
access: action.access ? await serializeAccessConfig(action.access) : void 0
|
|
14680
|
+
}))
|
|
14681
|
+
);
|
|
14682
|
+
}
|
|
13980
14683
|
async function serializeViewForApi(view, serializeAccess) {
|
|
13981
14684
|
if (!view) return view;
|
|
13982
14685
|
const serializeAccessConfig = async (access) => {
|
|
@@ -14003,20 +14706,7 @@ async function serializeViewForApi(view, serializeAccess) {
|
|
|
14003
14706
|
features: view.features,
|
|
14004
14707
|
actionOrder: view.actionOrder,
|
|
14005
14708
|
components: view.components,
|
|
14006
|
-
actions: await
|
|
14007
|
-
(view.actions || []).map(async (action) => ({
|
|
14008
|
-
name: action.name,
|
|
14009
|
-
label: action.label,
|
|
14010
|
-
icon: action.icon,
|
|
14011
|
-
type: action.type ?? "row",
|
|
14012
|
-
confirm: action.confirm,
|
|
14013
|
-
submitLabel: action.submitLabel,
|
|
14014
|
-
fields: action.fields?.map(serializeFieldForApi),
|
|
14015
|
-
mutation: action.mutation,
|
|
14016
|
-
// Self-hosted handlers are intentionally omitted: they never leave the server.
|
|
14017
|
-
access: action.access ? await serializeAccessConfig(action.access) : void 0
|
|
14018
|
-
}))
|
|
14019
|
-
),
|
|
14709
|
+
actions: await serializeActionsForApi(view.actions, serializeAccess),
|
|
14020
14710
|
access: view.access ? await serializeAccessConfig(view.access) : void 0
|
|
14021
14711
|
};
|
|
14022
14712
|
}
|
|
@@ -14098,6 +14788,7 @@ function registerRoutes(app, config) {
|
|
|
14098
14788
|
drafts: !!col.drafts,
|
|
14099
14789
|
defaultView: col.defaultView ?? col.admin?.defaultView,
|
|
14100
14790
|
views: await Promise.all((col.views || []).map((view) => serializeViewForApi(view, serializeAccess))),
|
|
14791
|
+
actions: await serializeActionsForApi(col.actions, serializeAccess),
|
|
14101
14792
|
admin: col.admin,
|
|
14102
14793
|
detail: serializeDetailForApi(col.detail),
|
|
14103
14794
|
workflow: col.workflow ? {
|
|
@@ -14328,6 +15019,7 @@ function registerRoutes(app, config) {
|
|
|
14328
15019
|
const user = c.get("user");
|
|
14329
15020
|
const key = c.req.param("key");
|
|
14330
15021
|
const scope = c.req.query("scope");
|
|
15022
|
+
const roleParam = c.req.query("role");
|
|
14331
15023
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
14332
15024
|
if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
|
|
14333
15025
|
const getGlobalPreference = async () => {
|
|
@@ -14337,27 +15029,61 @@ function registerRoutes(app, config) {
|
|
|
14337
15029
|
});
|
|
14338
15030
|
return globalDoc ? globalDoc.value : null;
|
|
14339
15031
|
};
|
|
14340
|
-
|
|
15032
|
+
const getRolePreference = async (r) => {
|
|
15033
|
+
const roleDoc = await db.findOne({
|
|
15034
|
+
collection: "__role_preferences",
|
|
15035
|
+
id: `${r}_${key}`
|
|
15036
|
+
});
|
|
15037
|
+
return roleDoc ? roleDoc.value : null;
|
|
15038
|
+
};
|
|
15039
|
+
const getUserPreferences = async () => {
|
|
15040
|
+
if (!user?.collection || !user.sub) return null;
|
|
15041
|
+
const doc = await db.findOne({ collection: user.collection, id: user.sub });
|
|
15042
|
+
if (!doc) return null;
|
|
15043
|
+
return typeof doc.__preferences === "object" && doc.__preferences !== null ? doc.__preferences : {};
|
|
15044
|
+
};
|
|
15045
|
+
if (scope === "global") {
|
|
14341
15046
|
const globalValue2 = await getGlobalPreference();
|
|
14342
15047
|
return c.json({ key, value: globalValue2 });
|
|
14343
15048
|
}
|
|
14344
|
-
|
|
14345
|
-
|
|
14346
|
-
|
|
14347
|
-
|
|
15049
|
+
if (scope === "role") {
|
|
15050
|
+
if (roleParam) {
|
|
15051
|
+
const val = await getRolePreference(roleParam);
|
|
15052
|
+
return c.json({ key, value: val, role: roleParam });
|
|
15053
|
+
}
|
|
15054
|
+
const userRoles2 = Array.isArray(user?.roles) ? user.roles : typeof user?.role === "string" ? [user.role] : [];
|
|
15055
|
+
for (const r of userRoles2) {
|
|
15056
|
+
const val = await getRolePreference(r);
|
|
15057
|
+
if (val !== null && val !== void 0) {
|
|
15058
|
+
return c.json({ key, value: val, role: r });
|
|
15059
|
+
}
|
|
15060
|
+
}
|
|
15061
|
+
return c.json({ key, value: null });
|
|
14348
15062
|
}
|
|
14349
|
-
|
|
14350
|
-
|
|
14351
|
-
return c.json({ key, value:
|
|
15063
|
+
if (scope === "personal") {
|
|
15064
|
+
const prefs2 = await getUserPreferences();
|
|
15065
|
+
return c.json({ key, value: prefs2 && key in prefs2 ? prefs2[key] ?? null : null });
|
|
15066
|
+
}
|
|
15067
|
+
const prefs = await getUserPreferences();
|
|
15068
|
+
if (prefs && key in prefs && prefs[key] !== null && prefs[key] !== void 0) {
|
|
15069
|
+
return c.json({ key, value: prefs[key] });
|
|
15070
|
+
}
|
|
15071
|
+
const userRoles = Array.isArray(user?.roles) ? user.roles : typeof user?.role === "string" ? [user.role] : [];
|
|
15072
|
+
for (const r of userRoles) {
|
|
15073
|
+
const val = await getRolePreference(r);
|
|
15074
|
+
if (val !== null && val !== void 0) {
|
|
15075
|
+
return c.json({ key, value: val });
|
|
15076
|
+
}
|
|
14352
15077
|
}
|
|
14353
15078
|
const globalValue = await getGlobalPreference();
|
|
14354
|
-
return c.json({ key, value: globalValue });
|
|
15079
|
+
return c.json({ key, value: globalValue ?? null });
|
|
14355
15080
|
});
|
|
14356
15081
|
app.put("/api/preferences/:key", requireAuth(config), async (c) => {
|
|
14357
15082
|
const db = config.db;
|
|
14358
15083
|
const user = c.get("user");
|
|
14359
15084
|
const key = c.req.param("key");
|
|
14360
|
-
const scope = c.req.query("scope");
|
|
15085
|
+
const scope = c.req.query("scope") || "personal";
|
|
15086
|
+
const roleParam = c.req.query("role");
|
|
14361
15087
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
14362
15088
|
if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
|
|
14363
15089
|
if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
|
|
@@ -14390,7 +15116,47 @@ function registerRoutes(app, config) {
|
|
|
14390
15116
|
data: { id: key, value: body.value }
|
|
14391
15117
|
});
|
|
14392
15118
|
}
|
|
14393
|
-
return c.json({ key, value: body.value });
|
|
15119
|
+
return c.json({ key, value: body.value, scope: "global" });
|
|
15120
|
+
}
|
|
15121
|
+
if (scope === "role") {
|
|
15122
|
+
const userCol = config.collections?.find((col) => col.slug === user?.collection);
|
|
15123
|
+
const isAdminUser = isUserAdmin(user, userCol);
|
|
15124
|
+
if (!isAdminUser) {
|
|
15125
|
+
return c.json(
|
|
15126
|
+
{
|
|
15127
|
+
error: true,
|
|
15128
|
+
message: "Only administrators can save role preferences."
|
|
15129
|
+
},
|
|
15130
|
+
403
|
|
15131
|
+
);
|
|
15132
|
+
}
|
|
15133
|
+
if (!roleParam) {
|
|
15134
|
+
return c.json(
|
|
15135
|
+
{
|
|
15136
|
+
error: true,
|
|
15137
|
+
message: "Role query parameter is required for role-scoped preferences."
|
|
15138
|
+
},
|
|
15139
|
+
400
|
|
15140
|
+
);
|
|
15141
|
+
}
|
|
15142
|
+
const roleDocId = `${roleParam}_${key}`;
|
|
15143
|
+
const existing = await db.findOne({
|
|
15144
|
+
collection: "__role_preferences",
|
|
15145
|
+
id: roleDocId
|
|
15146
|
+
});
|
|
15147
|
+
if (existing) {
|
|
15148
|
+
await db.update({
|
|
15149
|
+
collection: "__role_preferences",
|
|
15150
|
+
id: roleDocId,
|
|
15151
|
+
data: { value: body.value, role: roleParam, key }
|
|
15152
|
+
});
|
|
15153
|
+
} else {
|
|
15154
|
+
await db.create({
|
|
15155
|
+
collection: "__role_preferences",
|
|
15156
|
+
data: { id: roleDocId, role: roleParam, key, value: body.value }
|
|
15157
|
+
});
|
|
15158
|
+
}
|
|
15159
|
+
return c.json({ key, value: body.value, role: roleParam, scope: "role" });
|
|
14394
15160
|
}
|
|
14395
15161
|
const doc = await db.findOne({ collection: user.collection, id: user.sub });
|
|
14396
15162
|
if (!doc) return c.json({ error: true, message: "User not found." }, 404);
|
|
@@ -14401,13 +15167,14 @@ function registerRoutes(app, config) {
|
|
|
14401
15167
|
id: user.sub,
|
|
14402
15168
|
data: { __preferences: nextPreferences }
|
|
14403
15169
|
});
|
|
14404
|
-
return c.json({ key, value: body.value });
|
|
15170
|
+
return c.json({ key, value: body.value, scope: "personal" });
|
|
14405
15171
|
});
|
|
14406
15172
|
app.delete("/api/preferences/:key", requireAuth(config), async (c) => {
|
|
14407
15173
|
const db = config.db;
|
|
14408
15174
|
const user = c.get("user");
|
|
14409
15175
|
const key = c.req.param("key");
|
|
14410
|
-
const scope = c.req.query("scope");
|
|
15176
|
+
const scope = c.req.query("scope") || "personal";
|
|
15177
|
+
const roleParam = c.req.query("role");
|
|
14411
15178
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
14412
15179
|
if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
|
|
14413
15180
|
if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
|
|
@@ -14426,6 +15193,30 @@ function registerRoutes(app, config) {
|
|
|
14426
15193
|
await db.delete({ collection: "__global_preferences", id: key });
|
|
14427
15194
|
return c.json({ success: true });
|
|
14428
15195
|
}
|
|
15196
|
+
if (scope === "role") {
|
|
15197
|
+
const userCol = config.collections?.find((col) => col.slug === user?.collection);
|
|
15198
|
+
const isAdminUser = isUserAdmin(user, userCol);
|
|
15199
|
+
if (!isAdminUser) {
|
|
15200
|
+
return c.json(
|
|
15201
|
+
{
|
|
15202
|
+
error: true,
|
|
15203
|
+
message: "Only administrators can delete role preferences."
|
|
15204
|
+
},
|
|
15205
|
+
403
|
|
15206
|
+
);
|
|
15207
|
+
}
|
|
15208
|
+
if (!roleParam) {
|
|
15209
|
+
return c.json(
|
|
15210
|
+
{
|
|
15211
|
+
error: true,
|
|
15212
|
+
message: "Role query parameter is required for role-scoped preferences."
|
|
15213
|
+
},
|
|
15214
|
+
400
|
|
15215
|
+
);
|
|
15216
|
+
}
|
|
15217
|
+
await db.delete({ collection: "__role_preferences", id: `${roleParam}_${key}` });
|
|
15218
|
+
return c.json({ success: true });
|
|
15219
|
+
}
|
|
14429
15220
|
const doc = await db.findOne({ collection: user.collection, id: user.sub });
|
|
14430
15221
|
if (!doc) return c.json({ error: true, message: "User not found." }, 404);
|
|
14431
15222
|
const preferences = typeof doc.__preferences === "object" && doc.__preferences !== null ? { ...doc.__preferences } : {};
|
|
@@ -14437,6 +15228,198 @@ function registerRoutes(app, config) {
|
|
|
14437
15228
|
});
|
|
14438
15229
|
return c.json({ success: true });
|
|
14439
15230
|
});
|
|
15231
|
+
app.get("/api/admin/navigation", optionalAuth(config), async (c) => {
|
|
15232
|
+
const user = c.get("user");
|
|
15233
|
+
const compiled = compileNavigation(config);
|
|
15234
|
+
const pruned = pruneNavigationForUser(compiled, user);
|
|
15235
|
+
return c.json(pruned);
|
|
15236
|
+
});
|
|
15237
|
+
app.on(["GET", "POST"], "/api/admin/navigation/badges", optionalAuth(config), async (c) => {
|
|
15238
|
+
const db = config.db;
|
|
15239
|
+
if (!db) return c.json({ badges: {} });
|
|
15240
|
+
const user = c.get("user");
|
|
15241
|
+
const compiled = compileNavigation(config);
|
|
15242
|
+
let userNavPrefs = null;
|
|
15243
|
+
const prefsQuery = c.req.query("prefs");
|
|
15244
|
+
if (prefsQuery) {
|
|
15245
|
+
try {
|
|
15246
|
+
userNavPrefs = JSON.parse(decodeURIComponent(prefsQuery));
|
|
15247
|
+
} catch {
|
|
15248
|
+
}
|
|
15249
|
+
}
|
|
15250
|
+
if (!userNavPrefs && c.req.method === "POST") {
|
|
15251
|
+
try {
|
|
15252
|
+
const body = await c.req.json();
|
|
15253
|
+
if (body?.preferences) userNavPrefs = body.preferences;
|
|
15254
|
+
} catch {
|
|
15255
|
+
}
|
|
15256
|
+
}
|
|
15257
|
+
if (!userNavPrefs && user?.collection && user?.sub) {
|
|
15258
|
+
try {
|
|
15259
|
+
const userDoc = await db.findOne({ collection: user.collection, id: user.sub });
|
|
15260
|
+
if (userDoc?.__preferences && typeof userDoc.__preferences === "object") {
|
|
15261
|
+
userNavPrefs = userDoc.__preferences["admin:navigation"];
|
|
15262
|
+
}
|
|
15263
|
+
} catch {
|
|
15264
|
+
}
|
|
15265
|
+
}
|
|
15266
|
+
if (!userNavPrefs) {
|
|
15267
|
+
try {
|
|
15268
|
+
const globalDoc = await db.findOne({
|
|
15269
|
+
collection: "__global_preferences",
|
|
15270
|
+
id: "admin:navigation"
|
|
15271
|
+
});
|
|
15272
|
+
if (globalDoc?.value) {
|
|
15273
|
+
userNavPrefs = globalDoc.value;
|
|
15274
|
+
}
|
|
15275
|
+
} catch {
|
|
15276
|
+
}
|
|
15277
|
+
}
|
|
15278
|
+
let allItems;
|
|
15279
|
+
if (userNavPrefs) {
|
|
15280
|
+
const reconciled = reconcileNavigation(compiled, userNavPrefs, {
|
|
15281
|
+
collections: config.collections,
|
|
15282
|
+
globals: config.globals
|
|
15283
|
+
});
|
|
15284
|
+
allItems = [
|
|
15285
|
+
...reconciled.ungrouped || [],
|
|
15286
|
+
...reconciled.pinnedItems || [],
|
|
15287
|
+
...(reconciled.groups || []).flatMap((g) => g.items || [])
|
|
15288
|
+
];
|
|
15289
|
+
} else {
|
|
15290
|
+
const pruned = pruneNavigationForUser(compiled, user);
|
|
15291
|
+
allItems = [
|
|
15292
|
+
...pruned.ungrouped,
|
|
15293
|
+
...pruned.groups.flatMap((g) => g.items)
|
|
15294
|
+
];
|
|
15295
|
+
}
|
|
15296
|
+
const badges = {};
|
|
15297
|
+
const requestsByCollection = /* @__PURE__ */ new Map();
|
|
15298
|
+
let reqCounter = 0;
|
|
15299
|
+
for (const item of allItems) {
|
|
15300
|
+
if (item.badge) {
|
|
15301
|
+
const itemBadge = item.badge;
|
|
15302
|
+
const badgeKey = item.slug || item.collection || item.global || item.id;
|
|
15303
|
+
if (typeof itemBadge === "string") {
|
|
15304
|
+
badges[badgeKey] = { count: itemBadge, variant: "default" };
|
|
15305
|
+
} else {
|
|
15306
|
+
const badgeConfig = itemBadge;
|
|
15307
|
+
const variant = badgeConfig.variant ?? "default";
|
|
15308
|
+
if (badgeConfig.text) {
|
|
15309
|
+
badges[badgeKey] = { count: badgeConfig.text, variant };
|
|
15310
|
+
} else {
|
|
15311
|
+
let targetCol;
|
|
15312
|
+
let targetWhere;
|
|
15313
|
+
if (badgeConfig.aggregate) {
|
|
15314
|
+
targetCol = badgeConfig.aggregate.collection;
|
|
15315
|
+
targetWhere = badgeConfig.aggregate.where;
|
|
15316
|
+
} else if (badgeConfig.count) {
|
|
15317
|
+
targetCol = item.collection || item.views?.[0]?.collection;
|
|
15318
|
+
const primaryView = item.views?.[0];
|
|
15319
|
+
if (primaryView?.filter) {
|
|
15320
|
+
targetWhere = primaryView.filter;
|
|
15321
|
+
}
|
|
15322
|
+
}
|
|
15323
|
+
if (targetCol) {
|
|
15324
|
+
const reqId = `agg_${reqCounter++}`;
|
|
15325
|
+
const list = requestsByCollection.get(targetCol) || [];
|
|
15326
|
+
list.push({
|
|
15327
|
+
id: reqId,
|
|
15328
|
+
where: targetWhere,
|
|
15329
|
+
onResult: (count2) => {
|
|
15330
|
+
badges[badgeKey] = { count: count2, variant };
|
|
15331
|
+
}
|
|
15332
|
+
});
|
|
15333
|
+
requestsByCollection.set(targetCol, list);
|
|
15334
|
+
}
|
|
15335
|
+
}
|
|
15336
|
+
}
|
|
15337
|
+
}
|
|
15338
|
+
if (item.views && item.views.length > 0) {
|
|
15339
|
+
for (const view of item.views) {
|
|
15340
|
+
if (!view.badge) continue;
|
|
15341
|
+
const viewBadge = view.badge;
|
|
15342
|
+
const viewKey = `${item.slug}:${view.slug}`;
|
|
15343
|
+
const altKey = `${item.slug}_${view.slug}`;
|
|
15344
|
+
const plainKey = view.slug;
|
|
15345
|
+
const setBadgeResult = (res) => {
|
|
15346
|
+
badges[viewKey] = res;
|
|
15347
|
+
badges[altKey] = res;
|
|
15348
|
+
if (!badges[plainKey]) {
|
|
15349
|
+
badges[plainKey] = res;
|
|
15350
|
+
}
|
|
15351
|
+
if (view.collection) {
|
|
15352
|
+
badges[`${view.collection}:${view.slug}`] = res;
|
|
15353
|
+
badges[`${view.collection}_${view.slug}`] = res;
|
|
15354
|
+
}
|
|
15355
|
+
};
|
|
15356
|
+
if (typeof viewBadge === "string") {
|
|
15357
|
+
setBadgeResult({ count: viewBadge, variant: "default" });
|
|
15358
|
+
continue;
|
|
15359
|
+
}
|
|
15360
|
+
const badgeConfig = viewBadge;
|
|
15361
|
+
const variant = badgeConfig.variant ?? "default";
|
|
15362
|
+
if (badgeConfig.text) {
|
|
15363
|
+
setBadgeResult({ count: badgeConfig.text, variant });
|
|
15364
|
+
continue;
|
|
15365
|
+
}
|
|
15366
|
+
const targetCol = badgeConfig.aggregate?.collection || view.collection || item.collection || (item.type === "collection" ? item.slug : void 0);
|
|
15367
|
+
const targetWhere = badgeConfig.aggregate?.where || (typeof view.filter === "object" ? view.filter : void 0);
|
|
15368
|
+
if (!targetCol) continue;
|
|
15369
|
+
const reqId = `agg_${reqCounter++}`;
|
|
15370
|
+
const list = requestsByCollection.get(targetCol) || [];
|
|
15371
|
+
list.push({
|
|
15372
|
+
id: reqId,
|
|
15373
|
+
where: targetWhere,
|
|
15374
|
+
onResult: (count2) => {
|
|
15375
|
+
setBadgeResult({ count: count2, variant });
|
|
15376
|
+
}
|
|
15377
|
+
});
|
|
15378
|
+
requestsByCollection.set(targetCol, list);
|
|
15379
|
+
}
|
|
15380
|
+
}
|
|
15381
|
+
}
|
|
15382
|
+
const collectionTasks = Array.from(requestsByCollection.entries()).map(async ([col, reqs]) => {
|
|
15383
|
+
if (typeof db.aggregate === "function") {
|
|
15384
|
+
try {
|
|
15385
|
+
const aggregates = {};
|
|
15386
|
+
for (const req of reqs) {
|
|
15387
|
+
aggregates[req.id] = { count: "*", where: req.where };
|
|
15388
|
+
}
|
|
15389
|
+
const agg = await db.aggregate({
|
|
15390
|
+
collection: col,
|
|
15391
|
+
aggregates
|
|
15392
|
+
});
|
|
15393
|
+
for (const req of reqs) {
|
|
15394
|
+
const val = agg?.[req.id];
|
|
15395
|
+
req.onResult(typeof val === "number" ? val : 0);
|
|
15396
|
+
}
|
|
15397
|
+
} catch {
|
|
15398
|
+
for (const req of reqs) {
|
|
15399
|
+
req.onResult(0);
|
|
15400
|
+
}
|
|
15401
|
+
}
|
|
15402
|
+
} else {
|
|
15403
|
+
await Promise.allSettled(
|
|
15404
|
+
reqs.map(async (req) => {
|
|
15405
|
+
try {
|
|
15406
|
+
const res = await db.find({
|
|
15407
|
+
collection: col,
|
|
15408
|
+
where: req.where,
|
|
15409
|
+
limit: 1
|
|
15410
|
+
});
|
|
15411
|
+
const total = res?.totalDocs ?? res?.total ?? 0;
|
|
15412
|
+
req.onResult(total);
|
|
15413
|
+
} catch {
|
|
15414
|
+
req.onResult(0);
|
|
15415
|
+
}
|
|
15416
|
+
})
|
|
15417
|
+
);
|
|
15418
|
+
}
|
|
15419
|
+
});
|
|
15420
|
+
await Promise.allSettled(collectionTasks);
|
|
15421
|
+
return c.json({ badges });
|
|
15422
|
+
});
|
|
14440
15423
|
app.get("/api/media/folders", async (c) => {
|
|
14441
15424
|
const folderController = new MediaFolderController("media");
|
|
14442
15425
|
return folderController.list(c);
|
|
@@ -14686,6 +15669,31 @@ function registerRoutes(app, config) {
|
|
|
14686
15669
|
});
|
|
14687
15670
|
}
|
|
14688
15671
|
|
|
15672
|
+
// src/tasks.ts
|
|
15673
|
+
var TASK_LOCKS_COLLECTION = "__task_locks";
|
|
15674
|
+
var TASK_LOCKS_COLLECTION_CONFIG = {
|
|
15675
|
+
slug: TASK_LOCKS_COLLECTION,
|
|
15676
|
+
labels: { singular: "Task lock", plural: "Task locks" },
|
|
15677
|
+
fields: [
|
|
15678
|
+
{ name: "name", type: "text", required: true },
|
|
15679
|
+
// Numeric and promoted so the lock compare-and-set is a real column comparison.
|
|
15680
|
+
{ name: "lockedUntil", type: "number", required: true, promoted: true },
|
|
15681
|
+
{ name: "lockedBy", type: "text" },
|
|
15682
|
+
{ name: "lastScheduledFor", type: "date" },
|
|
15683
|
+
{ name: "lastStartedAt", type: "date" },
|
|
15684
|
+
{ name: "lastFinishedAt", type: "date" },
|
|
15685
|
+
{ name: "lastStatus", type: "text" },
|
|
15686
|
+
{ name: "lastError", type: "textarea" }
|
|
15687
|
+
],
|
|
15688
|
+
access: {
|
|
15689
|
+
read: ({ user }) => !!user?.roles?.includes("admin"),
|
|
15690
|
+
create: () => false,
|
|
15691
|
+
update: () => false,
|
|
15692
|
+
delete: () => false
|
|
15693
|
+
},
|
|
15694
|
+
admin: { hidden: true }
|
|
15695
|
+
};
|
|
15696
|
+
|
|
14689
15697
|
// src/utils/config.ts
|
|
14690
15698
|
var AUDIT_COLLECTION_SLUG = "__audit";
|
|
14691
15699
|
var SYSTEM_FIELDS = [
|
|
@@ -14837,6 +15845,7 @@ function normalizeConfig(config) {
|
|
|
14837
15845
|
const globals = schemaAwareConfig?.globals || [];
|
|
14838
15846
|
const needsAudit = collections.some((col) => col.audit);
|
|
14839
15847
|
const needsWorkflow = collections.some((col) => col.workflow || col.drafts);
|
|
15848
|
+
const needsTasks = (config.tasks?.length ?? 0) > 0;
|
|
14840
15849
|
const needsAuthSessions = collections.some((col) => !!col.auth);
|
|
14841
15850
|
const adminAuthCollectionSlug = getAdminAuthCollection({
|
|
14842
15851
|
collections,
|
|
@@ -14916,7 +15925,33 @@ function normalizeConfig(config) {
|
|
|
14916
15925
|
}
|
|
14917
15926
|
];
|
|
14918
15927
|
}
|
|
15928
|
+
if (!existingFieldNames.has("__preferences")) {
|
|
15929
|
+
fields = [
|
|
15930
|
+
...fields,
|
|
15931
|
+
{
|
|
15932
|
+
name: "__preferences",
|
|
15933
|
+
type: "json",
|
|
15934
|
+
label: "Preferences",
|
|
15935
|
+
promoted: true,
|
|
15936
|
+
admin: {
|
|
15937
|
+
hidden: true,
|
|
15938
|
+
readOnly: true
|
|
15939
|
+
}
|
|
15940
|
+
}
|
|
15941
|
+
];
|
|
15942
|
+
}
|
|
14919
15943
|
fields = fields.map((field) => {
|
|
15944
|
+
if (field.name === "__preferences") {
|
|
15945
|
+
return {
|
|
15946
|
+
...field,
|
|
15947
|
+
promoted: true,
|
|
15948
|
+
admin: {
|
|
15949
|
+
...field.admin || {},
|
|
15950
|
+
hidden: true,
|
|
15951
|
+
readOnly: true
|
|
15952
|
+
}
|
|
15953
|
+
};
|
|
15954
|
+
}
|
|
14920
15955
|
if (field.name === "email") {
|
|
14921
15956
|
return {
|
|
14922
15957
|
...field,
|
|
@@ -15043,6 +16078,9 @@ function normalizeConfig(config) {
|
|
|
15043
16078
|
if (needsWorkflow && !normalizedCollections.some((col) => col.slug === LIFECYCLE_EVENTS_COLLECTION)) {
|
|
15044
16079
|
systemCollections.push(LIFECYCLE_EVENTS_COLLECTION_CONFIG);
|
|
15045
16080
|
}
|
|
16081
|
+
if (needsTasks && !normalizedCollections.some((col) => col.slug === TASK_LOCKS_COLLECTION)) {
|
|
16082
|
+
systemCollections.push(TASK_LOCKS_COLLECTION_CONFIG);
|
|
16083
|
+
}
|
|
15046
16084
|
if (needsAuthSessions && !normalizedCollections.some((col) => col.slug === AUTH_SESSIONS_COLLECTION)) {
|
|
15047
16085
|
systemCollections.push(AUTH_SESSIONS_COLLECTION_CONFIG);
|
|
15048
16086
|
}
|
|
@@ -15413,6 +16451,7 @@ init_embedding_service();
|
|
|
15413
16451
|
revokeAllAuthSessions,
|
|
15414
16452
|
revokeAuthSession,
|
|
15415
16453
|
sendEmail,
|
|
16454
|
+
serializeActionsForApi,
|
|
15416
16455
|
serializeDetailForApi,
|
|
15417
16456
|
serializeViewForApi,
|
|
15418
16457
|
shouldSampleRequest,
|