nextjs-cms 0.9.37 → 0.9.39

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.
Files changed (84) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +279 -279
  3. package/dist/api/actions/files.d.ts.map +1 -1
  4. package/dist/api/actions/pages.d.ts +28 -7
  5. package/dist/api/actions/pages.d.ts.map +1 -1
  6. package/dist/api/actions/pages.js +112 -27
  7. package/dist/api/trpc/root.d.ts +65 -10
  8. package/dist/api/trpc/root.d.ts.map +1 -1
  9. package/dist/api/trpc/routers/config.d.ts.map +1 -1
  10. package/dist/api/trpc/routers/hasItemsSection.d.ts +23 -2
  11. package/dist/api/trpc/routers/hasItemsSection.d.ts.map +1 -1
  12. package/dist/api/trpc/routers/hasItemsSection.js +2 -1
  13. package/dist/api/trpc/routers/logs.d.ts +37 -4
  14. package/dist/api/trpc/routers/logs.d.ts.map +1 -1
  15. package/dist/api/trpc/routers/logs.js +92 -43
  16. package/dist/api/trpc/routers/navigation.d.ts +4 -3
  17. package/dist/api/trpc/routers/navigation.d.ts.map +1 -1
  18. package/dist/api/trpc/routers/simpleSection.d.ts +1 -1
  19. package/dist/api/trpc/routers/simpleSection.d.ts.map +1 -1
  20. package/dist/api/trpc/server.d.ts +195 -30
  21. package/dist/api/trpc/server.d.ts.map +1 -1
  22. package/dist/cli/lib/db-config.js +10 -10
  23. package/dist/cli/lib/update-sections.js +24 -24
  24. package/dist/core/config/config-loader.d.ts +2 -2
  25. package/dist/core/db/table-checker/MysqlTable.js +10 -10
  26. package/dist/core/factories/FieldFactory.d.ts +1 -6
  27. package/dist/core/factories/FieldFactory.d.ts.map +1 -1
  28. package/dist/core/factories/FieldFactory.js +3 -68
  29. package/dist/core/factories/section-factory-with-esbuild.js +9 -9
  30. package/dist/core/factories/section-factory-with-jiti.js +9 -9
  31. package/dist/core/fields/checkbox.d.ts +3 -3
  32. package/dist/core/fields/color.d.ts +3 -3
  33. package/dist/core/fields/date-range.d.ts +2 -2
  34. package/dist/core/fields/date.d.ts +3 -3
  35. package/dist/core/fields/document.d.ts +3 -3
  36. package/dist/core/fields/field.d.ts +25 -12
  37. package/dist/core/fields/field.d.ts.map +1 -1
  38. package/dist/core/fields/field.js +51 -23
  39. package/dist/core/fields/index.d.ts +1 -0
  40. package/dist/core/fields/index.d.ts.map +1 -1
  41. package/dist/core/fields/index.js +2 -0
  42. package/dist/core/fields/map.d.ts +3 -3
  43. package/dist/core/fields/number.d.ts +4 -4
  44. package/dist/core/fields/password.d.ts +3 -3
  45. package/dist/core/fields/photo.d.ts +9 -9
  46. package/dist/core/fields/richText.d.ts +12 -12
  47. package/dist/core/fields/select.d.ts +7 -7
  48. package/dist/core/fields/selectMultiple.d.ts +4 -4
  49. package/dist/core/fields/slug.d.ts +9 -9
  50. package/dist/core/fields/tags.d.ts +3 -3
  51. package/dist/core/fields/text.d.ts +2 -2
  52. package/dist/core/fields/textArea.d.ts +2 -2
  53. package/dist/core/fields/video.d.ts +3 -3
  54. package/dist/core/helpers/conditional-rules.d.ts +63 -0
  55. package/dist/core/helpers/conditional-rules.d.ts.map +1 -0
  56. package/dist/core/helpers/conditional-rules.js +113 -0
  57. package/dist/core/helpers/index.d.ts +1 -0
  58. package/dist/core/helpers/index.d.ts.map +1 -1
  59. package/dist/core/helpers/index.js +4 -0
  60. package/dist/core/sections/category.d.ts +50 -50
  61. package/dist/core/sections/hasItems.d.ts +74 -74
  62. package/dist/core/sections/section.d.ts +23 -23
  63. package/dist/core/sections/simple.d.ts +6 -6
  64. package/dist/core/submit/submit.d.ts +4 -1
  65. package/dist/core/submit/submit.d.ts.map +1 -1
  66. package/dist/core/submit/submit.js +9 -19
  67. package/dist/core/types/index.d.ts +30 -8
  68. package/dist/core/types/index.d.ts.map +1 -1
  69. package/dist/index.d.ts +1 -0
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +4 -0
  72. package/dist/logging/event-labels.d.ts +11 -0
  73. package/dist/logging/event-labels.d.ts.map +1 -0
  74. package/dist/logging/event-labels.js +14 -0
  75. package/dist/logging/log.d.ts +1 -1
  76. package/dist/logging/log.d.ts.map +1 -1
  77. package/dist/translations/base/en.d.ts +33 -0
  78. package/dist/translations/base/en.d.ts.map +1 -1
  79. package/dist/translations/base/en.js +33 -0
  80. package/dist/translations/client.d.ts +400 -4
  81. package/dist/translations/client.d.ts.map +1 -1
  82. package/dist/translations/server.d.ts +400 -4
  83. package/dist/translations/server.d.ts.map +1 -1
  84. package/package.json +7 -3
