@dyrected/core 2.16.0 → 2.17.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/server.cjs CHANGED
@@ -13829,6 +13829,537 @@ function getSwaggerHtml(specUrl) {
13829
13829
  `;
13830
13830
  }
13831
13831
 
13832
+ // src/types/navigation.ts
13833
+ var CURRENT_NAV_PREFERENCES_VERSION = 1;
13834
+ var DEFAULT_USER_NAV_PREFERENCES = {
13835
+ _version: CURRENT_NAV_PREFERENCES_VERSION,
13836
+ pinned: [],
13837
+ hidden: [],
13838
+ groupOrder: [],
13839
+ itemOrder: {},
13840
+ groups: [],
13841
+ items: []
13842
+ };
13843
+
13844
+ // src/utils/navigation-reconciler.ts
13845
+ var normalizeKey = (val) => (val || "").toLowerCase().replace(/[^a-z0-9]/g, "-");
13846
+ function reconcileNavigation(baseTree, prefs, schemas, options) {
13847
+ if (!baseTree) {
13848
+ return {
13849
+ groups: [],
13850
+ ungrouped: [],
13851
+ pinnedItems: []
13852
+ };
13853
+ }
13854
+ const includeHidden = options?.includeHidden ?? false;
13855
+ const effectivePrefs = prefs || DEFAULT_USER_NAV_PREFERENCES;
13856
+ const hiddenSet = new Set(effectivePrefs.hidden || []);
13857
+ const knownCollectionSlugs = new Set((schemas?.collections || []).map((c) => c.slug));
13858
+ const knownGlobalSlugs = new Set((schemas?.globals || []).map((g) => g.slug));
13859
+ const groupsList = [];
13860
+ const findGroup = (query) => {
13861
+ const qNorm = normalizeKey(query);
13862
+ return groupsList.find(
13863
+ (g) => g.id === query || g.slug === query || g.name === query || normalizeKey(g.id) === qNorm || normalizeKey(g.slug) === qNorm || normalizeKey(g.name) === qNorm
13864
+ );
13865
+ };
13866
+ for (const g of baseTree.groups || []) {
13867
+ groupsList.push({
13868
+ ...g,
13869
+ items: (g.items || []).map((item) => ({ ...item }))
13870
+ });
13871
+ }
13872
+ for (const ug of effectivePrefs.groups || []) {
13873
+ const existing = findGroup(ug.slug || ug.name);
13874
+ if (!existing) {
13875
+ groupsList.push({
13876
+ id: ug.slug ? `group_${ug.slug}` : `group_${normalizeKey(ug.name)}`,
13877
+ name: ug.name,
13878
+ slug: ug.slug || normalizeKey(ug.name),
13879
+ icon: ug.icon || "Folder",
13880
+ defaultExpanded: ug.defaultExpanded ?? true,
13881
+ order: ug.order ?? 100,
13882
+ items: []
13883
+ });
13884
+ }
13885
+ }
13886
+ for (const itemOpt of effectivePrefs.items || []) {
13887
+ const targetGroup = typeof itemOpt.group === "object" ? itemOpt.group?.name : itemOpt.group;
13888
+ const groupKey = targetGroup || "Custom";
13889
+ let group = findGroup(groupKey);
13890
+ if (!group) {
13891
+ group = {
13892
+ id: `group_${normalizeKey(groupKey)}`,
13893
+ name: groupKey,
13894
+ slug: normalizeKey(groupKey),
13895
+ icon: "Folder",
13896
+ defaultExpanded: true,
13897
+ order: 99,
13898
+ items: []
13899
+ };
13900
+ groupsList.push(group);
13901
+ }
13902
+ const itemId = itemOpt.slug ? `workspace_${itemOpt.slug}` : itemOpt.collection ? `collection_${itemOpt.collection}` : `custom_${Math.random()}`;
13903
+ for (const otherGroup of groupsList) {
13904
+ if (otherGroup !== group) {
13905
+ otherGroup.items = otherGroup.items.filter((i) => i.slug !== itemOpt.slug && i.id !== itemId);
13906
+ }
13907
+ }
13908
+ const existingIndex = group.items.findIndex((i) => i.id === itemId || i.slug === itemOpt.slug);
13909
+ const existingItem = existingIndex >= 0 ? group.items[existingIndex] : void 0;
13910
+ const col = schemas?.collections?.find((c) => c.slug === itemOpt.collection);
13911
+ const colViews = col?.views || [];
13912
+ const baseViews = existingItem?.views || [];
13913
+ const userViews = itemOpt.views || [];
13914
+ const userViewSlugs = new Set(userViews.map((v) => v.slug));
13915
+ let mergedViews = [
13916
+ ...baseViews.filter((v) => !userViewSlugs.has(v.slug)),
13917
+ ...userViews
13918
+ ];
13919
+ if (mergedViews.length === 0 && itemOpt.collection) {
13920
+ if (colViews.length > 0) {
13921
+ mergedViews = [...colViews];
13922
+ } else {
13923
+ mergedViews = [
13924
+ {
13925
+ slug: "default",
13926
+ label: col?.label || col?.labels?.plural || itemOpt.label || "All Records",
13927
+ layout: "table",
13928
+ collection: itemOpt.collection
13929
+ }
13930
+ ];
13931
+ }
13932
+ }
13933
+ mergedViews = mergedViews.map((v) => ({
13934
+ ...v,
13935
+ collection: v.collection || itemOpt.collection || existingItem?.collection
13936
+ }));
13937
+ const compiledItem = {
13938
+ id: itemId,
13939
+ type: itemOpt.slug ? "workspace" : itemOpt.collection ? "collection" : itemOpt.global ? "global" : "workspace",
13940
+ slug: itemOpt.slug || itemOpt.collection || itemOpt.global || "item",
13941
+ label: itemOpt.label || itemOpt.slug || "Item",
13942
+ icon: itemOpt.icon || existingItem?.icon || col?.admin?.icon || "Briefcase",
13943
+ group: group.name,
13944
+ order: itemOpt.order ?? existingItem?.order ?? 100,
13945
+ views: mergedViews,
13946
+ collection: itemOpt.collection || existingItem?.collection,
13947
+ global: itemOpt.global || existingItem?.global,
13948
+ badge: itemOpt.badge || existingItem?.badge,
13949
+ access: itemOpt.access || existingItem?.access
13950
+ };
13951
+ if (existingIndex >= 0) {
13952
+ group.items[existingIndex] = compiledItem;
13953
+ } else {
13954
+ group.items.push(compiledItem);
13955
+ }
13956
+ }
13957
+ const allAvailableItems = [];
13958
+ const reconciledGroups = [];
13959
+ for (const group of groupsList) {
13960
+ const isGroupHidden = hiddenSet.has(group.id) || !!group.slug && hiddenSet.has(group.slug) || hiddenSet.has(group.name);
13961
+ if (!includeHidden && isGroupHidden) {
13962
+ continue;
13963
+ }
13964
+ const visibleItems = [];
13965
+ for (const item of group.items) {
13966
+ const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
13967
+ if (!includeHidden && isItemHidden) {
13968
+ continue;
13969
+ }
13970
+ const allViews = item.views || [];
13971
+ const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
13972
+ const compoundKey = `${item.slug}_${v.slug}`;
13973
+ return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
13974
+ });
13975
+ let isTombstone = false;
13976
+ if (schemas && schemas.collections && schemas.collections.length > 0) {
13977
+ if (item.collection && !knownCollectionSlugs.has(item.collection)) {
13978
+ isTombstone = true;
13979
+ }
13980
+ }
13981
+ if (schemas && schemas.globals && schemas.globals.length > 0) {
13982
+ if (item.global && !knownGlobalSlugs.has(item.global)) {
13983
+ isTombstone = true;
13984
+ }
13985
+ }
13986
+ const processedItem = {
13987
+ ...item,
13988
+ views: visibleViews,
13989
+ ...isTombstone ? { isTombstone: true } : {}
13990
+ };
13991
+ visibleItems.push(processedItem);
13992
+ allAvailableItems.push(processedItem);
13993
+ }
13994
+ const groupKey = group.slug || group.name;
13995
+ const itemOrderList = effectivePrefs.itemOrder?.[group.id] || effectivePrefs.itemOrder?.[groupKey];
13996
+ if (itemOrderList && itemOrderList.length > 0) {
13997
+ visibleItems.sort((a, b) => {
13998
+ const aIdx = itemOrderList.indexOf(a.id) >= 0 ? itemOrderList.indexOf(a.id) : a.slug ? itemOrderList.indexOf(a.slug) : -1;
13999
+ const bIdx = itemOrderList.indexOf(b.id) >= 0 ? itemOrderList.indexOf(b.id) : b.slug ? itemOrderList.indexOf(b.slug) : -1;
14000
+ if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
14001
+ if (aIdx >= 0) return -1;
14002
+ if (bIdx >= 0) return 1;
14003
+ return (a.order ?? 100) - (b.order ?? 100);
14004
+ });
14005
+ } else {
14006
+ visibleItems.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
14007
+ }
14008
+ reconciledGroups.push({
14009
+ ...group,
14010
+ items: visibleItems
14011
+ });
14012
+ }
14013
+ if (effectivePrefs.groupOrder && effectivePrefs.groupOrder.length > 0) {
14014
+ reconciledGroups.sort((a, b) => {
14015
+ 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);
14016
+ 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);
14017
+ if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
14018
+ if (aIdx >= 0) return -1;
14019
+ if (bIdx >= 0) return 1;
14020
+ return (a.order ?? 100) - (b.order ?? 100);
14021
+ });
14022
+ } else {
14023
+ reconciledGroups.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
14024
+ }
14025
+ const reconciledUngrouped = (baseTree.ungrouped || []).filter((item) => {
14026
+ const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
14027
+ return includeHidden || !isItemHidden;
14028
+ }).map((item) => {
14029
+ const allViews = item.views || [];
14030
+ const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
14031
+ const compoundKey = `${item.slug}_${v.slug}`;
14032
+ return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
14033
+ });
14034
+ const processed = { ...item, views: visibleViews };
14035
+ allAvailableItems.push(processed);
14036
+ return processed;
14037
+ });
14038
+ const pinnedItems = [];
14039
+ for (const pin of effectivePrefs.pinned || []) {
14040
+ const match = allAvailableItems.find((i) => i.slug === pin.slug || i.id === pin.slug);
14041
+ if (match) {
14042
+ pinnedItems.push({
14043
+ ...match,
14044
+ id: `pin_${match.id}`,
14045
+ label: pin.label || match.label,
14046
+ icon: pin.icon || match.icon
14047
+ });
14048
+ } else {
14049
+ pinnedItems.push({
14050
+ id: `pin_${pin.slug}`,
14051
+ type: pin.type,
14052
+ slug: pin.slug,
14053
+ label: pin.label || pin.slug,
14054
+ icon: pin.icon || "Star",
14055
+ order: 0,
14056
+ views: []
14057
+ });
14058
+ }
14059
+ }
14060
+ return {
14061
+ groups: reconciledGroups,
14062
+ ungrouped: reconciledUngrouped,
14063
+ pinnedItems
14064
+ };
14065
+ }
14066
+
14067
+ // src/utils/navigation.ts
14068
+ var RESERVED_NAVIGATION_SLUGS = /* @__PURE__ */ new Set([
14069
+ "collections",
14070
+ "globals",
14071
+ "setup",
14072
+ "api",
14073
+ "login",
14074
+ "logout",
14075
+ "settings",
14076
+ "admin",
14077
+ "preferences",
14078
+ "auth"
14079
+ ]);
14080
+ function assertValidNavigationSlugs(navigation) {
14081
+ for (const item of navigation) {
14082
+ if (item.slug) {
14083
+ const normalizedSlug = item.slug.toLowerCase().trim().replace(/^\//, "");
14084
+ if (RESERVED_NAVIGATION_SLUGS.has(normalizedSlug)) {
14085
+ throw new Error(
14086
+ `[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.`
14087
+ );
14088
+ }
14089
+ }
14090
+ }
14091
+ }
14092
+ function resolveGroupMeta(groupProp, defaultGroupName = "Collections") {
14093
+ if (!groupProp) {
14094
+ return { name: defaultGroupName };
14095
+ }
14096
+ if (typeof groupProp === "string") {
14097
+ return { name: groupProp };
14098
+ }
14099
+ return { name: groupProp.name, meta: groupProp };
14100
+ }
14101
+ function compileNavigation(config) {
14102
+ const collections = config.collections || [];
14103
+ const globals = config.globals || [];
14104
+ const explicitNav = config.admin?.navigation || [];
14105
+ assertValidNavigationSlugs(explicitNav);
14106
+ const collectionsBySlug = /* @__PURE__ */ new Map();
14107
+ for (const col of collections) {
14108
+ collectionsBySlug.set(col.slug, col);
14109
+ }
14110
+ const globalsBySlug = /* @__PURE__ */ new Map();
14111
+ for (const gl of globals) {
14112
+ globalsBySlug.set(gl.slug, gl);
14113
+ }
14114
+ const mentionedCollections = /* @__PURE__ */ new Set();
14115
+ const mentionedGlobals = /* @__PURE__ */ new Set();
14116
+ const injectedViewsByCollection = /* @__PURE__ */ new Map();
14117
+ for (const item of explicitNav) {
14118
+ if (item.addToCollection) {
14119
+ const existing = injectedViewsByCollection.get(item.addToCollection) || [];
14120
+ if (item.views && item.views.length > 0) {
14121
+ injectedViewsByCollection.set(item.addToCollection, [...existing, ...item.views]);
14122
+ }
14123
+ }
14124
+ if (item.collection) mentionedCollections.add(item.collection);
14125
+ if (item.global) mentionedGlobals.add(item.global);
14126
+ }
14127
+ const groupMetadataMap = /* @__PURE__ */ new Map();
14128
+ const itemsByGroup = /* @__PURE__ */ new Map();
14129
+ const ungroupedItems = [];
14130
+ const registerItem = (groupName, item) => {
14131
+ if (!groupName) {
14132
+ ungroupedItems.push(item);
14133
+ } else {
14134
+ const list = itemsByGroup.get(groupName) || [];
14135
+ list.push(item);
14136
+ itemsByGroup.set(groupName, list);
14137
+ }
14138
+ };
14139
+ for (const item of explicitNav) {
14140
+ if (item.addToCollection && !item.slug && !item.collection && !item.global && !item.dashboard && !item.href) {
14141
+ continue;
14142
+ }
14143
+ const { name: groupName, meta: groupMeta } = resolveGroupMeta(item.group, void 0);
14144
+ if (groupName && groupMeta) {
14145
+ groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
14146
+ }
14147
+ if (item.dashboard) {
14148
+ registerItem(groupName, {
14149
+ id: "dashboard",
14150
+ type: "dashboard",
14151
+ slug: "dashboard",
14152
+ label: item.label || "Dashboard",
14153
+ icon: item.icon || "LayoutDashboard",
14154
+ group: groupName,
14155
+ order: item.order ?? 0,
14156
+ views: [],
14157
+ access: item.access
14158
+ });
14159
+ continue;
14160
+ }
14161
+ if (item.collection) {
14162
+ const col = collectionsBySlug.get(item.collection);
14163
+ const combinedViews = [
14164
+ ...col?.views || [],
14165
+ ...injectedViewsByCollection.get(item.collection) || [],
14166
+ ...item.views || []
14167
+ ];
14168
+ registerItem(groupName, {
14169
+ id: item.slug || `collection_${item.collection}`,
14170
+ type: item.slug ? "workspace" : "collection",
14171
+ slug: item.slug || item.collection,
14172
+ collection: item.collection,
14173
+ label: item.label || col?.labels?.plural || item.collection,
14174
+ icon: item.icon || col?.admin?.icon || "Folder",
14175
+ group: groupName,
14176
+ order: item.order ?? col?.admin?.order ?? 100,
14177
+ views: combinedViews,
14178
+ badge: item.badge,
14179
+ access: item.access || col?.access?.read
14180
+ });
14181
+ continue;
14182
+ }
14183
+ if (item.global) {
14184
+ const gl = globalsBySlug.get(item.global);
14185
+ registerItem(groupName, {
14186
+ id: item.slug || `global_${item.global}`,
14187
+ type: "global",
14188
+ slug: item.slug || item.global,
14189
+ global: item.global,
14190
+ label: item.label || gl?.label || item.global,
14191
+ icon: item.icon || "Settings",
14192
+ group: groupName,
14193
+ order: item.order ?? 100,
14194
+ views: [],
14195
+ badge: item.badge,
14196
+ access: item.access
14197
+ });
14198
+ continue;
14199
+ }
14200
+ if (item.href) {
14201
+ registerItem(groupName, {
14202
+ id: `link_${item.slug || item.href}`,
14203
+ type: "link",
14204
+ slug: item.slug || "link",
14205
+ label: item.label || "Link",
14206
+ href: item.href,
14207
+ icon: item.icon || "ExternalLink",
14208
+ group: groupName,
14209
+ order: item.order ?? 100,
14210
+ views: [],
14211
+ badge: item.badge,
14212
+ access: item.access
14213
+ });
14214
+ continue;
14215
+ }
14216
+ const workspaceSlug = item.slug || "workspace";
14217
+ registerItem(groupName, {
14218
+ id: `workspace_${workspaceSlug}`,
14219
+ type: "workspace",
14220
+ slug: workspaceSlug,
14221
+ label: item.label || workspaceSlug,
14222
+ icon: item.icon || "Briefcase",
14223
+ group: groupName,
14224
+ order: item.order ?? 100,
14225
+ views: item.views || [],
14226
+ badge: item.badge,
14227
+ access: item.access
14228
+ });
14229
+ }
14230
+ for (const col of collections) {
14231
+ if (mentionedCollections.has(col.slug)) continue;
14232
+ if (col.admin?.hidden) continue;
14233
+ const groupProp = col.admin?.group;
14234
+ const { name: groupName, meta: groupMeta } = resolveGroupMeta(groupProp, "Collections");
14235
+ if (groupMeta) {
14236
+ groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
14237
+ }
14238
+ const combinedViews = [
14239
+ ...col.views || [],
14240
+ ...injectedViewsByCollection.get(col.slug) || []
14241
+ ];
14242
+ registerItem(groupName, {
14243
+ id: `collection_${col.slug}`,
14244
+ type: "collection",
14245
+ slug: col.slug,
14246
+ collection: col.slug,
14247
+ label: col.labels?.plural || col.slug,
14248
+ icon: col.admin?.icon || "Folder",
14249
+ group: groupName,
14250
+ order: col?.admin?.order ?? 100,
14251
+ views: combinedViews,
14252
+ access: col.access?.read
14253
+ });
14254
+ }
14255
+ for (const gl of globals) {
14256
+ if (mentionedGlobals.has(gl.slug)) continue;
14257
+ const groupName = "Configuration";
14258
+ registerItem(groupName, {
14259
+ id: `global_${gl.slug}`,
14260
+ type: "global",
14261
+ slug: gl.slug,
14262
+ global: gl.slug,
14263
+ label: gl.label || gl.slug,
14264
+ icon: "Settings",
14265
+ group: groupName,
14266
+ order: 100,
14267
+ views: []
14268
+ });
14269
+ }
14270
+ const sortAndSpliceItems = (items) => {
14271
+ const sorted = [...items].sort((a, b) => a.order - b.order);
14272
+ const explicitMap = /* @__PURE__ */ new Map();
14273
+ for (const opt of explicitNav) {
14274
+ if (opt.slug) explicitMap.set(opt.slug, opt);
14275
+ if (opt.collection) explicitMap.set(opt.collection, opt);
14276
+ if (opt.global) explicitMap.set(opt.global, opt);
14277
+ }
14278
+ for (const opt of explicitNav) {
14279
+ const itemKey = opt.slug || opt.collection || opt.global;
14280
+ if (!itemKey) continue;
14281
+ if (opt.after) {
14282
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
14283
+ const targetIdx = sorted.findIndex((i) => i.slug === opt.after);
14284
+ if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
14285
+ const [moved] = sorted.splice(itemIdx, 1);
14286
+ const newTargetIdx = sorted.findIndex((i) => i.slug === opt.after);
14287
+ sorted.splice(newTargetIdx + 1, 0, moved);
14288
+ }
14289
+ } else if (opt.before) {
14290
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
14291
+ const targetIdx = sorted.findIndex((i) => i.slug === opt.before);
14292
+ if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
14293
+ const [moved] = sorted.splice(itemIdx, 1);
14294
+ const newTargetIdx = sorted.findIndex((i) => i.slug === opt.before);
14295
+ sorted.splice(newTargetIdx, 0, moved);
14296
+ }
14297
+ } else if (opt.position === "first") {
14298
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
14299
+ if (itemIdx > 0) {
14300
+ const [moved] = sorted.splice(itemIdx, 1);
14301
+ sorted.unshift(moved);
14302
+ }
14303
+ } else if (opt.position === "last") {
14304
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
14305
+ if (itemIdx !== -1 && itemIdx < sorted.length - 1) {
14306
+ const [moved] = sorted.splice(itemIdx, 1);
14307
+ sorted.push(moved);
14308
+ }
14309
+ }
14310
+ }
14311
+ return sorted;
14312
+ };
14313
+ const compiledGroups = [];
14314
+ for (const [groupName, rawItems] of itemsByGroup.entries()) {
14315
+ const meta = groupMetadataMap.get(groupName);
14316
+ const sortedItems = sortAndSpliceItems(rawItems);
14317
+ compiledGroups.push({
14318
+ id: `group_${groupName.toLowerCase().replace(/\s+/g, "_")}`,
14319
+ name: groupName,
14320
+ slug: meta?.slug || groupName.toLowerCase().replace(/\s+/g, "-"),
14321
+ icon: meta?.icon,
14322
+ defaultExpanded: meta?.defaultExpanded ?? true,
14323
+ order: meta?.order ?? 100,
14324
+ items: sortedItems
14325
+ });
14326
+ }
14327
+ compiledGroups.sort((a, b) => a.order - b.order);
14328
+ return {
14329
+ groups: compiledGroups,
14330
+ ungrouped: sortAndSpliceItems(ungroupedItems)
14331
+ };
14332
+ }
14333
+ function pruneNavigationForUser(tree, user) {
14334
+ const userRoles = /* @__PURE__ */ new Set();
14335
+ if (user?.role) userRoles.add(user.role);
14336
+ if (Array.isArray(user?.roles)) {
14337
+ for (const r of user.roles) userRoles.add(r);
14338
+ }
14339
+ const isAllowed = (item) => {
14340
+ if (!item.access) return true;
14341
+ if (Array.isArray(item.access)) {
14342
+ if (item.access.length === 0) return true;
14343
+ return item.access.some((role) => userRoles.has(role));
14344
+ }
14345
+ return true;
14346
+ };
14347
+ const prunedGroups = [];
14348
+ for (const group of tree.groups) {
14349
+ const visibleItems = group.items.filter(isAllowed);
14350
+ if (visibleItems.length > 0) {
14351
+ prunedGroups.push({
14352
+ ...group,
14353
+ items: visibleItems
14354
+ });
14355
+ }
14356
+ }
14357
+ return {
14358
+ groups: prunedGroups,
14359
+ ungrouped: tree.ungrouped.filter(isAllowed)
14360
+ };
14361
+ }
14362
+
13832
14363
  // src/router.ts
13833
14364
  init_access_control();
13834
14365
  var import_http_exception5 = require("hono/http-exception");
@@ -14328,6 +14859,7 @@ function registerRoutes(app, config) {
14328
14859
  const user = c.get("user");
14329
14860
  const key = c.req.param("key");
14330
14861
  const scope = c.req.query("scope");
14862
+ const roleParam = c.req.query("role");
14331
14863
  if (!db) return c.json({ message: "Database not configured" }, 500);
14332
14864
  if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
14333
14865
  const getGlobalPreference = async () => {
@@ -14337,27 +14869,61 @@ function registerRoutes(app, config) {
14337
14869
  });
14338
14870
  return globalDoc ? globalDoc.value : null;
14339
14871
  };
14340
- if (scope === "global" || !user?.collection || !user.sub) {
14872
+ const getRolePreference = async (r) => {
14873
+ const roleDoc = await db.findOne({
14874
+ collection: "__role_preferences",
14875
+ id: `${r}_${key}`
14876
+ });
14877
+ return roleDoc ? roleDoc.value : null;
14878
+ };
14879
+ const getUserPreferences = async () => {
14880
+ if (!user?.collection || !user.sub) return null;
14881
+ const doc = await db.findOne({ collection: user.collection, id: user.sub });
14882
+ if (!doc) return null;
14883
+ return typeof doc.__preferences === "object" && doc.__preferences !== null ? doc.__preferences : {};
14884
+ };
14885
+ if (scope === "global") {
14341
14886
  const globalValue2 = await getGlobalPreference();
14342
14887
  return c.json({ key, value: globalValue2 });
14343
14888
  }
14344
- const doc = await db.findOne({ collection: user.collection, id: user.sub });
14345
- if (!doc) {
14346
- const globalValue2 = await getGlobalPreference();
14347
- return c.json({ key, value: globalValue2 });
14889
+ if (scope === "role") {
14890
+ if (roleParam) {
14891
+ const val = await getRolePreference(roleParam);
14892
+ return c.json({ key, value: val, role: roleParam });
14893
+ }
14894
+ const userRoles2 = Array.isArray(user?.roles) ? user.roles : typeof user?.role === "string" ? [user.role] : [];
14895
+ for (const r of userRoles2) {
14896
+ const val = await getRolePreference(r);
14897
+ if (val !== null && val !== void 0) {
14898
+ return c.json({ key, value: val, role: r });
14899
+ }
14900
+ }
14901
+ return c.json({ key, value: null });
14348
14902
  }
14349
- const preferences = typeof doc.__preferences === "object" && doc.__preferences !== null ? doc.__preferences : {};
14350
- if (key in preferences) {
14351
- return c.json({ key, value: preferences[key] ?? null });
14903
+ if (scope === "personal") {
14904
+ const prefs2 = await getUserPreferences();
14905
+ return c.json({ key, value: prefs2 && key in prefs2 ? prefs2[key] ?? null : null });
14906
+ }
14907
+ const prefs = await getUserPreferences();
14908
+ if (prefs && key in prefs && prefs[key] !== null && prefs[key] !== void 0) {
14909
+ return c.json({ key, value: prefs[key] });
14910
+ }
14911
+ const userRoles = Array.isArray(user?.roles) ? user.roles : typeof user?.role === "string" ? [user.role] : [];
14912
+ for (const r of userRoles) {
14913
+ const val = await getRolePreference(r);
14914
+ if (val !== null && val !== void 0) {
14915
+ return c.json({ key, value: val });
14916
+ }
14352
14917
  }
14353
14918
  const globalValue = await getGlobalPreference();
14354
- return c.json({ key, value: globalValue });
14919
+ return c.json({ key, value: globalValue ?? null });
14355
14920
  });
14356
14921
  app.put("/api/preferences/:key", requireAuth(config), async (c) => {
14357
14922
  const db = config.db;
14358
14923
  const user = c.get("user");
14359
14924
  const key = c.req.param("key");
14360
- const scope = c.req.query("scope");
14925
+ const scope = c.req.query("scope") || "personal";
14926
+ const roleParam = c.req.query("role");
14361
14927
  if (!db) return c.json({ message: "Database not configured" }, 500);
14362
14928
  if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
14363
14929
  if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
@@ -14390,7 +14956,47 @@ function registerRoutes(app, config) {
14390
14956
  data: { id: key, value: body.value }
14391
14957
  });
14392
14958
  }
14393
- return c.json({ key, value: body.value });
14959
+ return c.json({ key, value: body.value, scope: "global" });
14960
+ }
14961
+ if (scope === "role") {
14962
+ const userCol = config.collections?.find((col) => col.slug === user?.collection);
14963
+ const isAdminUser = isUserAdmin(user, userCol);
14964
+ if (!isAdminUser) {
14965
+ return c.json(
14966
+ {
14967
+ error: true,
14968
+ message: "Only administrators can save role preferences."
14969
+ },
14970
+ 403
14971
+ );
14972
+ }
14973
+ if (!roleParam) {
14974
+ return c.json(
14975
+ {
14976
+ error: true,
14977
+ message: "Role query parameter is required for role-scoped preferences."
14978
+ },
14979
+ 400
14980
+ );
14981
+ }
14982
+ const roleDocId = `${roleParam}_${key}`;
14983
+ const existing = await db.findOne({
14984
+ collection: "__role_preferences",
14985
+ id: roleDocId
14986
+ });
14987
+ if (existing) {
14988
+ await db.update({
14989
+ collection: "__role_preferences",
14990
+ id: roleDocId,
14991
+ data: { value: body.value, role: roleParam, key }
14992
+ });
14993
+ } else {
14994
+ await db.create({
14995
+ collection: "__role_preferences",
14996
+ data: { id: roleDocId, role: roleParam, key, value: body.value }
14997
+ });
14998
+ }
14999
+ return c.json({ key, value: body.value, role: roleParam, scope: "role" });
14394
15000
  }
14395
15001
  const doc = await db.findOne({ collection: user.collection, id: user.sub });
14396
15002
  if (!doc) return c.json({ error: true, message: "User not found." }, 404);
@@ -14401,13 +15007,14 @@ function registerRoutes(app, config) {
14401
15007
  id: user.sub,
14402
15008
  data: { __preferences: nextPreferences }
14403
15009
  });
14404
- return c.json({ key, value: body.value });
15010
+ return c.json({ key, value: body.value, scope: "personal" });
14405
15011
  });
14406
15012
  app.delete("/api/preferences/:key", requireAuth(config), async (c) => {
14407
15013
  const db = config.db;
14408
15014
  const user = c.get("user");
14409
15015
  const key = c.req.param("key");
14410
- const scope = c.req.query("scope");
15016
+ const scope = c.req.query("scope") || "personal";
15017
+ const roleParam = c.req.query("role");
14411
15018
  if (!db) return c.json({ message: "Database not configured" }, 500);
14412
15019
  if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
14413
15020
  if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
@@ -14426,6 +15033,30 @@ function registerRoutes(app, config) {
14426
15033
  await db.delete({ collection: "__global_preferences", id: key });
14427
15034
  return c.json({ success: true });
14428
15035
  }
15036
+ if (scope === "role") {
15037
+ const userCol = config.collections?.find((col) => col.slug === user?.collection);
15038
+ const isAdminUser = isUserAdmin(user, userCol);
15039
+ if (!isAdminUser) {
15040
+ return c.json(
15041
+ {
15042
+ error: true,
15043
+ message: "Only administrators can delete role preferences."
15044
+ },
15045
+ 403
15046
+ );
15047
+ }
15048
+ if (!roleParam) {
15049
+ return c.json(
15050
+ {
15051
+ error: true,
15052
+ message: "Role query parameter is required for role-scoped preferences."
15053
+ },
15054
+ 400
15055
+ );
15056
+ }
15057
+ await db.delete({ collection: "__role_preferences", id: `${roleParam}_${key}` });
15058
+ return c.json({ success: true });
15059
+ }
14429
15060
  const doc = await db.findOne({ collection: user.collection, id: user.sub });
14430
15061
  if (!doc) return c.json({ error: true, message: "User not found." }, 404);
14431
15062
  const preferences = typeof doc.__preferences === "object" && doc.__preferences !== null ? { ...doc.__preferences } : {};
@@ -14437,6 +15068,198 @@ function registerRoutes(app, config) {
14437
15068
  });
14438
15069
  return c.json({ success: true });
14439
15070
  });
15071
+ app.get("/api/admin/navigation", optionalAuth(config), async (c) => {
15072
+ const user = c.get("user");
15073
+ const compiled = compileNavigation(config);
15074
+ const pruned = pruneNavigationForUser(compiled, user);
15075
+ return c.json(pruned);
15076
+ });
15077
+ app.on(["GET", "POST"], "/api/admin/navigation/badges", optionalAuth(config), async (c) => {
15078
+ const db = config.db;
15079
+ if (!db) return c.json({ badges: {} });
15080
+ const user = c.get("user");
15081
+ const compiled = compileNavigation(config);
15082
+ let userNavPrefs = null;
15083
+ const prefsQuery = c.req.query("prefs");
15084
+ if (prefsQuery) {
15085
+ try {
15086
+ userNavPrefs = JSON.parse(decodeURIComponent(prefsQuery));
15087
+ } catch {
15088
+ }
15089
+ }
15090
+ if (!userNavPrefs && c.req.method === "POST") {
15091
+ try {
15092
+ const body = await c.req.json();
15093
+ if (body?.preferences) userNavPrefs = body.preferences;
15094
+ } catch {
15095
+ }
15096
+ }
15097
+ if (!userNavPrefs && user?.collection && user?.sub) {
15098
+ try {
15099
+ const userDoc = await db.findOne({ collection: user.collection, id: user.sub });
15100
+ if (userDoc?.__preferences && typeof userDoc.__preferences === "object") {
15101
+ userNavPrefs = userDoc.__preferences["admin:navigation"];
15102
+ }
15103
+ } catch {
15104
+ }
15105
+ }
15106
+ if (!userNavPrefs) {
15107
+ try {
15108
+ const globalDoc = await db.findOne({
15109
+ collection: "__global_preferences",
15110
+ id: "admin:navigation"
15111
+ });
15112
+ if (globalDoc?.value) {
15113
+ userNavPrefs = globalDoc.value;
15114
+ }
15115
+ } catch {
15116
+ }
15117
+ }
15118
+ let allItems;
15119
+ if (userNavPrefs) {
15120
+ const reconciled = reconcileNavigation(compiled, userNavPrefs, {
15121
+ collections: config.collections,
15122
+ globals: config.globals
15123
+ });
15124
+ allItems = [
15125
+ ...reconciled.ungrouped || [],
15126
+ ...reconciled.pinnedItems || [],
15127
+ ...(reconciled.groups || []).flatMap((g) => g.items || [])
15128
+ ];
15129
+ } else {
15130
+ const pruned = pruneNavigationForUser(compiled, user);
15131
+ allItems = [
15132
+ ...pruned.ungrouped,
15133
+ ...pruned.groups.flatMap((g) => g.items)
15134
+ ];
15135
+ }
15136
+ const badges = {};
15137
+ const requestsByCollection = /* @__PURE__ */ new Map();
15138
+ let reqCounter = 0;
15139
+ for (const item of allItems) {
15140
+ if (item.badge) {
15141
+ const itemBadge = item.badge;
15142
+ const badgeKey = item.slug || item.collection || item.global || item.id;
15143
+ if (typeof itemBadge === "string") {
15144
+ badges[badgeKey] = { count: itemBadge, variant: "default" };
15145
+ } else {
15146
+ const badgeConfig = itemBadge;
15147
+ const variant = badgeConfig.variant ?? "default";
15148
+ if (badgeConfig.text) {
15149
+ badges[badgeKey] = { count: badgeConfig.text, variant };
15150
+ } else {
15151
+ let targetCol;
15152
+ let targetWhere;
15153
+ if (badgeConfig.aggregate) {
15154
+ targetCol = badgeConfig.aggregate.collection;
15155
+ targetWhere = badgeConfig.aggregate.where;
15156
+ } else if (badgeConfig.count) {
15157
+ targetCol = item.collection || item.views?.[0]?.collection;
15158
+ const primaryView = item.views?.[0];
15159
+ if (primaryView?.filter) {
15160
+ targetWhere = primaryView.filter;
15161
+ }
15162
+ }
15163
+ if (targetCol) {
15164
+ const reqId = `agg_${reqCounter++}`;
15165
+ const list = requestsByCollection.get(targetCol) || [];
15166
+ list.push({
15167
+ id: reqId,
15168
+ where: targetWhere,
15169
+ onResult: (count2) => {
15170
+ badges[badgeKey] = { count: count2, variant };
15171
+ }
15172
+ });
15173
+ requestsByCollection.set(targetCol, list);
15174
+ }
15175
+ }
15176
+ }
15177
+ }
15178
+ if (item.views && item.views.length > 0) {
15179
+ for (const view of item.views) {
15180
+ if (!view.badge) continue;
15181
+ const viewBadge = view.badge;
15182
+ const viewKey = `${item.slug}:${view.slug}`;
15183
+ const altKey = `${item.slug}_${view.slug}`;
15184
+ const plainKey = view.slug;
15185
+ const setBadgeResult = (res) => {
15186
+ badges[viewKey] = res;
15187
+ badges[altKey] = res;
15188
+ if (!badges[plainKey]) {
15189
+ badges[plainKey] = res;
15190
+ }
15191
+ if (view.collection) {
15192
+ badges[`${view.collection}:${view.slug}`] = res;
15193
+ badges[`${view.collection}_${view.slug}`] = res;
15194
+ }
15195
+ };
15196
+ if (typeof viewBadge === "string") {
15197
+ setBadgeResult({ count: viewBadge, variant: "default" });
15198
+ continue;
15199
+ }
15200
+ const badgeConfig = viewBadge;
15201
+ const variant = badgeConfig.variant ?? "default";
15202
+ if (badgeConfig.text) {
15203
+ setBadgeResult({ count: badgeConfig.text, variant });
15204
+ continue;
15205
+ }
15206
+ const targetCol = badgeConfig.aggregate?.collection || view.collection || item.collection || (item.type === "collection" ? item.slug : void 0);
15207
+ const targetWhere = badgeConfig.aggregate?.where || (typeof view.filter === "object" ? view.filter : void 0);
15208
+ if (!targetCol) continue;
15209
+ const reqId = `agg_${reqCounter++}`;
15210
+ const list = requestsByCollection.get(targetCol) || [];
15211
+ list.push({
15212
+ id: reqId,
15213
+ where: targetWhere,
15214
+ onResult: (count2) => {
15215
+ setBadgeResult({ count: count2, variant });
15216
+ }
15217
+ });
15218
+ requestsByCollection.set(targetCol, list);
15219
+ }
15220
+ }
15221
+ }
15222
+ const collectionTasks = Array.from(requestsByCollection.entries()).map(async ([col, reqs]) => {
15223
+ if (typeof db.aggregate === "function") {
15224
+ try {
15225
+ const aggregates = {};
15226
+ for (const req of reqs) {
15227
+ aggregates[req.id] = { count: "*", where: req.where };
15228
+ }
15229
+ const agg = await db.aggregate({
15230
+ collection: col,
15231
+ aggregates
15232
+ });
15233
+ for (const req of reqs) {
15234
+ const val = agg?.[req.id];
15235
+ req.onResult(typeof val === "number" ? val : 0);
15236
+ }
15237
+ } catch {
15238
+ for (const req of reqs) {
15239
+ req.onResult(0);
15240
+ }
15241
+ }
15242
+ } else {
15243
+ await Promise.allSettled(
15244
+ reqs.map(async (req) => {
15245
+ try {
15246
+ const res = await db.find({
15247
+ collection: col,
15248
+ where: req.where,
15249
+ limit: 1
15250
+ });
15251
+ const total = res?.totalDocs ?? res?.total ?? 0;
15252
+ req.onResult(total);
15253
+ } catch {
15254
+ req.onResult(0);
15255
+ }
15256
+ })
15257
+ );
15258
+ }
15259
+ });
15260
+ await Promise.allSettled(collectionTasks);
15261
+ return c.json({ badges });
15262
+ });
14440
15263
  app.get("/api/media/folders", async (c) => {
14441
15264
  const folderController = new MediaFolderController("media");
14442
15265
  return folderController.list(c);
@@ -14916,7 +15739,33 @@ function normalizeConfig(config) {
14916
15739
  }
14917
15740
  ];
14918
15741
  }
15742
+ if (!existingFieldNames.has("__preferences")) {
15743
+ fields = [
15744
+ ...fields,
15745
+ {
15746
+ name: "__preferences",
15747
+ type: "json",
15748
+ label: "Preferences",
15749
+ promoted: true,
15750
+ admin: {
15751
+ hidden: true,
15752
+ readOnly: true
15753
+ }
15754
+ }
15755
+ ];
15756
+ }
14919
15757
  fields = fields.map((field) => {
15758
+ if (field.name === "__preferences") {
15759
+ return {
15760
+ ...field,
15761
+ promoted: true,
15762
+ admin: {
15763
+ ...field.admin || {},
15764
+ hidden: true,
15765
+ readOnly: true
15766
+ }
15767
+ };
15768
+ }
14920
15769
  if (field.name === "email") {
14921
15770
  return {
14922
15771
  ...field,