nextjs-cms 0.9.21 → 0.9.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -13
- package/dist/api/actions/files.d.ts +30 -0
- package/dist/api/actions/files.d.ts.map +1 -0
- package/dist/api/actions/files.js +234 -0
- package/dist/api/actions/index.d.ts +4 -0
- package/dist/api/actions/index.d.ts.map +1 -0
- package/dist/api/actions/index.js +3 -0
- package/dist/api/actions/pages.d.ts +297 -0
- package/dist/api/actions/pages.d.ts.map +1 -0
- package/dist/api/actions/pages.js +1215 -0
- package/dist/api/actions/privileges.d.ts +25 -0
- package/dist/api/actions/privileges.d.ts.map +1 -0
- package/dist/api/actions/privileges.js +98 -0
- package/dist/api/client/index.d.ts +4 -0
- package/dist/api/client/index.d.ts.map +1 -0
- package/dist/api/client/index.js +3 -0
- package/dist/api/client.d.ts +30 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +82 -0
- package/dist/api/index.d.ts +1 -938
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +0 -13
- package/dist/api/lib/serverActions.d.ts +3 -3
- package/dist/api/plugin/index.d.ts +7 -0
- package/dist/api/plugin/index.d.ts.map +1 -0
- package/dist/api/plugin/index.js +5 -0
- package/dist/api/root.d.ts +18 -1844
- package/dist/api/root.d.ts.map +1 -1
- package/dist/api/root.js +18 -83
- package/dist/api/routers/navigation.d.ts +3 -3
- package/dist/api/server/index.d.ts +8 -0
- package/dist/api/server/index.d.ts.map +1 -0
- package/dist/api/server/index.js +3 -0
- package/dist/api/server.d.ts +2748 -0
- package/dist/api/server.d.ts.map +1 -0
- package/dist/api/server.js +100 -0
- package/dist/api/trpc/client.d.ts +19 -3
- package/dist/api/trpc/client.d.ts.map +1 -1
- package/dist/api/trpc/client.js +55 -1
- package/dist/api/trpc/query-client.d.ts +3 -1
- package/dist/api/trpc/query-client.d.ts.map +1 -1
- package/dist/api/trpc/query-client.js +25 -20
- package/dist/api/trpc/root.d.ts +906 -0
- package/dist/api/trpc/root.d.ts.map +1 -0
- package/dist/api/trpc/root.js +47 -0
- package/dist/api/trpc/routers/accountSettings.d.ts +66 -0
- package/dist/api/trpc/routers/accountSettings.d.ts.map +1 -0
- package/dist/api/trpc/routers/accountSettings.js +200 -0
- package/dist/api/trpc/routers/admins.d.ts +112 -0
- package/dist/api/trpc/routers/admins.d.ts.map +1 -0
- package/dist/api/trpc/routers/admins.js +331 -0
- package/dist/api/trpc/routers/auth.d.ts +54 -0
- package/dist/api/trpc/routers/auth.d.ts.map +1 -0
- package/dist/api/trpc/routers/auth.js +50 -0
- package/dist/api/trpc/routers/categorySection.d.ts +105 -0
- package/dist/api/trpc/routers/categorySection.d.ts.map +1 -0
- package/dist/api/trpc/routers/categorySection.js +49 -0
- package/dist/api/trpc/routers/config.d.ts +48 -0
- package/dist/api/trpc/routers/config.d.ts.map +1 -0
- package/dist/api/trpc/routers/config.js +18 -0
- package/dist/api/trpc/routers/cpanel.d.ts +82 -0
- package/dist/api/trpc/routers/cpanel.d.ts.map +1 -0
- package/dist/api/trpc/routers/cpanel.js +216 -0
- package/dist/api/trpc/routers/fields.d.ts +35 -0
- package/dist/api/trpc/routers/fields.d.ts.map +1 -0
- package/dist/api/trpc/routers/fields.js +81 -0
- package/dist/api/trpc/routers/files.d.ts +34 -0
- package/dist/api/trpc/routers/files.d.ts.map +1 -0
- package/dist/api/trpc/routers/files.js +14 -0
- package/dist/api/trpc/routers/gallery.d.ts +35 -0
- package/dist/api/trpc/routers/gallery.d.ts.map +1 -0
- package/dist/api/trpc/routers/gallery.js +92 -0
- package/dist/api/trpc/routers/hasItemsSection.d.ts +194 -0
- package/dist/api/trpc/routers/hasItemsSection.d.ts.map +1 -0
- package/dist/api/trpc/routers/hasItemsSection.js +86 -0
- package/dist/api/trpc/routers/logs.d.ts +59 -0
- package/dist/api/trpc/routers/logs.d.ts.map +1 -0
- package/dist/api/trpc/routers/logs.js +79 -0
- package/dist/api/trpc/routers/navigation.d.ts +65 -0
- package/dist/api/trpc/routers/navigation.d.ts.map +1 -0
- package/dist/api/trpc/routers/navigation.js +11 -0
- package/dist/api/trpc/routers/simpleSection.d.ts +93 -0
- package/dist/api/trpc/routers/simpleSection.d.ts.map +1 -0
- package/dist/api/trpc/routers/simpleSection.js +54 -0
- package/dist/api/trpc/server.d.ts +2789 -5
- package/dist/api/trpc/server.d.ts.map +1 -1
- package/dist/api/trpc/server.js +91 -52
- package/dist/api/trpc/trpc.d.ts +111 -0
- package/dist/api/trpc/trpc.d.ts.map +1 -0
- package/dist/api/trpc/trpc.js +99 -0
- package/dist/api/trpc/utils/async-caller-proxy.d.ts +2 -0
- package/dist/api/trpc/utils/async-caller-proxy.d.ts.map +1 -0
- package/dist/api/trpc/utils/async-caller-proxy.js +38 -0
- package/dist/api/trpc/utils/refresh-token-link.d.ts +6 -0
- package/dist/api/trpc/utils/refresh-token-link.d.ts.map +1 -0
- package/dist/api/trpc/utils/refresh-token-link.js +81 -0
- package/dist/api/trpc/utils/router-types.d.ts +7 -0
- package/dist/api/trpc/utils/router-types.d.ts.map +1 -0
- package/dist/api/trpc/utils/router-types.js +0 -0
- package/dist/api/use-axios-private.d.ts +6 -0
- package/dist/api/use-axios-private.d.ts.map +1 -0
- package/dist/api/use-axios-private.js +57 -0
- package/dist/api/utils/async-caller-proxy.d.ts +2 -0
- package/dist/api/utils/async-caller-proxy.d.ts.map +1 -0
- package/dist/api/utils/async-caller-proxy.js +36 -0
- package/dist/api/utils/lazy-caller-proxy.d.ts +2 -0
- package/dist/api/utils/lazy-caller-proxy.d.ts.map +1 -0
- package/dist/api/utils/lazy-caller-proxy.js +36 -0
- package/dist/api/utils/router-types.d.ts +7 -0
- package/dist/api/utils/router-types.d.ts.map +1 -0
- package/dist/api/utils/router-types.js +0 -0
- package/dist/auth/hooks/index.d.ts +1 -2
- package/dist/auth/hooks/index.d.ts.map +1 -1
- package/dist/auth/hooks/index.js +1 -2
- package/dist/auth/react.d.ts +1 -2
- package/dist/auth/react.d.ts.map +1 -1
- package/dist/auth/react.js +1 -2
- package/dist/auth/trpc.d.ts +1 -1
- package/dist/auth/trpc.d.ts.map +1 -1
- package/dist/auth/trpc.js +0 -1
- package/dist/cli/lib/fix-master-admin.d.ts.map +1 -1
- package/dist/cli/lib/fix-master-admin.js +12 -25
- package/dist/cli/lib/update-sections.d.ts.map +1 -1
- package/dist/cli/lib/update-sections.js +90 -46
- package/dist/core/config/config-loader.d.ts +23 -7
- package/dist/core/config/config-loader.d.ts.map +1 -1
- package/dist/core/config/config-loader.js +26 -9
- package/dist/core/db/table-checker/MysqlTable.d.ts.map +1 -1
- package/dist/core/db/table-checker/MysqlTable.js +3 -1
- package/dist/core/fields/date-range.d.ts +4 -4
- package/dist/core/fields/select.d.ts +1 -1
- package/dist/core/sections/category.d.ts +8 -8
- package/dist/core/sections/hasItems.d.ts +8 -8
- package/dist/core/sections/section.d.ts +5 -5
- package/dist/core/sections/simple.d.ts +4 -4
- package/dist/core/types/index.d.ts +17 -0
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/plugins/client.d.ts +19 -0
- package/dist/plugins/client.d.ts.map +1 -0
- package/dist/plugins/client.js +24 -0
- package/dist/plugins/define.d.ts +4 -0
- package/dist/plugins/define.d.ts.map +1 -0
- package/dist/plugins/define.js +3 -0
- package/dist/plugins/derive.d.ts +32 -0
- package/dist/plugins/derive.d.ts.map +1 -0
- package/dist/plugins/derive.js +77 -0
- package/dist/plugins/loader.d.ts +51 -7
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +111 -51
- package/dist/plugins/manifest.d.ts +28 -0
- package/dist/plugins/manifest.d.ts.map +1 -0
- package/dist/plugins/manifest.js +83 -0
- package/dist/plugins/prefetch.d.ts +16 -0
- package/dist/plugins/prefetch.d.ts.map +1 -0
- package/dist/plugins/prefetch.js +40 -0
- package/dist/plugins/registry.d.ts +22 -0
- package/dist/plugins/registry.d.ts.map +1 -0
- package/dist/plugins/registry.js +25 -0
- package/dist/plugins/server.d.ts +2 -0
- package/dist/plugins/server.d.ts.map +1 -1
- package/dist/plugins/server.js +2 -0
- package/dist/plugins/types.d.ts +9 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/plugins/types.js +0 -0
- package/dist/translations/base/en.d.ts +5 -0
- package/dist/translations/base/en.d.ts.map +1 -1
- package/dist/translations/base/en.js +5 -0
- package/dist/translations/client.d.ts +64 -4
- package/dist/translations/client.d.ts.map +1 -1
- package/dist/translations/server.d.ts +64 -4
- package/dist/translations/server.d.ts.map +1 -1
- package/dist/utils/console-log.d.ts +18 -0
- package/dist/utils/console-log.d.ts.map +1 -0
- package/dist/utils/console-log.js +28 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/log.d.ts +18 -0
- package/dist/utils/log.d.ts.map +1 -0
- package/dist/utils/log.js +28 -0
- package/dist/validators/index.d.ts +1 -0
- package/dist/validators/index.d.ts.map +1 -1
- package/dist/validators/index.js +1 -0
- package/dist/validators/tags.d.ts +4 -0
- package/dist/validators/tags.d.ts.map +1 -0
- package/dist/validators/tags.js +8 -0
- package/package.json +36 -18
|
@@ -0,0 +1,1215 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { and, eq, sql } from 'drizzle-orm';
|
|
5
|
+
import { getCMSConfig } from '../../core/config/index.js';
|
|
6
|
+
import { MysqlTableChecker } from '../../core/db/index.js';
|
|
7
|
+
import { FieldFactory, SectionFactory } from '../../core/factories/index.js';
|
|
8
|
+
import { SelectField } from '../../core/fields/index.js';
|
|
9
|
+
import { resolveLocale } from '../../core/localization/index.js';
|
|
10
|
+
import { db } from '../../db/client.js';
|
|
11
|
+
import { EditorPhotosTable } from '../../db/schema.js';
|
|
12
|
+
import { recordLog } from '../../logging/index.js';
|
|
13
|
+
import { getPluginNavigation } from '../../plugins/loader.js';
|
|
14
|
+
import getString from '../../translations/index.js';
|
|
15
|
+
import { resolveLanguage, resolveMultilingualString } from '../../translations/language-utils.js';
|
|
16
|
+
import { getAdminPrivileges } from './privileges.js';
|
|
17
|
+
async function deleteGalleryFiles({ uploadsFolder, sectionName, photos, }) {
|
|
18
|
+
for (const photo of photos) {
|
|
19
|
+
try {
|
|
20
|
+
await fs.promises.unlink(path.join(uploadsFolder, '.photos', sectionName, photo));
|
|
21
|
+
await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', sectionName, photo));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error('Error deleting gallery photo', error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function deleteLocalizedGalleryRows({ section, gallery, sectionItemId, locale, uploadsFolder, }) {
|
|
29
|
+
if (!gallery.localized)
|
|
30
|
+
return;
|
|
31
|
+
const { tableName, referenceIdentifierField, photoNameField } = gallery.db;
|
|
32
|
+
const columns = await MysqlTableChecker.getColumns(tableName);
|
|
33
|
+
if (!columns.includes(referenceIdentifierField) ||
|
|
34
|
+
!columns.includes(photoNameField) ||
|
|
35
|
+
!columns.includes('locale')) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const [rows] = await db.execute(sql `SELECT \`${sql.raw(photoNameField)}\` AS photo FROM \`${sql.raw(tableName)}\` WHERE \`${sql.raw(referenceIdentifierField)}\` = ${sectionItemId} AND \`locale\` = ${locale}`);
|
|
39
|
+
const photos = (rows ?? []).map((row) => row.photo).filter(Boolean);
|
|
40
|
+
await deleteGalleryFiles({ uploadsFolder, sectionName: section.name, photos });
|
|
41
|
+
await db.execute(sql `DELETE FROM \`${sql.raw(tableName)}\` WHERE \`${sql.raw(referenceIdentifierField)}\` = ${sectionItemId} AND \`locale\` = ${locale}`);
|
|
42
|
+
}
|
|
43
|
+
function sectionHasLocalizedContent(section) {
|
|
44
|
+
return section.hasLocalizedContent ?? section.hasLocalizedFields ?? false;
|
|
45
|
+
}
|
|
46
|
+
export const createSimpleSectionPage = async (session, sectionName, locale) => {
|
|
47
|
+
try {
|
|
48
|
+
const cmsConfig = await getCMSConfig();
|
|
49
|
+
const localeResult = resolveLocale({
|
|
50
|
+
localization: cmsConfig.localization,
|
|
51
|
+
locale,
|
|
52
|
+
});
|
|
53
|
+
if (locale && !localeResult.resolvedLocale) {
|
|
54
|
+
if (localeResult.localizationEnabled === false) {
|
|
55
|
+
return {
|
|
56
|
+
error: {
|
|
57
|
+
message: getString('localizationNotEnabledForSection', session.user.language),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
error: {
|
|
63
|
+
message: getString('invalidLocale', session.user.language, {
|
|
64
|
+
locale,
|
|
65
|
+
locales: localeResult.availableLocales.map((l) => l.code).join(', '),
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Let's fetch the section information
|
|
72
|
+
*/
|
|
73
|
+
const fieldsFactory = new FieldFactory({
|
|
74
|
+
type: 'edit',
|
|
75
|
+
sectionName,
|
|
76
|
+
session: session,
|
|
77
|
+
itemId: 1, // itemId is always 1 for simple sections
|
|
78
|
+
});
|
|
79
|
+
await fieldsFactory.initialize();
|
|
80
|
+
if (fieldsFactory.error) {
|
|
81
|
+
return {
|
|
82
|
+
error: {
|
|
83
|
+
message: fieldsFactory.errorMessage,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
await fieldsFactory.generateFields();
|
|
88
|
+
const sectionInfo = fieldsFactory.sectionInfo;
|
|
89
|
+
const gallery = await sectionInfo.getGallery();
|
|
90
|
+
// Get the locale for resolving localized titles
|
|
91
|
+
const uiLanguage = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
|
|
92
|
+
const resolvedTitle = resolveMultilingualString(sectionInfo.title, uiLanguage, cmsConfig.i18n.fallbackLanguage);
|
|
93
|
+
/**
|
|
94
|
+
* Fetch localization metadata for sections with localized fields
|
|
95
|
+
*/
|
|
96
|
+
let localizationData = null;
|
|
97
|
+
if (localeResult.localizationEnabled) {
|
|
98
|
+
let existingTranslations = [];
|
|
99
|
+
const hasLocalizedContent = sectionHasLocalizedContent(sectionInfo);
|
|
100
|
+
if (hasLocalizedContent) {
|
|
101
|
+
const localesTableName = sectionInfo.localesTableName;
|
|
102
|
+
const localeColumns = await MysqlTableChecker.getColumns(localesTableName);
|
|
103
|
+
const localeTableExists = localeColumns.length > 0;
|
|
104
|
+
if (localeTableExists) {
|
|
105
|
+
// Simple sections always have itemId = 1
|
|
106
|
+
const sectionItemId = 1;
|
|
107
|
+
const [rows] = await db.execute(sql `SELECT locale FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId}`);
|
|
108
|
+
existingTranslations = rows.map((r) => r.locale);
|
|
109
|
+
// Override localized field values with locale-specific data
|
|
110
|
+
if (locale && localeResult.resolvedLocale && localeResult.isDefault === false) {
|
|
111
|
+
const [localeRows] = await db.execute(sql `SELECT * FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId} AND locale = ${localeResult.resolvedLocale.code} LIMIT 1`);
|
|
112
|
+
const localeRow = localeRows[0] ?? null;
|
|
113
|
+
for (const field of sectionInfo.fields) {
|
|
114
|
+
if (!field.localized)
|
|
115
|
+
continue;
|
|
116
|
+
// For select/tags with destinationDb, fetch locale-scoped junction values
|
|
117
|
+
const f = field;
|
|
118
|
+
if (f.destinationDb && (field.type === 'select' || field.type === 'select_multiple')) {
|
|
119
|
+
if (f.db) {
|
|
120
|
+
const [_rows] = await db.execute(sql `SELECT * FROM \`${sql.raw(f.destinationDb.table)}\` a JOIN \`${sql.raw(f.db.table)}\` b ON a.\`${sql.raw(f.destinationDb.selectIdentifier)}\` = b.\`${sql.raw(f.db.identifier)}\` WHERE a.\`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND a.\`locale\` = ${localeResult.resolvedLocale.code}`);
|
|
121
|
+
const values = Array.isArray(_rows)
|
|
122
|
+
? _rows.map((row) => ({
|
|
123
|
+
value: row[f.destinationDb.selectIdentifier],
|
|
124
|
+
label: row[f.db.label],
|
|
125
|
+
}))
|
|
126
|
+
: [];
|
|
127
|
+
field.setValue(values);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const [_rows] = await db.execute(sql `SELECT * FROM \`${sql.raw(f.destinationDb.table)}\` WHERE \`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND \`locale\` = ${localeResult.resolvedLocale.code}`);
|
|
131
|
+
const values = Array.isArray(_rows)
|
|
132
|
+
? _rows.map((row) => ({
|
|
133
|
+
value: row[f.destinationDb.selectIdentifier],
|
|
134
|
+
label: f.options?.find((opt) => opt.value?.toString() ===
|
|
135
|
+
row[f.destinationDb.selectIdentifier]?.toString())?.label ?? '',
|
|
136
|
+
}))
|
|
137
|
+
: [];
|
|
138
|
+
field.setValue(values);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (f.destinationDb && field.type === 'tags') {
|
|
142
|
+
const [_rows] = await db.execute(sql `SELECT \`${sql.raw(f.destinationDb.selectIdentifier)}\` FROM \`${sql.raw(f.destinationDb.table)}\` WHERE \`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND \`locale\` = ${localeResult.resolvedLocale.code}`);
|
|
143
|
+
const tags = Array.isArray(_rows)
|
|
144
|
+
? _rows.map((row) => row[f.destinationDb.selectIdentifier])
|
|
145
|
+
: [];
|
|
146
|
+
field.setValue(tags.join(','));
|
|
147
|
+
}
|
|
148
|
+
else if (field.type === 'date_range' && typeof f.setRangeValues === 'function') {
|
|
149
|
+
f.setRangeValues(localeRow ? (localeRow[f.startName] ?? null) : null, localeRow ? (localeRow[f.endName] ?? null) : null);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// Simple field: override from locale row
|
|
153
|
+
field.setValue(localeRow ? (localeRow[field.name] ?? null) : null);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Show the locale switcher when multiple locales are configured AND either:
|
|
160
|
+
// - the section has localized fields (production case), or
|
|
161
|
+
// - the app is in development mode (so devs can preview the switcher
|
|
162
|
+
// while wiring up localization on a section).
|
|
163
|
+
// The developer note is rendered in dev-mode only, when the switcher is
|
|
164
|
+
// visible but the section itself has no localized content yet.
|
|
165
|
+
const localeSwitcherEnabled = localeResult.availableLocales.length > 1 &&
|
|
166
|
+
(hasLocalizedContent === true || process.env.NODE_ENV === 'development');
|
|
167
|
+
const developerNoteEnabled = localeSwitcherEnabled && hasLocalizedContent === false;
|
|
168
|
+
localizationData = {
|
|
169
|
+
defaultLocale: localeResult.defaultLocale,
|
|
170
|
+
currentLocale: localeResult.resolvedLocale ?? localeResult.defaultLocale,
|
|
171
|
+
existingTranslations,
|
|
172
|
+
locales: localeResult.availableLocales,
|
|
173
|
+
localeSwitcherEnabled,
|
|
174
|
+
developerNoteEnabled,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
section: {
|
|
179
|
+
name: sectionInfo.name,
|
|
180
|
+
title: resolvedTitle,
|
|
181
|
+
gallery: gallery,
|
|
182
|
+
variants: sectionInfo.variants,
|
|
183
|
+
configFile: sectionInfo.configFile,
|
|
184
|
+
},
|
|
185
|
+
inputGroups: await fieldsFactory.getGroupedFields(),
|
|
186
|
+
localization: localizationData,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
191
|
+
console.error('Error creating section page', err);
|
|
192
|
+
return {
|
|
193
|
+
error: {
|
|
194
|
+
message: `Failed to create section page: ${errorMessage}`,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
export const deleteSectionItem = async (session, sectionName, sectionItemId, recursive, requestMetadata) => {
|
|
200
|
+
/**
|
|
201
|
+
* Convert the section item id to string
|
|
202
|
+
*/
|
|
203
|
+
sectionItemId = sectionItemId.toString();
|
|
204
|
+
try {
|
|
205
|
+
const _s = (await SectionFactory.getSectionForAdmin({
|
|
206
|
+
name: sectionName,
|
|
207
|
+
admin: {
|
|
208
|
+
id: session.user.id,
|
|
209
|
+
requiredRole: 'D',
|
|
210
|
+
},
|
|
211
|
+
}));
|
|
212
|
+
const section = _s?.build();
|
|
213
|
+
if (!section) {
|
|
214
|
+
return {
|
|
215
|
+
error: {
|
|
216
|
+
message: getString('sectionNotFound', session.user.language),
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const tableName = section.db.table;
|
|
221
|
+
const identifierFieldName = section.db.identifier.name;
|
|
222
|
+
const columns = await MysqlTableChecker.getColumns(tableName);
|
|
223
|
+
if (!columns.includes(identifierFieldName)) {
|
|
224
|
+
return {
|
|
225
|
+
error: {
|
|
226
|
+
message: getString('sectionTableIdentifierNotFound', session.user.language),
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Get the section item from the table
|
|
232
|
+
*/
|
|
233
|
+
const [_v, _f] = await db.execute(sql `select * from ${sql.raw(tableName)} where ${sql.raw(identifierFieldName)} = ${sectionItemId} limit 1`);
|
|
234
|
+
// @ts-ignore
|
|
235
|
+
// Bug: this is a bug in drizzle-orm/mysql2
|
|
236
|
+
const sectionItemRow = _v[0];
|
|
237
|
+
/**
|
|
238
|
+
* Run beforeDelete hook before the actual deletion
|
|
239
|
+
*/
|
|
240
|
+
if (section.hooks?.beforeDelete) {
|
|
241
|
+
const hook = section.hooks.beforeDelete;
|
|
242
|
+
const handler = typeof hook === 'function' ? hook : hook.handler;
|
|
243
|
+
await handler({
|
|
244
|
+
itemId: sectionItemId,
|
|
245
|
+
values: sectionItemRow ?? {},
|
|
246
|
+
section: section,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Cascade delete translations from the locales table.
|
|
251
|
+
* Use deleteLocaleTranslation per locale to ensure localized files are cleaned up from disk.
|
|
252
|
+
*/
|
|
253
|
+
const cmsConfig = await getCMSConfig();
|
|
254
|
+
if (cmsConfig.localization?.enabled && sectionHasLocalizedContent(section)) {
|
|
255
|
+
const localesTableName = section.localesTableName;
|
|
256
|
+
const localeColumns = await MysqlTableChecker.getColumns(localesTableName);
|
|
257
|
+
if (localeColumns.length > 0) {
|
|
258
|
+
const [localeRows] = await db.execute(sql `SELECT locale FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId}`);
|
|
259
|
+
const locales = localeRows.map((r) => r.locale);
|
|
260
|
+
for (const locale of locales) {
|
|
261
|
+
const result = await deleteLocaleTranslation(session, sectionName, sectionItemId, locale);
|
|
262
|
+
if (result && typeof result === 'object' && 'error' in result) {
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Grab the file fields to delete the files
|
|
270
|
+
*/
|
|
271
|
+
const fileFieldConfigs = section.fieldConfigs.filter((fieldConfig) => fieldConfig.type === 'document' || fieldConfig.type === 'photo' || fieldConfig.type === 'video');
|
|
272
|
+
const uploadsFolder = cmsConfig.media.upload.path;
|
|
273
|
+
if (sectionItemRow) {
|
|
274
|
+
for (const field of fileFieldConfigs) {
|
|
275
|
+
// @ts-ignore
|
|
276
|
+
const value = sectionItemRow[field.name];
|
|
277
|
+
if (value) {
|
|
278
|
+
if (field.type === 'document') {
|
|
279
|
+
try {
|
|
280
|
+
await fs.promises.unlink(path.join(uploadsFolder, '.documents', section.name, value));
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
// Log but continue - file may not exist or already deleted
|
|
284
|
+
console.error('Error deleting document', error);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else if (field.type === 'video') {
|
|
288
|
+
try {
|
|
289
|
+
await fs.promises.unlink(path.join(uploadsFolder, '.videos', section.name, value));
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
// Log but continue - file may not exist or already deleted
|
|
293
|
+
console.error('Error deleting video', error);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
try {
|
|
298
|
+
await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, value));
|
|
299
|
+
await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', section.name, value));
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
// Log but continue - file may not exist or already deleted
|
|
303
|
+
console.error('Error deleting photo', error);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Delete the rows from the destination table if they exist
|
|
311
|
+
*/
|
|
312
|
+
const fieldsWithDestinationDbTable = section.fieldConfigs.filter((fieldConfig) => ['select_multiple', 'select', 'tags'].includes(fieldConfig.type));
|
|
313
|
+
for (const fieldConfig of fieldsWithDestinationDbTable) {
|
|
314
|
+
const field = fieldConfig.build();
|
|
315
|
+
if (field.destinationDb) {
|
|
316
|
+
const columns = await MysqlTableChecker.getColumns(field.destinationDb.table);
|
|
317
|
+
if (columns.includes(field.destinationDb.selectIdentifier) &&
|
|
318
|
+
columns.includes(field.destinationDb.itemIdentifier)) {
|
|
319
|
+
await db.execute(sql `delete from ${sql.raw(field.destinationDb.table)} where ${sql.raw(field.destinationDb.itemIdentifier)} = ${sectionItemId}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Also delete the gallery photos if they exist
|
|
325
|
+
*/
|
|
326
|
+
const gallery = await section.getGallery();
|
|
327
|
+
if (gallery) {
|
|
328
|
+
const { tableName, referenceIdentifierField, photoNameField, metaField } = gallery.db;
|
|
329
|
+
const columns = await MysqlTableChecker.getColumns(tableName);
|
|
330
|
+
if (columns.includes(photoNameField) &&
|
|
331
|
+
columns.includes(referenceIdentifierField) &&
|
|
332
|
+
columns.includes(metaField)) {
|
|
333
|
+
/**
|
|
334
|
+
* First, get the gallery photos
|
|
335
|
+
*/
|
|
336
|
+
const [_v, _f] = await db.execute(sql `select * from \`${sql.raw(tableName)}\` where \`${sql.raw(referenceIdentifierField)}\` = ${sectionItemId}`);
|
|
337
|
+
// @ts-ignore
|
|
338
|
+
// Bug: this is a bug in drizzle-orm/mysql2
|
|
339
|
+
const galleryPhotos = _v;
|
|
340
|
+
/**
|
|
341
|
+
* Delete the photos from disk
|
|
342
|
+
*/
|
|
343
|
+
for (const photo of galleryPhotos) {
|
|
344
|
+
try {
|
|
345
|
+
await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, photo[photoNameField]));
|
|
346
|
+
await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', section.name, photo[photoNameField]));
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
// Log but continue - file may not exist or already deleted
|
|
350
|
+
console.error('Error deleting photo', error);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Delete the photos from the table
|
|
355
|
+
*/
|
|
356
|
+
await db.execute(sql `DELETE FROM ${sql.raw(tableName)} WHERE ${sql.raw(referenceIdentifierField)} = ${sectionItemId}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Check if there are photos in the editor_photos table
|
|
361
|
+
*/
|
|
362
|
+
const editorPhotos = await db
|
|
363
|
+
.select()
|
|
364
|
+
.from(EditorPhotosTable)
|
|
365
|
+
.where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, sectionItemId)));
|
|
366
|
+
if (editorPhotos.length > 0) {
|
|
367
|
+
/**
|
|
368
|
+
* Delete the photos from disk
|
|
369
|
+
*/
|
|
370
|
+
for (const photo of editorPhotos) {
|
|
371
|
+
try {
|
|
372
|
+
await fs.promises.unlink(path.join(uploadsFolder, '.photos', photo.section, photo.name));
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
// Log but continue - file may not exist or already deleted
|
|
376
|
+
console.error('Error deleting photo', error);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Delete the photos from the editor_photos table
|
|
381
|
+
*/
|
|
382
|
+
await db
|
|
383
|
+
.delete(EditorPhotosTable)
|
|
384
|
+
.where(and(eq(EditorPhotosTable.section, sectionName), eq(EditorPhotosTable.itemId, sectionItemId)));
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Delete the row from the table
|
|
388
|
+
*/
|
|
389
|
+
await db.execute(sql `delete from \`${sql.raw(tableName)}\` where \`${sql.raw(identifierFieldName)}\` = ${sectionItemId}`);
|
|
390
|
+
/**
|
|
391
|
+
* Run afterDelete hook after the deletion
|
|
392
|
+
*/
|
|
393
|
+
if (section.hooks?.afterDelete) {
|
|
394
|
+
const hook = section.hooks.afterDelete;
|
|
395
|
+
const handler = typeof hook === 'function' ? hook : hook.handler;
|
|
396
|
+
try {
|
|
397
|
+
await handler({
|
|
398
|
+
itemId: sectionItemId,
|
|
399
|
+
values: sectionItemRow ?? {},
|
|
400
|
+
section: section,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
console.error('afterDelete hook failed:', error);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const headingFieldName = section.headingField.name;
|
|
408
|
+
const entityLabel = headingFieldName && sectionItemRow
|
|
409
|
+
? // @ts-ignore
|
|
410
|
+
String(sectionItemRow[headingFieldName] ?? '')
|
|
411
|
+
: null;
|
|
412
|
+
await recordLog({
|
|
413
|
+
eventType: 'section.item.delete',
|
|
414
|
+
actorId: session.user.id,
|
|
415
|
+
actorUsername: session.user.name,
|
|
416
|
+
entityType: 'section_item',
|
|
417
|
+
entityId: sectionItemId,
|
|
418
|
+
entityLabel: entityLabel?.trim() ? entityLabel : null,
|
|
419
|
+
sectionName: section.name,
|
|
420
|
+
metadata: {
|
|
421
|
+
sectionType: section.type,
|
|
422
|
+
recursive: Boolean(recursive),
|
|
423
|
+
},
|
|
424
|
+
requestMetadata,
|
|
425
|
+
});
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
430
|
+
console.error('Error deleting section item', err);
|
|
431
|
+
return {
|
|
432
|
+
error: {
|
|
433
|
+
message: `Failed to delete section item: ${errorMessage}`,
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
export const deleteLocaleTranslation = async (session, sectionName, sectionItemId, locale) => {
|
|
439
|
+
sectionItemId = sectionItemId.toString();
|
|
440
|
+
try {
|
|
441
|
+
const _s = (await SectionFactory.getSectionForAdmin({
|
|
442
|
+
name: sectionName,
|
|
443
|
+
admin: {
|
|
444
|
+
id: session.user.id,
|
|
445
|
+
requiredRole: 'D',
|
|
446
|
+
},
|
|
447
|
+
}));
|
|
448
|
+
const section = _s?.build();
|
|
449
|
+
if (!section) {
|
|
450
|
+
return {
|
|
451
|
+
error: {
|
|
452
|
+
message: getString('sectionNotFound', session.user.language),
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const cmsConfig = await getCMSConfig();
|
|
457
|
+
if (!cmsConfig.localization?.enabled || !sectionHasLocalizedContent(section)) {
|
|
458
|
+
return {
|
|
459
|
+
error: {
|
|
460
|
+
message: getString('localizationNotEnabledForSection', session.user.language),
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
if (locale === cmsConfig.localization.defaultLocale) {
|
|
465
|
+
return {
|
|
466
|
+
error: {
|
|
467
|
+
message: getString('cannotDeleteBaseLocaleTranslation', session.user.language),
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const localesTableName = section.localesTableName;
|
|
472
|
+
const localeColumns = await MysqlTableChecker.getColumns(localesTableName);
|
|
473
|
+
if (localeColumns.length === 0) {
|
|
474
|
+
return {
|
|
475
|
+
error: {
|
|
476
|
+
message: getString('localesTableDoesNotExist', session.user.language),
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
// Fetch the locale row before deleting so we can clean up files
|
|
481
|
+
const [localeRows] = await db.execute(sql `SELECT * FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId} AND locale = ${locale} LIMIT 1`);
|
|
482
|
+
const localeRow = localeRows[0] ?? null;
|
|
483
|
+
const beforeDeleteHook = section.hooks?.beforeDelete;
|
|
484
|
+
if (beforeDeleteHook && typeof beforeDeleteHook === 'object' && beforeDeleteHook.runForLocales) {
|
|
485
|
+
await beforeDeleteHook.handler({
|
|
486
|
+
itemId: sectionItemId,
|
|
487
|
+
values: localeRow ?? {},
|
|
488
|
+
section: section,
|
|
489
|
+
locale,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
// Delete files for localized photo/document/video fields
|
|
493
|
+
if (localeRow) {
|
|
494
|
+
const uploadsFolder = cmsConfig.media.upload.path;
|
|
495
|
+
const localizedFileFields = section.fieldConfigs.filter((f) => f.localized === true && (f.type === 'photo' || f.type === 'document' || f.type === 'video'));
|
|
496
|
+
for (const field of localizedFileFields) {
|
|
497
|
+
const value = localeRow[field.name];
|
|
498
|
+
if (!value)
|
|
499
|
+
continue;
|
|
500
|
+
if (field.type === 'document') {
|
|
501
|
+
try {
|
|
502
|
+
await fs.promises.unlink(path.join(uploadsFolder, '.documents', section.name, value));
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
console.error('Error deleting document', error);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
else if (field.type === 'video') {
|
|
509
|
+
try {
|
|
510
|
+
await fs.promises.unlink(path.join(uploadsFolder, '.videos', section.name, value));
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
console.error('Error deleting video', error);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
// photo
|
|
518
|
+
try {
|
|
519
|
+
await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, value));
|
|
520
|
+
await fs.promises.unlink(path.join(uploadsFolder, '.thumbs', section.name, value));
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
console.error('Error deleting photo', error);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const gallery = await section.getGallery();
|
|
529
|
+
if (gallery?.localized) {
|
|
530
|
+
await deleteLocalizedGalleryRows({
|
|
531
|
+
section,
|
|
532
|
+
gallery,
|
|
533
|
+
sectionItemId,
|
|
534
|
+
locale,
|
|
535
|
+
uploadsFolder: cmsConfig.media.upload.path,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
// Delete locale-scoped editor photos for localized rich_text fields.
|
|
539
|
+
// Uses raw SQL because the `locale` column only exists on the DB table when
|
|
540
|
+
// localization is enabled; the Drizzle schema no longer declares it.
|
|
541
|
+
const [editorPhotoRows] = await db.execute(sql `SELECT \`photo\` as \`name\` FROM \`editor_photos\` WHERE \`section\` = ${sectionName} AND \`item_id\` = ${sectionItemId.toString()} AND \`locale\` = ${locale}`);
|
|
542
|
+
const editorPhotos = editorPhotoRows ?? [];
|
|
543
|
+
if (editorPhotos.length > 0) {
|
|
544
|
+
const uploadsFolder = cmsConfig.media.upload.path;
|
|
545
|
+
for (const photo of editorPhotos) {
|
|
546
|
+
try {
|
|
547
|
+
await fs.promises.unlink(path.join(uploadsFolder, '.photos', section.name, photo.name));
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
console.error('Error deleting editor photo', error);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
await db.execute(sql `DELETE FROM \`editor_photos\` WHERE \`section\` = ${sectionName} AND \`item_id\` = ${sectionItemId.toString()} AND \`locale\` = ${locale}`);
|
|
554
|
+
}
|
|
555
|
+
// Delete locale-scoped junction table rows for localized select/tags fields
|
|
556
|
+
const localizedJunctionFields = section.fieldConfigs.filter((f) => f.localized === true &&
|
|
557
|
+
f.destinationDb &&
|
|
558
|
+
(f.type === 'select' || f.type === 'select_multiple' || f.type === 'tags'));
|
|
559
|
+
for (const field of localizedJunctionFields) {
|
|
560
|
+
const destDb = field.destinationDb;
|
|
561
|
+
if (!destDb)
|
|
562
|
+
continue;
|
|
563
|
+
try {
|
|
564
|
+
await db.execute(sql `DELETE FROM \`${sql.raw(destDb.table)}\` WHERE \`${sql.raw(destDb.itemIdentifier)}\` = ${sectionItemId} AND \`locale\` = ${locale}`);
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
console.error(`Error deleting junction table rows for ${field.name}`, error);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
await db.execute(sql `DELETE FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId} AND locale = ${locale}`);
|
|
571
|
+
const afterDeleteHook = section.hooks?.afterDelete;
|
|
572
|
+
if (afterDeleteHook && typeof afterDeleteHook === 'object' && afterDeleteHook.runForLocales) {
|
|
573
|
+
try {
|
|
574
|
+
await afterDeleteHook.handler({
|
|
575
|
+
itemId: sectionItemId,
|
|
576
|
+
values: localeRow ?? {},
|
|
577
|
+
section: section,
|
|
578
|
+
locale,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
catch (e) {
|
|
582
|
+
console.error('afterDelete hook failed:', e);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
await recordLog({
|
|
586
|
+
eventType: 'section.item.locale.delete',
|
|
587
|
+
actorId: session.user.id,
|
|
588
|
+
actorUsername: session.user.name,
|
|
589
|
+
entityType: 'section_item_locale',
|
|
590
|
+
entityId: sectionItemId,
|
|
591
|
+
entityLabel: locale,
|
|
592
|
+
sectionName: section.name,
|
|
593
|
+
metadata: {
|
|
594
|
+
locale,
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
catch (err) {
|
|
600
|
+
const errorMessage = err instanceof Error ? err.message : getString('unknownErrorOccurred', session.user.language);
|
|
601
|
+
console.error('Error deleting locale translation', err);
|
|
602
|
+
return {
|
|
603
|
+
error: {
|
|
604
|
+
message: getString('deleteLocaleTranslationFailed', session.user.language, { detail: errorMessage }),
|
|
605
|
+
},
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
export const createEditPage = async (session, sectionName, sectionItemId, locale) => {
|
|
610
|
+
/**
|
|
611
|
+
* Generate the fields for the edit page
|
|
612
|
+
*/
|
|
613
|
+
const fieldsFactory = new FieldFactory({
|
|
614
|
+
type: 'edit',
|
|
615
|
+
sectionName,
|
|
616
|
+
session,
|
|
617
|
+
itemId: sectionItemId,
|
|
618
|
+
});
|
|
619
|
+
try {
|
|
620
|
+
const cmsConfig = await getCMSConfig();
|
|
621
|
+
const localeResult = resolveLocale({
|
|
622
|
+
localization: cmsConfig.localization,
|
|
623
|
+
locale,
|
|
624
|
+
});
|
|
625
|
+
if (locale && !localeResult.resolvedLocale) {
|
|
626
|
+
if (localeResult.localizationEnabled === false) {
|
|
627
|
+
return {
|
|
628
|
+
error: {
|
|
629
|
+
message: getString('localizationNotEnabledForSection', session.user.language),
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
error: {
|
|
635
|
+
message: getString('invalidLocale', session.user.language, {
|
|
636
|
+
locale,
|
|
637
|
+
locales: localeResult.availableLocales.map((l) => l.code).join(', '),
|
|
638
|
+
}),
|
|
639
|
+
},
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
await fieldsFactory.initialize();
|
|
643
|
+
await fieldsFactory.generateFields();
|
|
644
|
+
if (fieldsFactory.error) {
|
|
645
|
+
return {
|
|
646
|
+
error: {
|
|
647
|
+
message: fieldsFactory.errorMessage,
|
|
648
|
+
},
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Let's check for variants
|
|
653
|
+
*/
|
|
654
|
+
/*const variants = section[0].variants
|
|
655
|
+
const sectionVariants: { name: string }[] = []
|
|
656
|
+
|
|
657
|
+
if (variants && variants.trim() !== '') {
|
|
658
|
+
/!**
|
|
659
|
+
* Convert to JSON
|
|
660
|
+
*!/
|
|
661
|
+
const variantsJson = JSON.parse(variants)
|
|
662
|
+
|
|
663
|
+
/!**
|
|
664
|
+
* Loop through the variants
|
|
665
|
+
*!/
|
|
666
|
+
|
|
667
|
+
variantsJson.forEach((variant: any) => {
|
|
668
|
+
const variantName = variant.name
|
|
669
|
+
const variantInfo = variant.info
|
|
670
|
+
})
|
|
671
|
+
}*/
|
|
672
|
+
/**
|
|
673
|
+
* Get the gallery photos
|
|
674
|
+
* TODO: This is a temp implementation, will be removed once converting the gallery into a field
|
|
675
|
+
*/
|
|
676
|
+
let galleryItems = [];
|
|
677
|
+
const gallery = await fieldsFactory.sectionInfo?.getGallery();
|
|
678
|
+
if (gallery) {
|
|
679
|
+
const { tableName, referenceIdentifierField, photoNameField, metaField } = gallery.db;
|
|
680
|
+
const columns = await MysqlTableChecker.getColumns(tableName);
|
|
681
|
+
if (columns.includes(photoNameField) &&
|
|
682
|
+
columns.includes(referenceIdentifierField) &&
|
|
683
|
+
columns.includes(metaField)) {
|
|
684
|
+
const galleryIsLocalized = gallery.localized === true && localeResult.localizationEnabled;
|
|
685
|
+
const currentLocale = localeResult.resolvedLocale?.code;
|
|
686
|
+
const localeFilter = galleryIsLocalized && currentLocale && columns.includes('locale')
|
|
687
|
+
? sql ` AND \`locale\` = ${currentLocale}`
|
|
688
|
+
: sql ``;
|
|
689
|
+
const [items] = await db.execute(sql `select * from \`${sql.raw(tableName)}\` where \`${sql.raw(referenceIdentifierField)}\` = ${sectionItemId}${localeFilter}`);
|
|
690
|
+
const galleryPhotos = items;
|
|
691
|
+
galleryPhotos?.map((item) => {
|
|
692
|
+
galleryItems.push({
|
|
693
|
+
referenceId: item[referenceIdentifierField],
|
|
694
|
+
photo: item[photoNameField],
|
|
695
|
+
meta: item[metaField],
|
|
696
|
+
locale: item.locale,
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const sectionInfo = fieldsFactory.sectionInfo;
|
|
702
|
+
// Resolve localized titles using the user's language
|
|
703
|
+
const uiLanguage = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
|
|
704
|
+
const resolvedTitle = {
|
|
705
|
+
section: resolveMultilingualString(sectionInfo.title.section, uiLanguage, cmsConfig.i18n.fallbackLanguage),
|
|
706
|
+
singular: resolveMultilingualString(sectionInfo.title.singular, uiLanguage, cmsConfig.i18n.fallbackLanguage),
|
|
707
|
+
plural: resolveMultilingualString(sectionInfo.title.plural, uiLanguage, cmsConfig.i18n.fallbackLanguage),
|
|
708
|
+
};
|
|
709
|
+
/**
|
|
710
|
+
* Fetch localization metadata for sections with localized fields
|
|
711
|
+
*/
|
|
712
|
+
let localizationData = null;
|
|
713
|
+
if (localeResult.localizationEnabled) {
|
|
714
|
+
let existingTranslations = [];
|
|
715
|
+
const hasLocalizedContent = sectionHasLocalizedContent(sectionInfo);
|
|
716
|
+
if (hasLocalizedContent) {
|
|
717
|
+
const localesTableName = sectionInfo.localesTableName;
|
|
718
|
+
const localeColumns = await MysqlTableChecker.getColumns(localesTableName);
|
|
719
|
+
const localeTableExists = localeColumns.length > 0;
|
|
720
|
+
if (localeTableExists) {
|
|
721
|
+
const [rows] = await db.execute(sql `SELECT locale FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId}`);
|
|
722
|
+
existingTranslations = rows.map((r) => r.locale);
|
|
723
|
+
// Override localized field values with locale-specific data
|
|
724
|
+
if (locale && localeResult.resolvedLocale && localeResult.isDefault === false) {
|
|
725
|
+
const [localeRows] = await db.execute(sql `SELECT * FROM \`${sql.raw(localesTableName)}\` WHERE parent_id = ${sectionItemId} AND locale = ${localeResult.resolvedLocale.code} LIMIT 1`);
|
|
726
|
+
const localeRow = localeRows[0] ?? null;
|
|
727
|
+
for (const field of sectionInfo.fields) {
|
|
728
|
+
if (!field.localized)
|
|
729
|
+
continue;
|
|
730
|
+
// For select/tags with destinationDb, fetch locale-scoped junction values
|
|
731
|
+
const f = field;
|
|
732
|
+
if (f.destinationDb && (field.type === 'select' || field.type === 'select_multiple')) {
|
|
733
|
+
if (f.db) {
|
|
734
|
+
const [_rows] = await db.execute(sql `SELECT * FROM \`${sql.raw(f.destinationDb.table)}\` a JOIN \`${sql.raw(f.db.table)}\` b ON a.\`${sql.raw(f.destinationDb.selectIdentifier)}\` = b.\`${sql.raw(f.db.identifier)}\` WHERE a.\`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND a.\`locale\` = ${localeResult.resolvedLocale.code}`);
|
|
735
|
+
const values = Array.isArray(_rows)
|
|
736
|
+
? _rows.map((row) => ({
|
|
737
|
+
value: row[f.destinationDb.selectIdentifier],
|
|
738
|
+
label: row[f.db.label],
|
|
739
|
+
}))
|
|
740
|
+
: [];
|
|
741
|
+
field.setValue(values);
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
const [_rows] = await db.execute(sql `SELECT * FROM \`${sql.raw(f.destinationDb.table)}\` WHERE \`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND \`locale\` = ${localeResult.resolvedLocale.code}`);
|
|
745
|
+
const values = Array.isArray(_rows)
|
|
746
|
+
? _rows.map((row) => ({
|
|
747
|
+
value: row[f.destinationDb.selectIdentifier],
|
|
748
|
+
label: f.options?.find((opt) => opt.value?.toString() ===
|
|
749
|
+
row[f.destinationDb.selectIdentifier]?.toString())?.label ?? '',
|
|
750
|
+
}))
|
|
751
|
+
: [];
|
|
752
|
+
field.setValue(values);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
else if (f.destinationDb && field.type === 'tags') {
|
|
756
|
+
const [_rows] = await db.execute(sql `SELECT \`${sql.raw(f.destinationDb.selectIdentifier)}\` FROM \`${sql.raw(f.destinationDb.table)}\` WHERE \`${sql.raw(f.destinationDb.itemIdentifier)}\` = ${sectionItemId} AND \`locale\` = ${localeResult.resolvedLocale.code}`);
|
|
757
|
+
const tags = Array.isArray(_rows)
|
|
758
|
+
? _rows.map((row) => row[f.destinationDb.selectIdentifier])
|
|
759
|
+
: [];
|
|
760
|
+
field.setValue(tags.join(','));
|
|
761
|
+
}
|
|
762
|
+
else if (field.type === 'date_range' && typeof f.setRangeValues === 'function') {
|
|
763
|
+
f.setRangeValues(localeRow ? (localeRow[f.startName] ?? null) : null, localeRow ? (localeRow[f.endName] ?? null) : null);
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
// Simple field: override from locale row
|
|
767
|
+
field.setValue(localeRow ? (localeRow[field.name] ?? null) : null);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
// Show the locale switcher when multiple locales are configured AND either:
|
|
774
|
+
// - the section has localized fields (production case), or
|
|
775
|
+
// - the app is in development mode (so devs can preview the switcher
|
|
776
|
+
// while wiring up localization on a section).
|
|
777
|
+
// The developer note is rendered in dev-mode only, when the switcher is
|
|
778
|
+
// visible but the section itself has no localized content yet.
|
|
779
|
+
const localeSwitcherEnabled = localeResult.availableLocales.length > 1 &&
|
|
780
|
+
(hasLocalizedContent === true || process.env.NODE_ENV === 'development');
|
|
781
|
+
const developerNoteEnabled = localeSwitcherEnabled && hasLocalizedContent === false;
|
|
782
|
+
localizationData = {
|
|
783
|
+
defaultLocale: localeResult.defaultLocale,
|
|
784
|
+
currentLocale: localeResult.resolvedLocale ?? localeResult.defaultLocale,
|
|
785
|
+
existingTranslations,
|
|
786
|
+
locales: localeResult.availableLocales,
|
|
787
|
+
localeSwitcherEnabled,
|
|
788
|
+
developerNoteEnabled,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
section: {
|
|
793
|
+
name: sectionInfo.name,
|
|
794
|
+
title: resolvedTitle,
|
|
795
|
+
gallery: gallery,
|
|
796
|
+
variants: sectionInfo.variants,
|
|
797
|
+
configFile: sectionInfo.configFile,
|
|
798
|
+
},
|
|
799
|
+
inputGroups: await fieldsFactory.getGroupedFields(),
|
|
800
|
+
gallery: galleryItems,
|
|
801
|
+
localization: localizationData,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
const errorMessage = error?.errorMessage || (error instanceof Error ? error.message : 'Unknown error occurred');
|
|
806
|
+
console.error('Error creating edit page', error);
|
|
807
|
+
return {
|
|
808
|
+
error: {
|
|
809
|
+
message: errorMessage,
|
|
810
|
+
},
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
export const createNewPage = async (session, sectionName) => {
|
|
815
|
+
try {
|
|
816
|
+
const fieldsFactory = new FieldFactory({
|
|
817
|
+
type: 'new',
|
|
818
|
+
sectionName,
|
|
819
|
+
session,
|
|
820
|
+
});
|
|
821
|
+
await fieldsFactory.initialize();
|
|
822
|
+
if (fieldsFactory.error) {
|
|
823
|
+
return {
|
|
824
|
+
error: {
|
|
825
|
+
message: fieldsFactory.errorMessage,
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
await fieldsFactory.generateFields();
|
|
831
|
+
}
|
|
832
|
+
catch (err) {
|
|
833
|
+
// console.error('Error generating fields', err)
|
|
834
|
+
return {
|
|
835
|
+
error: {
|
|
836
|
+
message: err.message,
|
|
837
|
+
},
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Let's check for variants
|
|
842
|
+
*/
|
|
843
|
+
/*const variants = section[0].variants
|
|
844
|
+
const sectionVariants: { name: string }[] = []
|
|
845
|
+
|
|
846
|
+
if (variants && variants.trim() !== '') {
|
|
847
|
+
/!**
|
|
848
|
+
* Convert to JSON
|
|
849
|
+
*!/
|
|
850
|
+
const variantsJson = JSON.parse(variants)
|
|
851
|
+
|
|
852
|
+
/!**
|
|
853
|
+
* Loop through the variants
|
|
854
|
+
*!/
|
|
855
|
+
|
|
856
|
+
variantsJson.forEach((variant: any) => {
|
|
857
|
+
const variantName = variant.name
|
|
858
|
+
const variantInfo = variant.info
|
|
859
|
+
})
|
|
860
|
+
}*/
|
|
861
|
+
const sectionInfo = fieldsFactory.sectionInfo;
|
|
862
|
+
const gallery = await sectionInfo.getGallery();
|
|
863
|
+
// Get the locale for resolving localized titles
|
|
864
|
+
const cmsConfig = await getCMSConfig();
|
|
865
|
+
const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
|
|
866
|
+
const resolvedTitle = {
|
|
867
|
+
section: resolveMultilingualString(sectionInfo.title.section, language, cmsConfig.i18n.fallbackLanguage),
|
|
868
|
+
singular: resolveMultilingualString(sectionInfo.title.singular, language, cmsConfig.i18n.fallbackLanguage),
|
|
869
|
+
plural: resolveMultilingualString(sectionInfo.title.plural, language, cmsConfig.i18n.fallbackLanguage),
|
|
870
|
+
};
|
|
871
|
+
const defaultLocale = cmsConfig.localization?.enabled
|
|
872
|
+
? (cmsConfig.localization.locales.find((l) => l.code === cmsConfig.localization.defaultLocale) ?? null)
|
|
873
|
+
: null;
|
|
874
|
+
return {
|
|
875
|
+
section: {
|
|
876
|
+
name: sectionInfo.name,
|
|
877
|
+
title: resolvedTitle,
|
|
878
|
+
gallery: gallery,
|
|
879
|
+
variants: sectionInfo.variants,
|
|
880
|
+
configFile: sectionInfo.configFile,
|
|
881
|
+
},
|
|
882
|
+
inputGroups: await fieldsFactory.getGroupedFields(),
|
|
883
|
+
defaultLocale,
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
catch (err) {
|
|
887
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
888
|
+
console.error('Error creating new page', err);
|
|
889
|
+
return {
|
|
890
|
+
error: {
|
|
891
|
+
message: errorMessage,
|
|
892
|
+
},
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
export const getCategorySectionChildren = async ({ session, id, sectionName, level, }) => {
|
|
897
|
+
/**
|
|
898
|
+
* First, level up to get the children (next level)
|
|
899
|
+
*/
|
|
900
|
+
level++;
|
|
901
|
+
// Let's fetch the section items and admin privileges for the section
|
|
902
|
+
const _s = (await SectionFactory.getSectionForAdmin({
|
|
903
|
+
name: sectionName,
|
|
904
|
+
admin: {
|
|
905
|
+
id: session.user.id,
|
|
906
|
+
},
|
|
907
|
+
}));
|
|
908
|
+
const section = _s?.build();
|
|
909
|
+
if (!section) {
|
|
910
|
+
return {
|
|
911
|
+
error: {
|
|
912
|
+
message: getString('sectionNotFound', session.user.language),
|
|
913
|
+
},
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Check if new level is allowed in the category section depth
|
|
918
|
+
*/
|
|
919
|
+
if (level > (section.depth ?? 1)) {
|
|
920
|
+
/**
|
|
921
|
+
* This is the last level, return an empty array
|
|
922
|
+
*/
|
|
923
|
+
return {
|
|
924
|
+
options: null,
|
|
925
|
+
parentId: id,
|
|
926
|
+
level: level,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Let's get the options for the select input
|
|
931
|
+
*/
|
|
932
|
+
const selectStatement = sql `select * from \`${sql.raw(section.db.table)}\` WHERE parent_id = ${id} AND level = ${level} ORDER BY \`${sql.raw(section.db.orderByField?.name ? section.db.orderByField?.name : section.db.identifier.name)}\` DESC`;
|
|
933
|
+
/**
|
|
934
|
+
* Get the options from the table
|
|
935
|
+
*/
|
|
936
|
+
const selectOptionsRows = await db.execute(selectStatement);
|
|
937
|
+
const rows = selectOptionsRows[0];
|
|
938
|
+
return {
|
|
939
|
+
options: rows.map((row) => {
|
|
940
|
+
return {
|
|
941
|
+
value: row[section.db.identifier.name],
|
|
942
|
+
label: row[section.headingField.name],
|
|
943
|
+
};
|
|
944
|
+
}),
|
|
945
|
+
parentId: id,
|
|
946
|
+
level: level++,
|
|
947
|
+
};
|
|
948
|
+
};
|
|
949
|
+
export const getCategorySection = async (session, sectionName) => {
|
|
950
|
+
// Let's fetch the section items and admin privileges for the section
|
|
951
|
+
const section = (await SectionFactory.getSectionForAdmin({
|
|
952
|
+
name: sectionName,
|
|
953
|
+
admin: {
|
|
954
|
+
id: session.user.id,
|
|
955
|
+
},
|
|
956
|
+
}));
|
|
957
|
+
if (!section) {
|
|
958
|
+
return {
|
|
959
|
+
error: {
|
|
960
|
+
message: getString('sectionNotFound', session.user.language),
|
|
961
|
+
},
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
const tableName = section.db.table;
|
|
965
|
+
/**
|
|
966
|
+
* Create a select field config for the category section
|
|
967
|
+
*/
|
|
968
|
+
const s = new SelectField({
|
|
969
|
+
name: section.name,
|
|
970
|
+
label: section.title.section,
|
|
971
|
+
required: false,
|
|
972
|
+
order: 1,
|
|
973
|
+
section: section,
|
|
974
|
+
destinationDb: undefined,
|
|
975
|
+
});
|
|
976
|
+
/**
|
|
977
|
+
* Build the select field to get the options
|
|
978
|
+
* from database and do all the necessary checks
|
|
979
|
+
*/
|
|
980
|
+
await s.build();
|
|
981
|
+
/**
|
|
982
|
+
* Set the value to undefined
|
|
983
|
+
*/
|
|
984
|
+
s.setValue(undefined);
|
|
985
|
+
// Get the locale for resolving localized titles (label must be a string for React)
|
|
986
|
+
const cmsConfig = await getCMSConfig();
|
|
987
|
+
const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
|
|
988
|
+
const resolvedTitle = {
|
|
989
|
+
section: resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage),
|
|
990
|
+
singular: resolveMultilingualString(section.title.singular, language, cmsConfig.i18n.fallbackLanguage),
|
|
991
|
+
plural: resolveMultilingualString(section.title.plural, language, cmsConfig.i18n.fallbackLanguage),
|
|
992
|
+
};
|
|
993
|
+
const categorySectionSelect = {
|
|
994
|
+
options: s.options,
|
|
995
|
+
required: s.required,
|
|
996
|
+
name: s.name,
|
|
997
|
+
label: resolvedTitle.section,
|
|
998
|
+
value: s.value,
|
|
999
|
+
parentId: undefined,
|
|
1000
|
+
level: 1,
|
|
1001
|
+
depth: section.depth,
|
|
1002
|
+
sectionName: section.name,
|
|
1003
|
+
allowRecursiveDelete: section.allowRecursiveDelete,
|
|
1004
|
+
};
|
|
1005
|
+
return {
|
|
1006
|
+
section: {
|
|
1007
|
+
tableName: tableName,
|
|
1008
|
+
sectionName: section.name,
|
|
1009
|
+
title: resolvedTitle,
|
|
1010
|
+
},
|
|
1011
|
+
data: categorySectionSelect,
|
|
1012
|
+
// publisher: privileges.publisher,
|
|
1013
|
+
};
|
|
1014
|
+
};
|
|
1015
|
+
export const getBrowsePage = async (session, sectionName, page = 1, q) => {
|
|
1016
|
+
// Let's fetch the section items and admin privileges for the section
|
|
1017
|
+
const _s = (await SectionFactory.getSectionForAdmin({
|
|
1018
|
+
name: sectionName,
|
|
1019
|
+
admin: {
|
|
1020
|
+
id: session.user.id,
|
|
1021
|
+
},
|
|
1022
|
+
}));
|
|
1023
|
+
const section = _s?.build();
|
|
1024
|
+
if (!section) {
|
|
1025
|
+
return {
|
|
1026
|
+
error: {
|
|
1027
|
+
message: getString('sectionNotFound', session.user.language),
|
|
1028
|
+
},
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
const limit = 12;
|
|
1032
|
+
const offset = (page - 1) * limit;
|
|
1033
|
+
const tableName = section.db.table;
|
|
1034
|
+
const orderByFieldName = section.db.orderByField?.name || 'created_at';
|
|
1035
|
+
const sqlChunks = [];
|
|
1036
|
+
const totalRowsSqlChunks = [];
|
|
1037
|
+
const sectionSearch = section.search?.searchFields ? section.search?.searchFields.length > 0 : false;
|
|
1038
|
+
// Check if we need to JOIN _locales for search
|
|
1039
|
+
const cmsConfig = await getCMSConfig();
|
|
1040
|
+
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}\``)}`);
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
sqlChunks.push(sql `select * from ${sql.raw(`\`${tableName}\``)}`);
|
|
1050
|
+
totalRowsSqlChunks.push(sql `select count(*) as total from ${sql.raw(`\`${tableName}\``)}`);
|
|
1051
|
+
}
|
|
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
|
+
}
|
|
1062
|
+
}
|
|
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
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
sqlChunks.push(sql `ORDER BY ${sql.raw(`\`${tableName}\``)}.${sql.raw(`\`${orderByFieldName}\``)} DESC LIMIT ${limit} OFFSET ${offset}`);
|
|
1070
|
+
const finalSql = sql.join(sqlChunks, sql.raw(' '));
|
|
1071
|
+
const totalRowsSql = sql.join(totalRowsSqlChunks, sql.raw(' '));
|
|
1072
|
+
// Now, let's get the section items from the table
|
|
1073
|
+
const sectionItems = await db.execute(finalSql);
|
|
1074
|
+
const totalCountResult = await db.execute(totalRowsSql);
|
|
1075
|
+
const totalCountRows = totalCountResult[0];
|
|
1076
|
+
const rows = sectionItems[0];
|
|
1077
|
+
// Resolve localized section title for the browse page header
|
|
1078
|
+
const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
|
|
1079
|
+
const resolvedTitle = resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage);
|
|
1080
|
+
return {
|
|
1081
|
+
section: {
|
|
1082
|
+
// tableName: tableName,
|
|
1083
|
+
// headingField: section.headingField.name,
|
|
1084
|
+
// identifierField: section.db.identifier.name,
|
|
1085
|
+
// coverPhotoField: section.coverPhotoField?.name,
|
|
1086
|
+
hasSearch: sectionSearch,
|
|
1087
|
+
name: section.name,
|
|
1088
|
+
title: resolvedTitle,
|
|
1089
|
+
},
|
|
1090
|
+
items: rows.map((row) => {
|
|
1091
|
+
// Custom browse page implementation: if browse.fields is configured,
|
|
1092
|
+
// return the specified fields plus default fields (id, headingTitle, coverPhoto, createdAt, createdBy, permission).
|
|
1093
|
+
// NOTE: This is for custom browse page implementations only.
|
|
1094
|
+
// The default CMS browse page component uses the fallback fields below.
|
|
1095
|
+
if (section.browse?.fields && section.browse.fields.length > 0) {
|
|
1096
|
+
const item = {
|
|
1097
|
+
// Always include default fields
|
|
1098
|
+
id: row[section.db.identifier.name],
|
|
1099
|
+
headingTitle: row[section.headingField.name],
|
|
1100
|
+
coverPhoto: section.coverPhotoField ? row[section.coverPhotoField?.name] : null,
|
|
1101
|
+
createdAt: row['created_at'],
|
|
1102
|
+
createdBy: row['created_by'],
|
|
1103
|
+
permission: row.permission,
|
|
1104
|
+
};
|
|
1105
|
+
// Add configured browse fields
|
|
1106
|
+
for (const field of section.browse.fields) {
|
|
1107
|
+
item[field.name] = row[field.name] ?? null;
|
|
1108
|
+
}
|
|
1109
|
+
return item;
|
|
1110
|
+
}
|
|
1111
|
+
// Default browse page fields (used by the standard CMS browse page)
|
|
1112
|
+
return {
|
|
1113
|
+
id: row[section.db.identifier.name],
|
|
1114
|
+
headingTitle: row[section.headingField.name],
|
|
1115
|
+
coverPhoto: section.coverPhotoField ? row[section.coverPhotoField?.name] : null,
|
|
1116
|
+
createdAt: row['created_at'],
|
|
1117
|
+
createdBy: row['created_by'],
|
|
1118
|
+
permission: row.permission,
|
|
1119
|
+
};
|
|
1120
|
+
}),
|
|
1121
|
+
totalCount: totalCountRows[0].total,
|
|
1122
|
+
};
|
|
1123
|
+
};
|
|
1124
|
+
export const getSidebar = async (session) => {
|
|
1125
|
+
// Let's get simple, has_items and categorized sections from the sections table
|
|
1126
|
+
const { simple, has_items, category, fixed } = await SectionFactory.getSectionsForAdmin({
|
|
1127
|
+
admin: { id: session.user.id },
|
|
1128
|
+
});
|
|
1129
|
+
const pluginNav = await getPluginNavigation();
|
|
1130
|
+
const privilegeSet = await getAdminPrivileges(session.user.id);
|
|
1131
|
+
// Get config and check for dashboard override
|
|
1132
|
+
const cmsConfig = await getCMSConfig();
|
|
1133
|
+
const dashboardOverridePath = cmsConfig.dashboard?.override;
|
|
1134
|
+
// Get the locale for resolving localized titles
|
|
1135
|
+
const language = resolveLanguage(session.user.language, cmsConfig.i18n.supportedLanguages, cmsConfig.i18n.fallbackLanguage);
|
|
1136
|
+
// Let's loop through the sections and add path, icon and title to each section item
|
|
1137
|
+
const simpleSectionItems = simple.map((section) => {
|
|
1138
|
+
return {
|
|
1139
|
+
title: resolveMultilingualString(section.title, language, cmsConfig.i18n.fallbackLanguage),
|
|
1140
|
+
path: `/section/${section.name}`,
|
|
1141
|
+
icon: section.icon,
|
|
1142
|
+
};
|
|
1143
|
+
});
|
|
1144
|
+
const hasItemsSectionItems = has_items.map((section) => {
|
|
1145
|
+
return {
|
|
1146
|
+
title: resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage),
|
|
1147
|
+
path: `/${section.name}`,
|
|
1148
|
+
icon: section.icon,
|
|
1149
|
+
};
|
|
1150
|
+
});
|
|
1151
|
+
const categorySectionsItems = category.map((section) => {
|
|
1152
|
+
return {
|
|
1153
|
+
title: resolveMultilingualString(section.title.section, language, cmsConfig.i18n.fallbackLanguage),
|
|
1154
|
+
path: `/categorized/${section.name}`,
|
|
1155
|
+
icon: section.icon,
|
|
1156
|
+
};
|
|
1157
|
+
});
|
|
1158
|
+
const pluginSections = [];
|
|
1159
|
+
for (const item of pluginNav) {
|
|
1160
|
+
const allowed = privilegeSet.has(item.pluginName);
|
|
1161
|
+
if (item.kind === 'leaf') {
|
|
1162
|
+
// Allowed if privileged, OR this leaf is the dashboard override (becomes /dashboard).
|
|
1163
|
+
if (item.path === dashboardOverridePath)
|
|
1164
|
+
continue;
|
|
1165
|
+
if (!allowed)
|
|
1166
|
+
continue;
|
|
1167
|
+
pluginSections.push({
|
|
1168
|
+
kind: 'leaf',
|
|
1169
|
+
title: resolveMultilingualString(item.title, language, cmsConfig.i18n.fallbackLanguage),
|
|
1170
|
+
path: item.path,
|
|
1171
|
+
icon: item.icon ?? '',
|
|
1172
|
+
});
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
if (!allowed)
|
|
1176
|
+
continue;
|
|
1177
|
+
const children = item.children
|
|
1178
|
+
.filter((c) => c.path !== dashboardOverridePath)
|
|
1179
|
+
.map((c) => ({
|
|
1180
|
+
title: resolveMultilingualString(c.title, language, cmsConfig.i18n.fallbackLanguage),
|
|
1181
|
+
path: c.path,
|
|
1182
|
+
icon: c.icon ?? '',
|
|
1183
|
+
}));
|
|
1184
|
+
if (children.length === 0)
|
|
1185
|
+
continue;
|
|
1186
|
+
pluginSections.push({
|
|
1187
|
+
kind: 'group',
|
|
1188
|
+
title: resolveMultilingualString(item.title, language, cmsConfig.i18n.fallbackLanguage),
|
|
1189
|
+
icon: item.icon ?? '',
|
|
1190
|
+
children,
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
const fixedSections = [
|
|
1194
|
+
/**
|
|
1195
|
+
* Add the dashboard section (points to override if configured)
|
|
1196
|
+
*/
|
|
1197
|
+
{
|
|
1198
|
+
title: getString('dashboard', session.user.language),
|
|
1199
|
+
path: dashboardOverridePath ?? '/dashboard',
|
|
1200
|
+
icon: 'home',
|
|
1201
|
+
},
|
|
1202
|
+
...fixed.map((section) => ({
|
|
1203
|
+
title: getString(section.name, session.user.language),
|
|
1204
|
+
path: `/${section.name}`,
|
|
1205
|
+
icon: section.icon,
|
|
1206
|
+
})),
|
|
1207
|
+
];
|
|
1208
|
+
return {
|
|
1209
|
+
fixed_sections: fixedSections,
|
|
1210
|
+
plugin_sections: pluginSections,
|
|
1211
|
+
cat_sections: categorySectionsItems,
|
|
1212
|
+
has_items_sections: hasItemsSectionItems,
|
|
1213
|
+
simple_sections: simpleSectionItems,
|
|
1214
|
+
};
|
|
1215
|
+
};
|