@@ -1 +1 @@
1
- {"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../../src/api/actions/pages.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AASpB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAKR,cAAc,EAGjB,MAAM,8BAA8B,CAAA;AACrC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AA0E7D,eAAO,MAAM,uBAAuB,GAAU,SAAS,OAAO,EAAE,aAAa,MAAM,EAAE,SAAS,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBA8DzE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,GAAG,CAAC;YAAC,GAAG,CAAC,EAAE,OAAO,CAAA;SAAE;uBAC3C;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,GAAG,CAAC;YAAC,GAAG,CAAC,EAAE,OAAO,CAAA;SAAE;8BACpC,MAAM,EAAE;iBACrB;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,GAAG,CAAC;YAAC,GAAG,CAAC,EAAE,OAAO,CAAA;SAAE,EAAE;+BAC/B,OAAO;8BACR,OAAO;;;EA6HxC,CAAA;AAED,eAAO,MAAM,iBAAiB,GAC1B,SAAS,OAAO,EAChB,aAAa,MAAM,EACnB,eAAe,MAAM,GAAG,MAAM,EAC9B,YAAY,OAAO,EACnB,kBAAkB,eAAe;;;;EAwRpC,CAAA;AAED,eAAO,MAAM,uBAAuB,GAChC,SAAS,OAAO,EAChB,aAAa,MAAM,EACnB,eAAe,MAAM,GAAG,MAAM,EAC9B,QAAQ,MAAM;;;;EAmMjB,CAAA;AAED,eAAO,MAAM,cAAc,GACvB,SAAS,OAAO,EAChB,aAAa,MAAM,EACnB,eAAe,MAAM,GAAG,MAAM,EAC9B,SAAS,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA2EM,MAAM;eACZ,MAAM;cACP,GAAG;iBACA,MAAM;;;uBAwDA;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,GAAG,CAAC,EAAE,OAAO,CAAA;SAAE;uBAC9C;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,GAAG,CAAC,EAAE,OAAO,CAAA;SAAE;8BACvC,MAAM,EAAE;iBACrB;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,GAAG,CAAC,EAAE,OAAO,CAAA;SAAE,EAAE;+BAClC,OAAO;8BACR,OAAO;;;;;;;;;;;EA4HxC,CAAA;AAED,eAAO,MAAM,aAAa,GAAU,SAAS,OAAO,EAAE,aAAa,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0FxE,CAAA;AAED,eAAO,MAAM,0BAA0B,GAAU,sCAK9C;IACC,OAAO,EAAE,OAAO,CAAA;IAChB,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;CAChB;;;;;;;;;;;;;;;;;;;;EA2DA,CAAA;AAED,eAAO,MAAM,kBAAkB,GAAU,SAAS,OAAO,EAAE,aAAa,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA6D/C,MAAM,GAAG,MAAM,GAAG,SAAS;;;;;;;EAgBzD,CAAA;AAED,eAAO,MAAM,aAAa,GAAU,SAAS,OAAO,EAAE,aAAa,MAAM,EAAE,OAAM,MAAU,EAAE,IAAI,MAAM;;;;;;;;;;;;;;YAuInF,MAAM,GAAG,MAAM;sBACL,MAAM;oBACR,MAAM,GAAG,IAAI;mBACd,MAAM;mBACN,MAAM;oBACL,MAAM;;;;EAKjC,CAAA;AAED,eAAO,MAAM,UAAU,GAAU,SAAS,OAAO;;;;;;;cA8ClB,MAAM;eAAS,MAAM;cAAQ,MAAM;cAAQ,MAAM;;cAElE,OAAO;eACN,MAAM;cACP,MAAM;kBACF;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,EAAE;;;;;;;;;;;;;;;;;EA0DhE,CAAA"}
1
+ {"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../../src/api/actions/pages.ts"],"names":[],"mappings":"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"}
@@ -1012,7 +1012,7 @@ export const getCategorySection = async (session, sectionName) => {
1012
1012
  // publisher: privileges.publisher,
1013
1013
  };
1014
1014
  };
1015
- export const getBrowsePage = async (session, sectionName, page = 1, q) => {
1015
+ export const getBrowsePage = async (session, sectionName, page = 1, q, locale) => {
1016
1016
  // Let's fetch the section items and admin privileges for the section
1017
1017
  const _s = (await SectionFactory.getSectionForAdmin({
1018
1018
  name: sectionName,
@@ -1031,40 +1031,69 @@ export const getBrowsePage = async (session, sectionName, page = 1, q) => {
1031
1031
  const limit = 12;
1032
1032
  const offset = (page - 1) * limit;
1033
1033
  const tableName = section.db.table;
1034
+ const identifierName = section.db.identifier.name;
1035
+ const localesTable = section.localesTableName;
1034
1036
  const orderByFieldName = section.db.orderByField?.name || 'created_at';
1035
1037
  const sqlChunks = [];
1036
1038
  const totalRowsSqlChunks = [];
1037
1039
  const sectionSearch = section.search?.searchFields ? section.search?.searchFields.length > 0 : false;
1038
- // Check if we need to JOIN _locales for search
1039
1040
  const cmsConfig = await getCMSConfig();
1040
1041
  const hasLocalization = !!cmsConfig.localization?.enabled;
1041
- const hasLocalizedSearchFields = hasLocalization && q && q.trim().length > 0 && section.search?.searchFields?.some((f) => f.localized);
1042
- const localesTable = hasLocalizedSearchFields ? section.localesTableName : null;
1043
- if (localesTable) {
1044
- // Use DISTINCT to avoid duplicate rows from the LEFT JOIN
1045
- sqlChunks.push(sql `select distinct ${sql.raw(`\`${tableName}\``)}.* from ${sql.raw(`\`${tableName}\``)} left join ${sql.raw(`\`${localesTable}\``)} on ${sql.raw(`\`${localesTable}\``)}.parent_id = ${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${section.db.identifier.name}\``)}`);
1046
- totalRowsSqlChunks.push(sql `select count(distinct ${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${section.db.identifier.name}\``)} ) as total from ${sql.raw(`\`${tableName}\``)} left join ${sql.raw(`\`${localesTable}\``)} on ${sql.raw(`\`${localesTable}\``)}.parent_id = ${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${section.db.identifier.name}\``)}`);
1042
+ // Resolve the requested browse locale. A non-default, resolvable locale on a section
1043
+ // with localized content switches the browse view to that locale: localized fields are
1044
+ // rendered from the locale row (base value as fallback) and search is scoped to that same
1045
+ // locale. The default locale (or none) keeps the plain base-table behavior.
1046
+ const localeResult = resolveLocale({ localization: cmsConfig.localization, locale });
1047
+ const browseLocale = localeResult.localizationEnabled &&
1048
+ localeResult.resolvedLocale &&
1049
+ localeResult.isDefault === false &&
1050
+ section.hasLocalizedContent
1051
+ ? localeResult.resolvedLocale.code
1052
+ : null;
1053
+ // Display fields whose value should come from the locale row when browsing a non-default locale.
1054
+ const localizedDisplayFieldNames = new Set();
1055
+ if (browseLocale) {
1056
+ if (section.headingField?.localized)
1057
+ localizedDisplayFieldNames.add(section.headingField.name);
1058
+ if (section.coverPhotoField?.localized)
1059
+ localizedDisplayFieldNames.add(section.coverPhotoField.name);
1060
+ if (section.browse?.fields) {
1061
+ for (const f of section.browse.fields) {
1062
+ const field = section.fields.find((sf) => sf.name === f.name);
1063
+ if (field?.localized)
1064
+ localizedDisplayFieldNames.add(f.name);
1065
+ }
1066
+ }
1067
+ }
1068
+ const trimmedQuery = q && q.trim().length > 0 ? q.trim() : null;
1069
+ if (browseLocale) {
1070
+ // 1:1 LEFT JOIN via the (parent_id, locale) unique key — at most one locale row per
1071
+ // item, so no DISTINCT is needed. Items without a translation still appear (locale
1072
+ // columns NULL → base fallback). Localized display columns are aliased `__loc_<name>`.
1073
+ const localeJoin = sql `from ${sql.raw(`\`${tableName}\``)} left join ${sql.raw(`\`${localesTable}\``)} on ${sql.raw(`\`${localesTable}\``)}.parent_id = ${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${identifierName}\``)} and ${sql.raw(`\`${localesTable}\``)}.locale = ${browseLocale}`;
1074
+ const selectParts = [sql `${sql.raw(`\`${tableName}\``)}.*`];
1075
+ for (const fieldName of localizedDisplayFieldNames) {
1076
+ selectParts.push(sql `${sql.raw(`\`${localesTable}\``)}.${sql.raw(`\`${fieldName}\``)} AS ${sql.raw(`\`__loc_${fieldName}\``)}`);
1077
+ }
1078
+ sqlChunks.push(sql `select`, sql.join(selectParts, sql `, `), localeJoin);
1079
+ totalRowsSqlChunks.push(sql `select count(*) as total`, localeJoin);
1047
1080
  }
1048
1081
  else {
1049
1082
  sqlChunks.push(sql `select * from ${sql.raw(`\`${tableName}\``)}`);
1050
1083
  totalRowsSqlChunks.push(sql `select count(*) as total from ${sql.raw(`\`${tableName}\``)}`);
1051
1084
  }
1052
- if (q && q.trim().length > 0) {
1053
- if (section.search?.searchFields.length) {
1054
- const whereParts = [];
1055
- for (const field of section.search.searchFields) {
1056
- // Search in main table
1057
- whereParts.push(sql `${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${field.name}\``)} LIKE CONCAT('%',${sql `${q.trim()}`},'%')`);
1058
- // Also search in _locales table for localized fields
1059
- if (localesTable && field.localized) {
1060
- whereParts.push(sql `${sql.raw(`\`${localesTable}\``)}.${sql.raw(`\`${field.name}\``)} LIKE CONCAT('%',${sql `${q.trim()}`},'%')`);
1061
- }
1085
+ if (trimmedQuery && section.search?.searchFields.length) {
1086
+ const whereParts = [];
1087
+ for (const field of section.search.searchFields) {
1088
+ // Base-table column (this is also the default-locale content)
1089
+ whereParts.push(sql `${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${field.name}\``)} LIKE CONCAT('%',${trimmedQuery},'%')`);
1090
+ // Current locale's column — scoped to the browsed locale only, not all locales
1091
+ if (browseLocale && field.localized) {
1092
+ whereParts.push(sql `${sql.raw(`\`${localesTable}\``)}.${sql.raw(`\`${field.name}\``)} LIKE CONCAT('%',${trimmedQuery},'%')`);
1062
1093
  }
1063
- sqlChunks.push(sql `where`);
1064
- sqlChunks.push(sql.join(whereParts, sql ` or `));
1065
- totalRowsSqlChunks.push(sql `where`);
1066
- totalRowsSqlChunks.push(sql.join(whereParts, sql ` or `));
1067
1094
  }
1095
+ sqlChunks.push(sql `where`, sql.join(whereParts, sql ` or `));
1096
+ totalRowsSqlChunks.push(sql `where`, sql.join(whereParts, sql ` or `));
1068
1097
  }
1069
1098
  sqlChunks.push(sql `ORDER BY ${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${orderByFieldName}\``)} DESC LIMIT ${limit} OFFSET ${offset}`);
1070
1099
  const finalSql = sql.join(sqlChunks, sql.raw(' '));
@@ -1074,9 +1103,61 @@ export const getBrowsePage = async (session, sectionName, page = 1, q) => {
1074
1103
  const totalCountResult = await db.execute(totalRowsSql);
1075
1104
  const totalCountRows = totalCountResult[0];
1076
1105
  const rows = sectionItems[0];
1106
+ // Existing translations per item, keyed by parent id.
1107
+ // Only the current page's rows are looked up, and the query is covered by the
1108
+ // `(parent_id, locale)` unique index on the _locales table, so it's an index-only
1109
+ // scan of at most `limit` groups — negligible cost. Skipped entirely for
1110
+ // non-localized sections.
1111
+ const localesByItemId = new Map();
1112
+ if (hasLocalization && section.hasLocalizedContent && rows.length > 0) {
1113
+ const itemIds = rows
1114
+ .map((row) => row[identifierName])
1115
+ .filter((id) => id !== null && id !== undefined);
1116
+ if (itemIds.length > 0) {
1117
+ // Configured order, so badges render consistently regardless of GROUP_CONCAT order.
1118
+ const localeOrder = cmsConfig.localization?.locales?.map((l) => l.code) ?? [];
1119
+ const idList = sql.join(itemIds.map((id) => sql `${id}`), sql `, `);
1120
+ const localesSql = sql `SELECT parent_id, GROUP_CONCAT(locale) AS locales FROM ${sql.raw(`\`${section.localesTableName}\``)} WHERE parent_id IN (${idList}) GROUP BY parent_id`;
1121
+ try {
1122
+ const localesResult = await db.execute(localesSql);
1123
+ const localeRows = localesResult[0];
1124
+ for (const localeRow of localeRows) {
1125
+ const existing = String(localeRow.locales ?? '')
1126
+ .split(',')
1127
+ .filter(Boolean);
1128
+ const ordered = localeOrder.filter((code) => existing.includes(code));
1129
+ localesByItemId.set(String(localeRow.parent_id), ordered);
1130
+ }
1131
+ }
1132
+ catch (e) {
1133
+ console.error('Failed to load existing locales for browse page items:', e);
1134
+ }
1135
+ }
1136
+ }
1077
1137
  // Resolve localized section title for the browse page header
1078
1138
  const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
1079
1139
  const resolvedTitle = resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage);
1140
+ // Returns a field's display value, preferring the localized (`__loc_<name>`) column when
1141
+ // browsing a non-default locale and a translation exists, falling back to the base value.
1142
+ const displayValue = (row, fieldName) => {
1143
+ if (localizedDisplayFieldNames.has(fieldName)) {
1144
+ const localized = row[`__loc_${fieldName}`];
1145
+ if (localized !== null && localized !== undefined)
1146
+ return localized;
1147
+ }
1148
+ return row[fieldName];
1149
+ };
1150
+ // Locale switcher metadata for the browse page. `currentLocale` reflects the resolved
1151
+ // browse locale (defaulting to the base locale when none/invalid was requested).
1152
+ const browseLocalization = localeResult.localizationEnabled
1153
+ ? {
1154
+ locales: localeResult.availableLocales,
1155
+ defaultLocale: localeResult.defaultLocale,
1156
+ currentLocale: localeResult.resolvedLocale ?? localeResult.defaultLocale,
1157
+ localeSwitcherEnabled: localeResult.availableLocales.length > 1 &&
1158
+ (section.hasLocalizedContent === true || process.env.NODE_ENV === 'development'),
1159
+ }
1160
+ : null;
1080
1161
  return {
1081
1162
  section: {
1082
1163
  // tableName: tableName,
@@ -1087,6 +1168,7 @@ export const getBrowsePage = async (session, sectionName, page = 1, q) => {
1087
1168
  name: section.name,
1088
1169
  title: resolvedTitle,
1089
1170
  },
1171
+ localization: browseLocalization,
1090
1172
  items: rows.map((row) => {
1091
1173
  // Custom browse page implementation: if browse.fields is configured,
1092
1174
  // return the specified fields plus default fields (id, headingTitle, coverPhoto, createdAt, createdBy, permission).
@@ -1096,26 +1178,28 @@ export const getBrowsePage = async (session, sectionName, page = 1, q) => {
1096
1178
  const item = {
1097
1179
  // Always include default fields
1098
1180
  id: row[section.db.identifier.name],
1099
- headingTitle: row[section.headingField.name],
1100
- coverPhoto: section.coverPhotoField ? row[section.coverPhotoField?.name] : null,
1181
+ headingTitle: displayValue(row, section.headingField.name),
1182
+ coverPhoto: section.coverPhotoField ? displayValue(row, section.coverPhotoField.name) : null,
1101
1183
  createdAt: row['created_at'],
1102
1184
  createdBy: row['created_by'],
1103
1185
  permission: row.permission,
1186
+ locales: localesByItemId.get(String(row[section.db.identifier.name])) ?? [],
1104
1187
  };
1105
1188
  // Add configured browse fields
1106
1189
  for (const field of section.browse.fields) {
1107
- item[field.name] = row[field.name] ?? null;
1190
+ item[field.name] = displayValue(row, field.name) ?? null;
1108
1191
  }
1109
1192
  return item;
1110
1193
  }
1111
1194
  // Default browse page fields (used by the standard CMS browse page)
1112
1195
  return {
1113
1196
  id: row[section.db.identifier.name],
1114
- headingTitle: row[section.headingField.name],
1115
- coverPhoto: section.coverPhotoField ? row[section.coverPhotoField?.name] : null,
1197
+ headingTitle: displayValue(row, section.headingField.name),
1198
+ coverPhoto: section.coverPhotoField ? displayValue(row, section.coverPhotoField.name) : null,
1116
1199
  createdAt: row['created_at'],
1117
1200
  createdBy: row['created_by'],
1118
1201
  permission: row.permission,
1202
+ locales: localesByItemId.get(String(row[section.db.identifier.name])) ?? [],
1119
1203
  };
1120
1204
  }),
1121
1205
  totalCount: totalCountRows[0].total,
@@ -1211,5 +1295,6 @@ export const getSidebar = async (session) => {
1211
1295
  cat_sections: categorySectionsItems,
1212
1296
  has_items_sections: hasItemsSectionItems,
1213
1297
  simple_sections: simpleSectionItems,
1298
+ can_access_logs: privilegeSet.has('log'),
1214
1299
  };
1215
1300
  };