nextjs-cms 0.5.9 → 0.5.10
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/api/axios/axiosInstance.d.ts +1 -1
- package/dist/api/axios/axiosInstance.js +8 -8
- package/dist/api/index.d.ts +855 -855
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +12 -12
- package/dist/api/lib/serverActions.d.ts +239 -239
- package/dist/api/lib/serverActions.d.ts.map +1 -1
- package/dist/api/lib/serverActions.js +834 -834
- package/dist/api/root.d.ts +828 -828
- package/dist/api/root.js +30 -30
- package/dist/api/routers/accountSettings.d.ts +60 -60
- package/dist/api/routers/accountSettings.js +108 -108
- package/dist/api/routers/admins.d.ts +105 -105
- package/dist/api/routers/admins.js +219 -219
- package/dist/api/routers/auth.d.ts +47 -47
- package/dist/api/routers/auth.js +25 -25
- package/dist/api/routers/categorySection.d.ts +103 -103
- package/dist/api/routers/categorySection.js +38 -38
- package/dist/api/routers/cmsSettings.d.ts +48 -48
- package/dist/api/routers/cmsSettings.js +51 -51
- package/dist/api/routers/cpanel.d.ts +83 -83
- package/dist/api/routers/cpanel.js +216 -216
- package/dist/api/routers/files.d.ts +47 -47
- package/dist/api/routers/files.js +23 -23
- package/dist/api/routers/gallery.d.ts +35 -35
- package/dist/api/routers/gallery.js +62 -62
- package/dist/api/routers/googleAnalytics.d.ts +30 -30
- package/dist/api/routers/googleAnalytics.js +7 -7
- package/dist/api/routers/hasItemsSection.d.ts +139 -139
- package/dist/api/routers/hasItemsSection.js +34 -34
- package/dist/api/routers/navigation.d.ts +51 -51
- package/dist/api/routers/navigation.js +11 -11
- package/dist/api/routers/simpleSection.d.ts +57 -57
- package/dist/api/routers/simpleSection.js +12 -12
- package/dist/api/trpc.d.ts +106 -106
- package/dist/api/trpc.js +72 -72
- package/dist/auth/axios/axiosInstance.d.ts +1 -1
- package/dist/auth/axios/axiosInstance.js +8 -8
- package/dist/auth/csrf.d.ts +29 -29
- package/dist/auth/csrf.js +76 -76
- package/dist/auth/hooks/index.d.ts +3 -3
- package/dist/auth/hooks/index.d.ts.map +1 -1
- package/dist/auth/hooks/index.js +3 -3
- package/dist/auth/hooks/useAxiosPrivate.d.ts +4 -4
- package/dist/auth/hooks/useAxiosPrivate.js +74 -74
- package/dist/auth/hooks/useRefreshToken.d.ts +6 -6
- package/dist/auth/hooks/useRefreshToken.js +79 -79
- package/dist/auth/index.d.ts +22 -22
- package/dist/auth/index.js +44 -44
- package/dist/auth/jwt.d.ts +5 -5
- package/dist/auth/jwt.js +25 -25
- package/dist/auth/lib/actions.d.ts +32 -32
- package/dist/auth/lib/actions.d.ts.map +1 -1
- package/dist/auth/lib/actions.js +209 -209
- package/dist/auth/lib/client.d.ts +3 -3
- package/dist/auth/lib/client.js +46 -46
- package/dist/auth/lib/index.d.ts +2 -2
- package/dist/auth/lib/index.d.ts.map +1 -1
- package/dist/auth/lib/index.js +2 -2
- package/dist/auth/react.d.ts +105 -105
- package/dist/auth/react.d.ts.map +1 -1
- package/dist/auth/react.js +347 -347
- package/dist/auth/trpc.d.ts +5 -5
- package/dist/auth/trpc.d.ts.map +1 -1
- package/dist/auth/trpc.js +81 -81
- package/dist/core/config/config-loader.d.ts +91 -91
- package/dist/core/config/config-loader.js +230 -230
- package/dist/core/config/index.d.ts +2 -2
- package/dist/core/config/index.d.ts.map +1 -1
- package/dist/core/config/index.js +1 -1
- package/dist/core/config/loader.d.ts +1 -1
- package/dist/core/config/loader.js +42 -42
- package/dist/core/db/index.d.ts +1 -1
- package/dist/core/db/index.d.ts.map +1 -1
- package/dist/core/db/index.js +1 -1
- package/dist/core/db/table-checker/DbTable.d.ts +5 -5
- package/dist/core/db/table-checker/DbTable.js +5 -5
- package/dist/core/db/table-checker/MysqlTable.d.ts +33 -33
- package/dist/core/db/table-checker/MysqlTable.d.ts.map +1 -1
- package/dist/core/db/table-checker/MysqlTable.js +94 -94
- package/dist/core/db/table-checker/index.d.ts +1 -1
- package/dist/core/db/table-checker/index.d.ts.map +1 -1
- package/dist/core/db/table-checker/index.js +1 -1
- package/dist/core/factories/FieldFactory.d.ts +123 -123
- package/dist/core/factories/FieldFactory.d.ts.map +1 -1
- package/dist/core/factories/FieldFactory.js +411 -411
- package/dist/core/factories/SectionFactory.d.ts +109 -109
- package/dist/core/factories/SectionFactory.d.ts.map +1 -1
- package/dist/core/factories/SectionFactory.js +415 -415
- package/dist/core/factories/index.d.ts +2 -2
- package/dist/core/factories/index.d.ts.map +1 -1
- package/dist/core/factories/index.js +2 -2
- package/dist/core/fields/checkbox.d.ts +62 -62
- package/dist/core/fields/checkbox.d.ts.map +1 -1
- package/dist/core/fields/checkbox.js +62 -62
- package/dist/core/fields/color.d.ts +83 -83
- package/dist/core/fields/color.d.ts.map +1 -1
- package/dist/core/fields/color.js +91 -91
- package/dist/core/fields/date.d.ts +99 -99
- package/dist/core/fields/date.d.ts.map +1 -1
- package/dist/core/fields/date.js +108 -108
- package/dist/core/fields/document.d.ts +179 -179
- package/dist/core/fields/document.d.ts.map +1 -1
- package/dist/core/fields/document.js +277 -277
- package/dist/core/fields/field-group.d.ts +17 -17
- package/dist/core/fields/field-group.d.ts.map +1 -1
- package/dist/core/fields/field-group.js +6 -6
- package/dist/core/fields/field.d.ts +125 -125
- package/dist/core/fields/field.d.ts.map +1 -1
- package/dist/core/fields/field.js +148 -148
- package/dist/core/fields/fileField.d.ts +14 -14
- package/dist/core/fields/fileField.d.ts.map +1 -1
- package/dist/core/fields/fileField.js +5 -5
- package/dist/core/fields/index.d.ts +64 -64
- package/dist/core/fields/index.d.ts.map +1 -1
- package/dist/core/fields/index.js +18 -18
- package/dist/core/fields/map.d.ts +166 -166
- package/dist/core/fields/map.d.ts.map +1 -1
- package/dist/core/fields/map.js +152 -152
- package/dist/core/fields/number.d.ts +185 -185
- package/dist/core/fields/number.d.ts.map +1 -1
- package/dist/core/fields/number.js +241 -241
- package/dist/core/fields/password.d.ts +108 -108
- package/dist/core/fields/password.d.ts.map +1 -1
- package/dist/core/fields/password.js +133 -133
- package/dist/core/fields/photo.d.ts +288 -288
- package/dist/core/fields/photo.d.ts.map +1 -1
- package/dist/core/fields/photo.js +410 -410
- package/dist/core/fields/richText.d.ts +294 -294
- package/dist/core/fields/richText.d.ts.map +1 -1
- package/dist/core/fields/richText.js +338 -338
- package/dist/core/fields/select.d.ts +365 -365
- package/dist/core/fields/select.d.ts.map +1 -1
- package/dist/core/fields/select.js +499 -499
- package/dist/core/fields/selectMultiple.d.ts +235 -235
- package/dist/core/fields/selectMultiple.d.ts.map +1 -1
- package/dist/core/fields/selectMultiple.js +417 -417
- package/dist/core/fields/tags.d.ts +130 -130
- package/dist/core/fields/tags.d.ts.map +1 -1
- package/dist/core/fields/tags.js +105 -105
- package/dist/core/fields/text.d.ts +135 -135
- package/dist/core/fields/text.d.ts.map +1 -1
- package/dist/core/fields/text.js +157 -157
- package/dist/core/fields/textArea.d.ts +106 -106
- package/dist/core/fields/textArea.d.ts.map +1 -1
- package/dist/core/fields/textArea.js +126 -126
- package/dist/core/fields/video.d.ts +147 -147
- package/dist/core/fields/video.d.ts.map +1 -1
- package/dist/core/fields/video.js +248 -248
- package/dist/core/helpers/entity.d.ts +7 -7
- package/dist/core/helpers/entity.js +27 -27
- package/dist/core/helpers/index.d.ts +4 -4
- package/dist/core/helpers/index.d.ts.map +1 -1
- package/dist/core/helpers/index.js +3 -3
- package/dist/core/index.d.ts +7 -7
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +7 -7
- package/dist/core/sections/category.d.ts +282 -282
- package/dist/core/sections/category.d.ts.map +1 -1
- package/dist/core/sections/category.js +147 -147
- package/dist/core/sections/hasItems.d.ts +631 -631
- package/dist/core/sections/hasItems.d.ts.map +1 -1
- package/dist/core/sections/hasItems.js +144 -144
- package/dist/core/sections/index.d.ts +4 -4
- package/dist/core/sections/index.d.ts.map +1 -1
- package/dist/core/sections/index.js +4 -4
- package/dist/core/sections/section.d.ts +225 -225
- package/dist/core/sections/section.d.ts.map +1 -1
- package/dist/core/sections/section.js +341 -341
- package/dist/core/sections/simple.d.ts +98 -98
- package/dist/core/sections/simple.d.ts.map +1 -1
- package/dist/core/sections/simple.js +95 -95
- package/dist/core/security/dom.d.ts +10 -10
- package/dist/core/security/dom.js +92 -92
- package/dist/core/submit/ItemEditSubmit.d.ts +75 -75
- package/dist/core/submit/ItemEditSubmit.js +186 -186
- package/dist/core/submit/NewItemSubmit.d.ts +13 -13
- package/dist/core/submit/NewItemSubmit.js +93 -93
- package/dist/core/submit/SimpleSectionSubmit.d.ts +12 -12
- package/dist/core/submit/SimpleSectionSubmit.js +93 -93
- package/dist/core/submit/index.d.ts +4 -4
- package/dist/core/submit/index.js +4 -4
- package/dist/core/submit/submit.d.ts +115 -115
- package/dist/core/submit/submit.js +479 -479
- package/dist/core/types/index.d.ts +279 -279
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/core/types/index.js +1 -1
- package/dist/db/client.d.ts +8 -8
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +19 -19
- package/dist/db/config.d.ts +5 -5
- package/dist/db/config.js +22 -22
- package/dist/db/drizzle.config.d.ts +5 -5
- package/dist/db/drizzle.config.js +18 -18
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +3 -3
- package/dist/db/schema.d.ts +638 -638
- package/dist/db/schema.js +73 -73
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -7
- package/dist/translations/index.d.ts +2 -2
- package/dist/translations/index.js +15 -15
- package/dist/utils/CpanelApi.d.ts +24 -24
- package/dist/utils/CpanelApi.js +64 -64
- package/dist/utils/constants.d.ts +13 -13
- package/dist/utils/constants.js +61 -61
- package/dist/utils/index.d.ts +4 -4
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +4 -4
- package/dist/utils/utils.d.ts +59 -59
- package/dist/utils/utils.js +132 -132
- package/dist/validators/checkbox.d.ts +3 -3
- package/dist/validators/checkbox.d.ts.map +1 -1
- package/dist/validators/checkbox.js +12 -12
- package/dist/validators/color.d.ts +3 -3
- package/dist/validators/color.d.ts.map +1 -1
- package/dist/validators/color.js +7 -7
- package/dist/validators/date.d.ts +3 -3
- package/dist/validators/date.d.ts.map +1 -1
- package/dist/validators/date.js +5 -5
- package/dist/validators/document.d.ts +3 -3
- package/dist/validators/document.d.ts.map +1 -1
- package/dist/validators/document.js +57 -57
- package/dist/validators/index.d.ts +14 -14
- package/dist/validators/index.d.ts.map +1 -1
- package/dist/validators/index.js +14 -14
- package/dist/validators/map.d.ts +3 -3
- package/dist/validators/map.d.ts.map +1 -1
- package/dist/validators/map.js +5 -5
- package/dist/validators/number.d.ts +3 -3
- package/dist/validators/number.d.ts.map +1 -1
- package/dist/validators/number.js +20 -20
- package/dist/validators/password.d.ts +3 -3
- package/dist/validators/password.d.ts.map +1 -1
- package/dist/validators/password.js +11 -11
- package/dist/validators/photo.d.ts +3 -3
- package/dist/validators/photo.d.ts.map +1 -1
- package/dist/validators/photo.js +100 -100
- package/dist/validators/richText.d.ts +3 -3
- package/dist/validators/richText.d.ts.map +1 -1
- package/dist/validators/richText.js +8 -8
- package/dist/validators/select-multiple.d.ts +9 -9
- package/dist/validators/select-multiple.d.ts.map +1 -1
- package/dist/validators/select-multiple.js +20 -20
- package/dist/validators/select.d.ts +3 -3
- package/dist/validators/select.d.ts.map +1 -1
- package/dist/validators/select.js +5 -5
- package/dist/validators/text.d.ts +3 -3
- package/dist/validators/text.d.ts.map +1 -1
- package/dist/validators/text.js +7 -7
- package/dist/validators/textarea.d.ts +3 -3
- package/dist/validators/textarea.d.ts.map +1 -1
- package/dist/validators/textarea.js +7 -7
- package/dist/validators/video.d.ts +3 -3
- package/dist/validators/video.d.ts.map +1 -1
- package/dist/validators/video.js +57 -57
- package/package.json +2 -3
|
@@ -1,411 +1,411 @@
|
|
|
1
|
-
import { sql } from 'drizzle-orm';
|
|
2
|
-
import { db } from
|
|
3
|
-
import { SectionFactory } from
|
|
4
|
-
import { SimpleSection, HasItemsSection, CategorySection } from
|
|
5
|
-
import { checkboxField, SelectField, SelectMultipleField } from
|
|
6
|
-
import { is } from
|
|
7
|
-
import { cloneDeep } from 'lodash-es';
|
|
8
|
-
import chalk from 'chalk';
|
|
9
|
-
export class FieldFactory {
|
|
10
|
-
session;
|
|
11
|
-
_values = {};
|
|
12
|
-
itemId = undefined;
|
|
13
|
-
_error = false;
|
|
14
|
-
_errorMessage = '';
|
|
15
|
-
type;
|
|
16
|
-
sectionName;
|
|
17
|
-
_sectionInfo;
|
|
18
|
-
// private _fields: Field[]
|
|
19
|
-
/**
|
|
20
|
-
* Default order value for items without explicit order.
|
|
21
|
-
* Ensures items without order come after explicitly ordered items.
|
|
22
|
-
* Typical order values: 0-15, with occasional values up to 1000.
|
|
23
|
-
*/
|
|
24
|
-
static DEFAULT_ORDER = 1_000_000;
|
|
25
|
-
/**
|
|
26
|
-
* Constructor
|
|
27
|
-
*/
|
|
28
|
-
constructor({ type, sectionName, session, itemId }) {
|
|
29
|
-
this.type = type;
|
|
30
|
-
this.sectionName = sectionName;
|
|
31
|
-
this.session = session;
|
|
32
|
-
this.itemId = itemId;
|
|
33
|
-
// this._fields = new Array<Field>()
|
|
34
|
-
this._sectionInfo = undefined;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Must be called after the constructor
|
|
38
|
-
*/
|
|
39
|
-
async initialize() {
|
|
40
|
-
await this.initializeSectionInfo(this.session);
|
|
41
|
-
/**
|
|
42
|
-
* If it's an edit operation, let's initialize the field values
|
|
43
|
-
*/
|
|
44
|
-
if (this.type === 'edit') {
|
|
45
|
-
await this.initializeFieldValues();
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
async getFieldValue(field) {
|
|
49
|
-
if (this.type === 'new')
|
|
50
|
-
return undefined;
|
|
51
|
-
try {
|
|
52
|
-
return this._values[field.name];
|
|
53
|
-
}
|
|
54
|
-
catch (e) {
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
async initializeFieldValues() {
|
|
59
|
-
if (this._error)
|
|
60
|
-
return;
|
|
61
|
-
if (this._sectionInfo?.db.table && this._sectionInfo.db.identifier) {
|
|
62
|
-
/**
|
|
63
|
-
* Let's get the section item values from its table and assign it to the `_values` property
|
|
64
|
-
*/
|
|
65
|
-
// TODO: We still need to get the values from the `destinationDbTable` if it exists on any field
|
|
66
|
-
// also, select_multiple fields need to be handled the same way, plus value is array of strings
|
|
67
|
-
const [_v, fields] = await db.execute(sql `select * from ${sql.raw(this._sectionInfo.db.table)} where ${sql.raw(this._sectionInfo.db.identifier.name)} = ${this.itemId} limit 1`);
|
|
68
|
-
// @ts-ignore
|
|
69
|
-
// Bug: this is a bug in drizzle-orm/mysql2
|
|
70
|
-
this._values = _v[0];
|
|
71
|
-
/**
|
|
72
|
-
* If the field has a destinationDb
|
|
73
|
-
* Get the result from the field's table and the destinationDb's table using a JOIN query
|
|
74
|
-
* Fields that can have a destinationDb are: SelectField, SelectMultipleField, TagsField
|
|
75
|
-
*/
|
|
76
|
-
for (const field of this._sectionInfo.fields) {
|
|
77
|
-
if (field.destinationDb && (is(field, SelectMultipleField) || is(field, SelectField))) {
|
|
78
|
-
const sqlChunks = [
|
|
79
|
-
sql `select * from ${sql.raw(field.destinationDb.table)} a JOIN ${sql.raw(field.db.table)} b ON a.${sql.raw(field.destinationDb.selectIdentifier)} = b.${sql.raw(field.db.identifier)} where ${sql.raw(field.destinationDb.itemIdentifier)} = ${this.itemId}`,
|
|
80
|
-
];
|
|
81
|
-
if (is(field, SelectField) && field.hasDepth()) {
|
|
82
|
-
/**
|
|
83
|
-
* Add the where clause to the select statement
|
|
84
|
-
*/
|
|
85
|
-
sqlChunks.push(sql ` ORDER BY b.level ASC`);
|
|
86
|
-
}
|
|
87
|
-
const [_rows, _fields] = await db.execute(sql.join(sqlChunks));
|
|
88
|
-
const values = [];
|
|
89
|
-
if (Array.isArray(_rows)) {
|
|
90
|
-
for (const row of _rows) {
|
|
91
|
-
values.push({
|
|
92
|
-
value: row[field.destinationDb.selectIdentifier],
|
|
93
|
-
label: row[field.db.label],
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
this._values[field.name] = values;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* If the section item is not found, set the `_error` property to true
|
|
102
|
-
*/
|
|
103
|
-
if (!is(this._sectionInfo, SimpleSection) && !this._values) {
|
|
104
|
-
this._error = true;
|
|
105
|
-
this._errorMessage = 'Item not found';
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
this._error = true;
|
|
111
|
-
this._errorMessage = 'Section table or section table identifier is missing';
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Gets the section info and assigns it to the `sectionInfo` property
|
|
117
|
-
* @param session
|
|
118
|
-
* @private
|
|
119
|
-
*/
|
|
120
|
-
async initializeSectionInfo(session) {
|
|
121
|
-
/**
|
|
122
|
-
* Get section info - could be a config (with build()) or an instance
|
|
123
|
-
* Section files should export configs, but we handle both for backward compatibility
|
|
124
|
-
*/
|
|
125
|
-
let _s = await this.getSectionInfo(session);
|
|
126
|
-
if (_s === undefined)
|
|
127
|
-
return;
|
|
128
|
-
/**
|
|
129
|
-
* Build the section from config when we need to use it
|
|
130
|
-
* SectionFactory returns configs, so we always build them here
|
|
131
|
-
*
|
|
132
|
-
* Double check if the config has a build() method
|
|
133
|
-
*/
|
|
134
|
-
if (typeof _s.build === 'function') {
|
|
135
|
-
// It's a config - build it to get a fresh instance
|
|
136
|
-
// We build here because we immediately need to access section properties (db.table, inputs, etc.)
|
|
137
|
-
this._sectionInfo = _s.build();
|
|
138
|
-
/**
|
|
139
|
-
* Build the fields from the field configs
|
|
140
|
-
*/
|
|
141
|
-
this._sectionInfo.buildFields();
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
const errorMessage = 'Section config must have a build() method. Use helper functions like simpleSection(), hasItemsSection(), categorySection() to create section configs.';
|
|
145
|
-
console.error(chalk.bold.red('[FieldFactory Error]', errorMessage));
|
|
146
|
-
this._error = true;
|
|
147
|
-
this._errorMessage = errorMessage;
|
|
148
|
-
throw new Error(errorMessage);
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Unset the `_s` variable to be garbage collected
|
|
152
|
-
*/
|
|
153
|
-
_s = undefined;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Get the section info from the `sectionsTable` table
|
|
157
|
-
* @param session
|
|
158
|
-
* @private
|
|
159
|
-
*/
|
|
160
|
-
async getSectionInfo(session) {
|
|
161
|
-
/**
|
|
162
|
-
* First, let's get the section details, and then get the section input fields
|
|
163
|
-
*/
|
|
164
|
-
const s = await SectionFactory.getSectionForAdmin({
|
|
165
|
-
name: this.sectionName,
|
|
166
|
-
admin: {
|
|
167
|
-
id: session.user.id,
|
|
168
|
-
requiredRole: this.type === 'new' ? 'C' : 'U',
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
if (!s || !s.name) {
|
|
172
|
-
this._error = true;
|
|
173
|
-
this._errorMessage = 'Section not found or you do not have access to this operation';
|
|
174
|
-
return undefined;
|
|
175
|
-
}
|
|
176
|
-
return s;
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Prepare a field for use
|
|
180
|
-
* Fields are already instances (resolved in Section constructor), so we just need to set values
|
|
181
|
-
* @param field
|
|
182
|
-
*/
|
|
183
|
-
async prepareField(field) {
|
|
184
|
-
// Fields are already instances - no need to build them
|
|
185
|
-
// They were resolved from configs in the Section constructor
|
|
186
|
-
if (this.type === 'edit') {
|
|
187
|
-
/**
|
|
188
|
-
* If it's an edit operation, get the value from the database and set it
|
|
189
|
-
*/
|
|
190
|
-
const value = await this.getFieldValue(field);
|
|
191
|
-
// It's important to set the value to whatever is in the database,
|
|
192
|
-
// even if the field has a `defaultValue` set.
|
|
193
|
-
field.setValue(value);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
// TODO: Implement this, maybe set the roles in the trpc context?
|
|
197
|
-
addPermissionField() {
|
|
198
|
-
if (is(this._sectionInfo, HasItemsSection)) {
|
|
199
|
-
if (this._sectionInfo.requirePublishPermission && this.session.user) {
|
|
200
|
-
return checkboxField({
|
|
201
|
-
name: 'publish',
|
|
202
|
-
label: 'Publish',
|
|
203
|
-
required: true,
|
|
204
|
-
order: 1000,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Generate the fields
|
|
211
|
-
*/
|
|
212
|
-
async generateFields() {
|
|
213
|
-
if (this._error)
|
|
214
|
-
return;
|
|
215
|
-
if (!this._sectionInfo)
|
|
216
|
-
return;
|
|
217
|
-
for (const input of this._sectionInfo.fields) {
|
|
218
|
-
/**
|
|
219
|
-
* If it's an edit operation, set the field to readonly if it's the identifier field,
|
|
220
|
-
* identifier fields must not be edited!
|
|
221
|
-
* They are always visible as a readonly text.
|
|
222
|
-
*/
|
|
223
|
-
if (this.type === 'edit' && input.name === this._sectionInfo.db.identifier.name) {
|
|
224
|
-
input.adminGenerated = 'readonly';
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Prepare the field (set values if editing)
|
|
228
|
-
*/
|
|
229
|
-
await this.prepareField(input);
|
|
230
|
-
}
|
|
231
|
-
this.sortFields();
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Sort the fields.
|
|
235
|
-
* This function sorts the conditional fields into their respective fields.
|
|
236
|
-
*/
|
|
237
|
-
sortFields() {
|
|
238
|
-
if (this._error)
|
|
239
|
-
return;
|
|
240
|
-
if (!this._sectionInfo)
|
|
241
|
-
return;
|
|
242
|
-
/**
|
|
243
|
-
* Create a map of conditional fields
|
|
244
|
-
*/
|
|
245
|
-
const _conditionalFieldMap = new Map();
|
|
246
|
-
/**
|
|
247
|
-
* Loop through the fields and get the conditional fields
|
|
248
|
-
*/
|
|
249
|
-
for (const input of this._sectionInfo.fields) {
|
|
250
|
-
input.conditionalFields = [];
|
|
251
|
-
/**
|
|
252
|
-
* Check if the field is conditional.
|
|
253
|
-
*/
|
|
254
|
-
if (input.isConditional()) {
|
|
255
|
-
for (const rule of input.conditionalRules ?? []) {
|
|
256
|
-
/**
|
|
257
|
-
* Add the conditional input to the conditional fields lists
|
|
258
|
-
*/
|
|
259
|
-
if (!_conditionalFieldMap.has(rule.field.name)) {
|
|
260
|
-
_conditionalFieldMap.set(rule.field.name, []);
|
|
261
|
-
}
|
|
262
|
-
_conditionalFieldMap.get(rule.field.name).push(
|
|
263
|
-
/**
|
|
264
|
-
* Clone the conditional field to avoid reference issues
|
|
265
|
-
*/
|
|
266
|
-
cloneDeep({
|
|
267
|
-
field: input.exportForClient(),
|
|
268
|
-
rule: {
|
|
269
|
-
condition: rule.condition,
|
|
270
|
-
value: rule.value,
|
|
271
|
-
},
|
|
272
|
-
}));
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Assign conditionalFields to the appropriate fields
|
|
278
|
-
*/
|
|
279
|
-
for (const field of this._sectionInfo.fields) {
|
|
280
|
-
const matchingConditionalFields = _conditionalFieldMap.get(field.name);
|
|
281
|
-
if (matchingConditionalFields) {
|
|
282
|
-
field.conditionalFields.push(...matchingConditionalFields.map((clonedConditionalField) => ({
|
|
283
|
-
...clonedConditionalField,
|
|
284
|
-
rule: {
|
|
285
|
-
condition: clonedConditionalField.rule.condition,
|
|
286
|
-
value: clonedConditionalField.rule.value,
|
|
287
|
-
},
|
|
288
|
-
})));
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Destroy the map
|
|
293
|
-
*/
|
|
294
|
-
_conditionalFieldMap.clear();
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Sort fields by their order property, preserving original position for fields without order.
|
|
298
|
-
*
|
|
299
|
-
* Sorting behavior:
|
|
300
|
-
* 1. Fields with explicit `order` values are sorted by that value (ascending)
|
|
301
|
-
* 2. Fields without `order` maintain their original array position (stable sort)
|
|
302
|
-
* 3. Fields with the same `order` value maintain their relative positions (stable sort)
|
|
303
|
-
*
|
|
304
|
-
* This leverages JavaScript's stable Array.sort() (ES2019+) for optimal performance.
|
|
305
|
-
* Note: This mutates the input array for performance.
|
|
306
|
-
*
|
|
307
|
-
* @param fields - Array of fields to sort (will be mutated)
|
|
308
|
-
* @returns The sorted array (same reference as input)
|
|
309
|
-
* @private
|
|
310
|
-
*/
|
|
311
|
-
sortFieldsByOrder(fields) {
|
|
312
|
-
// Sort in-place for performance (mutates array)
|
|
313
|
-
// JavaScript's sort is stable (ES2019+), so equal values maintain original order
|
|
314
|
-
return fields.sort((a, b) => (a.order ?? FieldFactory.DEFAULT_ORDER) - (b.order ?? FieldFactory.DEFAULT_ORDER));
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Sort groups by their groupOrder property, preserving original position for groups without order.
|
|
318
|
-
*
|
|
319
|
-
* Sorting behavior:
|
|
320
|
-
* 1. Groups with explicit `groupOrder` values are sorted by that value (ascending)
|
|
321
|
-
* 2. Groups without `groupOrder` maintain their original array position (stable sort)
|
|
322
|
-
* 3. Groups with the same `groupOrder` value maintain their relative positions (stable sort)
|
|
323
|
-
*
|
|
324
|
-
* This leverages JavaScript's stable Array.sort() (ES2019+) for optimal performance.
|
|
325
|
-
* Note: This mutates the input array for performance.
|
|
326
|
-
* Note: groupOrder is normalized at creation time, so no nullish coalescing needed here.
|
|
327
|
-
*
|
|
328
|
-
* @param groups - Array of groups to sort (will be mutated)
|
|
329
|
-
* @returns The sorted array (same reference as input)
|
|
330
|
-
* @private
|
|
331
|
-
*/
|
|
332
|
-
sortGroupsByOrder(groups) {
|
|
333
|
-
// Sort in-place for performance (mutates array)
|
|
334
|
-
// JavaScript's sort is stable (ES2019+), so equal values maintain original order
|
|
335
|
-
// groupOrder is normalized at creation time, so direct subtraction is safe
|
|
336
|
-
return groups.sort((a, b) => a.groupOrder - b.groupOrder);
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Get field groups with filtered and sorted fields.
|
|
340
|
-
* Leverages the pre-built fieldGroups from the Section class for better performance.
|
|
341
|
-
*/
|
|
342
|
-
getGroupedFields() {
|
|
343
|
-
if (this._error)
|
|
344
|
-
return;
|
|
345
|
-
if (!this._sectionInfo)
|
|
346
|
-
return;
|
|
347
|
-
const fieldGroups = this._sectionInfo.fieldGroups;
|
|
348
|
-
const isNewOperation = this.type === 'new';
|
|
349
|
-
const isEditOperation = this.type === 'edit';
|
|
350
|
-
const isCategorySection = is(this._sectionInfo, CategorySection);
|
|
351
|
-
// Build groups in a single pass, skipping empty groups
|
|
352
|
-
const processedGroups = [];
|
|
353
|
-
for (let i = 0, len = fieldGroups.length; i < len; i++) {
|
|
354
|
-
const group = fieldGroups[i];
|
|
355
|
-
if (!group)
|
|
356
|
-
continue;
|
|
357
|
-
const fields = group.fields;
|
|
358
|
-
const validFields = [];
|
|
359
|
-
// Filter and collect valid fields in one pass
|
|
360
|
-
for (let j = 0, fieldLen = fields.length; j < fieldLen; j++) {
|
|
361
|
-
const field = fields[j];
|
|
362
|
-
if (!field)
|
|
363
|
-
continue;
|
|
364
|
-
// Skip conditional fields (they're nested in their parent field)
|
|
365
|
-
if (field.isConditional())
|
|
366
|
-
continue;
|
|
367
|
-
// For new item operations, exclude fields with adminGenerated not set to true
|
|
368
|
-
if (isNewOperation && field.adminGenerated !== true)
|
|
369
|
-
continue;
|
|
370
|
-
// For edit operations, exclude fields with adminGenerated set to false
|
|
371
|
-
if (isEditOperation && field.adminGenerated === false)
|
|
372
|
-
continue;
|
|
373
|
-
// For category sections, exclude parent_id and level fields
|
|
374
|
-
if (isCategorySection && (field.name === 'parent_id' || field.name === 'level'))
|
|
375
|
-
continue;
|
|
376
|
-
/**
|
|
377
|
-
* Add the field to the valid fields array
|
|
378
|
-
*/
|
|
379
|
-
validFields.push(field);
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* Only return the group if it has valid fields
|
|
383
|
-
*/
|
|
384
|
-
if (validFields.length > 0) {
|
|
385
|
-
processedGroups.push({
|
|
386
|
-
groupId: group.id,
|
|
387
|
-
groupTitle: group.title ?? '',
|
|
388
|
-
// Normalize order value here to avoid checking again in sort function
|
|
389
|
-
groupOrder: group.order ?? FieldFactory.DEFAULT_ORDER,
|
|
390
|
-
inputs: this.sortFieldsByOrder(validFields).map((field) => field.exportForClient()),
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
// Sort groups by order using the optimized function
|
|
395
|
-
this.sortGroupsByOrder(processedGroups);
|
|
396
|
-
/**
|
|
397
|
-
* Unset the section info to be garbage collected
|
|
398
|
-
*/
|
|
399
|
-
this._sectionInfo = null;
|
|
400
|
-
return processedGroups;
|
|
401
|
-
}
|
|
402
|
-
get sectionInfo() {
|
|
403
|
-
return this._sectionInfo ?? undefined;
|
|
404
|
-
}
|
|
405
|
-
get errorMessage() {
|
|
406
|
-
return this._errorMessage;
|
|
407
|
-
}
|
|
408
|
-
get error() {
|
|
409
|
-
return this._error;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { db } from '../../db/client.js';
|
|
3
|
+
import { SectionFactory } from './SectionFactory.js';
|
|
4
|
+
import { SimpleSection, HasItemsSection, CategorySection } from '../sections/index.js';
|
|
5
|
+
import { checkboxField, SelectField, SelectMultipleField } from '../fields/index.js';
|
|
6
|
+
import { is } from '../helpers/index.js';
|
|
7
|
+
import { cloneDeep } from 'lodash-es';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
export class FieldFactory {
|
|
10
|
+
session;
|
|
11
|
+
_values = {};
|
|
12
|
+
itemId = undefined;
|
|
13
|
+
_error = false;
|
|
14
|
+
_errorMessage = '';
|
|
15
|
+
type;
|
|
16
|
+
sectionName;
|
|
17
|
+
_sectionInfo;
|
|
18
|
+
// private _fields: Field[]
|
|
19
|
+
/**
|
|
20
|
+
* Default order value for items without explicit order.
|
|
21
|
+
* Ensures items without order come after explicitly ordered items.
|
|
22
|
+
* Typical order values: 0-15, with occasional values up to 1000.
|
|
23
|
+
*/
|
|
24
|
+
static DEFAULT_ORDER = 1_000_000;
|
|
25
|
+
/**
|
|
26
|
+
* Constructor
|
|
27
|
+
*/
|
|
28
|
+
constructor({ type, sectionName, session, itemId }) {
|
|
29
|
+
this.type = type;
|
|
30
|
+
this.sectionName = sectionName;
|
|
31
|
+
this.session = session;
|
|
32
|
+
this.itemId = itemId;
|
|
33
|
+
// this._fields = new Array<Field>()
|
|
34
|
+
this._sectionInfo = undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Must be called after the constructor
|
|
38
|
+
*/
|
|
39
|
+
async initialize() {
|
|
40
|
+
await this.initializeSectionInfo(this.session);
|
|
41
|
+
/**
|
|
42
|
+
* If it's an edit operation, let's initialize the field values
|
|
43
|
+
*/
|
|
44
|
+
if (this.type === 'edit') {
|
|
45
|
+
await this.initializeFieldValues();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async getFieldValue(field) {
|
|
49
|
+
if (this.type === 'new')
|
|
50
|
+
return undefined;
|
|
51
|
+
try {
|
|
52
|
+
return this._values[field.name];
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async initializeFieldValues() {
|
|
59
|
+
if (this._error)
|
|
60
|
+
return;
|
|
61
|
+
if (this._sectionInfo?.db.table && this._sectionInfo.db.identifier) {
|
|
62
|
+
/**
|
|
63
|
+
* Let's get the section item values from its table and assign it to the `_values` property
|
|
64
|
+
*/
|
|
65
|
+
// TODO: We still need to get the values from the `destinationDbTable` if it exists on any field
|
|
66
|
+
// also, select_multiple fields need to be handled the same way, plus value is array of strings
|
|
67
|
+
const [_v, fields] = await db.execute(sql `select * from ${sql.raw(this._sectionInfo.db.table)} where ${sql.raw(this._sectionInfo.db.identifier.name)} = ${this.itemId} limit 1`);
|
|
68
|
+
// @ts-ignore
|
|
69
|
+
// Bug: this is a bug in drizzle-orm/mysql2
|
|
70
|
+
this._values = _v[0];
|
|
71
|
+
/**
|
|
72
|
+
* If the field has a destinationDb
|
|
73
|
+
* Get the result from the field's table and the destinationDb's table using a JOIN query
|
|
74
|
+
* Fields that can have a destinationDb are: SelectField, SelectMultipleField, TagsField
|
|
75
|
+
*/
|
|
76
|
+
for (const field of this._sectionInfo.fields) {
|
|
77
|
+
if (field.destinationDb && (is(field, SelectMultipleField) || is(field, SelectField))) {
|
|
78
|
+
const sqlChunks = [
|
|
79
|
+
sql `select * from ${sql.raw(field.destinationDb.table)} a JOIN ${sql.raw(field.db.table)} b ON a.${sql.raw(field.destinationDb.selectIdentifier)} = b.${sql.raw(field.db.identifier)} where ${sql.raw(field.destinationDb.itemIdentifier)} = ${this.itemId}`,
|
|
80
|
+
];
|
|
81
|
+
if (is(field, SelectField) && field.hasDepth()) {
|
|
82
|
+
/**
|
|
83
|
+
* Add the where clause to the select statement
|
|
84
|
+
*/
|
|
85
|
+
sqlChunks.push(sql ` ORDER BY b.level ASC`);
|
|
86
|
+
}
|
|
87
|
+
const [_rows, _fields] = await db.execute(sql.join(sqlChunks));
|
|
88
|
+
const values = [];
|
|
89
|
+
if (Array.isArray(_rows)) {
|
|
90
|
+
for (const row of _rows) {
|
|
91
|
+
values.push({
|
|
92
|
+
value: row[field.destinationDb.selectIdentifier],
|
|
93
|
+
label: row[field.db.label],
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
this._values[field.name] = values;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* If the section item is not found, set the `_error` property to true
|
|
102
|
+
*/
|
|
103
|
+
if (!is(this._sectionInfo, SimpleSection) && !this._values) {
|
|
104
|
+
this._error = true;
|
|
105
|
+
this._errorMessage = 'Item not found';
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
this._error = true;
|
|
111
|
+
this._errorMessage = 'Section table or section table identifier is missing';
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Gets the section info and assigns it to the `sectionInfo` property
|
|
117
|
+
* @param session
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
async initializeSectionInfo(session) {
|
|
121
|
+
/**
|
|
122
|
+
* Get section info - could be a config (with build()) or an instance
|
|
123
|
+
* Section files should export configs, but we handle both for backward compatibility
|
|
124
|
+
*/
|
|
125
|
+
let _s = await this.getSectionInfo(session);
|
|
126
|
+
if (_s === undefined)
|
|
127
|
+
return;
|
|
128
|
+
/**
|
|
129
|
+
* Build the section from config when we need to use it
|
|
130
|
+
* SectionFactory returns configs, so we always build them here
|
|
131
|
+
*
|
|
132
|
+
* Double check if the config has a build() method
|
|
133
|
+
*/
|
|
134
|
+
if (typeof _s.build === 'function') {
|
|
135
|
+
// It's a config - build it to get a fresh instance
|
|
136
|
+
// We build here because we immediately need to access section properties (db.table, inputs, etc.)
|
|
137
|
+
this._sectionInfo = _s.build();
|
|
138
|
+
/**
|
|
139
|
+
* Build the fields from the field configs
|
|
140
|
+
*/
|
|
141
|
+
this._sectionInfo.buildFields();
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const errorMessage = 'Section config must have a build() method. Use helper functions like simpleSection(), hasItemsSection(), categorySection() to create section configs.';
|
|
145
|
+
console.error(chalk.bold.red('[FieldFactory Error]', errorMessage));
|
|
146
|
+
this._error = true;
|
|
147
|
+
this._errorMessage = errorMessage;
|
|
148
|
+
throw new Error(errorMessage);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Unset the `_s` variable to be garbage collected
|
|
152
|
+
*/
|
|
153
|
+
_s = undefined;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get the section info from the `sectionsTable` table
|
|
157
|
+
* @param session
|
|
158
|
+
* @private
|
|
159
|
+
*/
|
|
160
|
+
async getSectionInfo(session) {
|
|
161
|
+
/**
|
|
162
|
+
* First, let's get the section details, and then get the section input fields
|
|
163
|
+
*/
|
|
164
|
+
const s = await SectionFactory.getSectionForAdmin({
|
|
165
|
+
name: this.sectionName,
|
|
166
|
+
admin: {
|
|
167
|
+
id: session.user.id,
|
|
168
|
+
requiredRole: this.type === 'new' ? 'C' : 'U',
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
if (!s || !s.name) {
|
|
172
|
+
this._error = true;
|
|
173
|
+
this._errorMessage = 'Section not found or you do not have access to this operation';
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
return s;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Prepare a field for use
|
|
180
|
+
* Fields are already instances (resolved in Section constructor), so we just need to set values
|
|
181
|
+
* @param field
|
|
182
|
+
*/
|
|
183
|
+
async prepareField(field) {
|
|
184
|
+
// Fields are already instances - no need to build them
|
|
185
|
+
// They were resolved from configs in the Section constructor
|
|
186
|
+
if (this.type === 'edit') {
|
|
187
|
+
/**
|
|
188
|
+
* If it's an edit operation, get the value from the database and set it
|
|
189
|
+
*/
|
|
190
|
+
const value = await this.getFieldValue(field);
|
|
191
|
+
// It's important to set the value to whatever is in the database,
|
|
192
|
+
// even if the field has a `defaultValue` set.
|
|
193
|
+
field.setValue(value);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// TODO: Implement this, maybe set the roles in the trpc context?
|
|
197
|
+
addPermissionField() {
|
|
198
|
+
if (is(this._sectionInfo, HasItemsSection)) {
|
|
199
|
+
if (this._sectionInfo.requirePublishPermission && this.session.user) {
|
|
200
|
+
return checkboxField({
|
|
201
|
+
name: 'publish',
|
|
202
|
+
label: 'Publish',
|
|
203
|
+
required: true,
|
|
204
|
+
order: 1000,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Generate the fields
|
|
211
|
+
*/
|
|
212
|
+
async generateFields() {
|
|
213
|
+
if (this._error)
|
|
214
|
+
return;
|
|
215
|
+
if (!this._sectionInfo)
|
|
216
|
+
return;
|
|
217
|
+
for (const input of this._sectionInfo.fields) {
|
|
218
|
+
/**
|
|
219
|
+
* If it's an edit operation, set the field to readonly if it's the identifier field,
|
|
220
|
+
* identifier fields must not be edited!
|
|
221
|
+
* They are always visible as a readonly text.
|
|
222
|
+
*/
|
|
223
|
+
if (this.type === 'edit' && input.name === this._sectionInfo.db.identifier.name) {
|
|
224
|
+
input.adminGenerated = 'readonly';
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Prepare the field (set values if editing)
|
|
228
|
+
*/
|
|
229
|
+
await this.prepareField(input);
|
|
230
|
+
}
|
|
231
|
+
this.sortFields();
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Sort the fields.
|
|
235
|
+
* This function sorts the conditional fields into their respective fields.
|
|
236
|
+
*/
|
|
237
|
+
sortFields() {
|
|
238
|
+
if (this._error)
|
|
239
|
+
return;
|
|
240
|
+
if (!this._sectionInfo)
|
|
241
|
+
return;
|
|
242
|
+
/**
|
|
243
|
+
* Create a map of conditional fields
|
|
244
|
+
*/
|
|
245
|
+
const _conditionalFieldMap = new Map();
|
|
246
|
+
/**
|
|
247
|
+
* Loop through the fields and get the conditional fields
|
|
248
|
+
*/
|
|
249
|
+
for (const input of this._sectionInfo.fields) {
|
|
250
|
+
input.conditionalFields = [];
|
|
251
|
+
/**
|
|
252
|
+
* Check if the field is conditional.
|
|
253
|
+
*/
|
|
254
|
+
if (input.isConditional()) {
|
|
255
|
+
for (const rule of input.conditionalRules ?? []) {
|
|
256
|
+
/**
|
|
257
|
+
* Add the conditional input to the conditional fields lists
|
|
258
|
+
*/
|
|
259
|
+
if (!_conditionalFieldMap.has(rule.field.name)) {
|
|
260
|
+
_conditionalFieldMap.set(rule.field.name, []);
|
|
261
|
+
}
|
|
262
|
+
_conditionalFieldMap.get(rule.field.name).push(
|
|
263
|
+
/**
|
|
264
|
+
* Clone the conditional field to avoid reference issues
|
|
265
|
+
*/
|
|
266
|
+
cloneDeep({
|
|
267
|
+
field: input.exportForClient(),
|
|
268
|
+
rule: {
|
|
269
|
+
condition: rule.condition,
|
|
270
|
+
value: rule.value,
|
|
271
|
+
},
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Assign conditionalFields to the appropriate fields
|
|
278
|
+
*/
|
|
279
|
+
for (const field of this._sectionInfo.fields) {
|
|
280
|
+
const matchingConditionalFields = _conditionalFieldMap.get(field.name);
|
|
281
|
+
if (matchingConditionalFields) {
|
|
282
|
+
field.conditionalFields.push(...matchingConditionalFields.map((clonedConditionalField) => ({
|
|
283
|
+
...clonedConditionalField,
|
|
284
|
+
rule: {
|
|
285
|
+
condition: clonedConditionalField.rule.condition,
|
|
286
|
+
value: clonedConditionalField.rule.value,
|
|
287
|
+
},
|
|
288
|
+
})));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Destroy the map
|
|
293
|
+
*/
|
|
294
|
+
_conditionalFieldMap.clear();
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Sort fields by their order property, preserving original position for fields without order.
|
|
298
|
+
*
|
|
299
|
+
* Sorting behavior:
|
|
300
|
+
* 1. Fields with explicit `order` values are sorted by that value (ascending)
|
|
301
|
+
* 2. Fields without `order` maintain their original array position (stable sort)
|
|
302
|
+
* 3. Fields with the same `order` value maintain their relative positions (stable sort)
|
|
303
|
+
*
|
|
304
|
+
* This leverages JavaScript's stable Array.sort() (ES2019+) for optimal performance.
|
|
305
|
+
* Note: This mutates the input array for performance.
|
|
306
|
+
*
|
|
307
|
+
* @param fields - Array of fields to sort (will be mutated)
|
|
308
|
+
* @returns The sorted array (same reference as input)
|
|
309
|
+
* @private
|
|
310
|
+
*/
|
|
311
|
+
sortFieldsByOrder(fields) {
|
|
312
|
+
// Sort in-place for performance (mutates array)
|
|
313
|
+
// JavaScript's sort is stable (ES2019+), so equal values maintain original order
|
|
314
|
+
return fields.sort((a, b) => (a.order ?? FieldFactory.DEFAULT_ORDER) - (b.order ?? FieldFactory.DEFAULT_ORDER));
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Sort groups by their groupOrder property, preserving original position for groups without order.
|
|
318
|
+
*
|
|
319
|
+
* Sorting behavior:
|
|
320
|
+
* 1. Groups with explicit `groupOrder` values are sorted by that value (ascending)
|
|
321
|
+
* 2. Groups without `groupOrder` maintain their original array position (stable sort)
|
|
322
|
+
* 3. Groups with the same `groupOrder` value maintain their relative positions (stable sort)
|
|
323
|
+
*
|
|
324
|
+
* This leverages JavaScript's stable Array.sort() (ES2019+) for optimal performance.
|
|
325
|
+
* Note: This mutates the input array for performance.
|
|
326
|
+
* Note: groupOrder is normalized at creation time, so no nullish coalescing needed here.
|
|
327
|
+
*
|
|
328
|
+
* @param groups - Array of groups to sort (will be mutated)
|
|
329
|
+
* @returns The sorted array (same reference as input)
|
|
330
|
+
* @private
|
|
331
|
+
*/
|
|
332
|
+
sortGroupsByOrder(groups) {
|
|
333
|
+
// Sort in-place for performance (mutates array)
|
|
334
|
+
// JavaScript's sort is stable (ES2019+), so equal values maintain original order
|
|
335
|
+
// groupOrder is normalized at creation time, so direct subtraction is safe
|
|
336
|
+
return groups.sort((a, b) => a.groupOrder - b.groupOrder);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Get field groups with filtered and sorted fields.
|
|
340
|
+
* Leverages the pre-built fieldGroups from the Section class for better performance.
|
|
341
|
+
*/
|
|
342
|
+
getGroupedFields() {
|
|
343
|
+
if (this._error)
|
|
344
|
+
return;
|
|
345
|
+
if (!this._sectionInfo)
|
|
346
|
+
return;
|
|
347
|
+
const fieldGroups = this._sectionInfo.fieldGroups;
|
|
348
|
+
const isNewOperation = this.type === 'new';
|
|
349
|
+
const isEditOperation = this.type === 'edit';
|
|
350
|
+
const isCategorySection = is(this._sectionInfo, CategorySection);
|
|
351
|
+
// Build groups in a single pass, skipping empty groups
|
|
352
|
+
const processedGroups = [];
|
|
353
|
+
for (let i = 0, len = fieldGroups.length; i < len; i++) {
|
|
354
|
+
const group = fieldGroups[i];
|
|
355
|
+
if (!group)
|
|
356
|
+
continue;
|
|
357
|
+
const fields = group.fields;
|
|
358
|
+
const validFields = [];
|
|
359
|
+
// Filter and collect valid fields in one pass
|
|
360
|
+
for (let j = 0, fieldLen = fields.length; j < fieldLen; j++) {
|
|
361
|
+
const field = fields[j];
|
|
362
|
+
if (!field)
|
|
363
|
+
continue;
|
|
364
|
+
// Skip conditional fields (they're nested in their parent field)
|
|
365
|
+
if (field.isConditional())
|
|
366
|
+
continue;
|
|
367
|
+
// For new item operations, exclude fields with adminGenerated not set to true
|
|
368
|
+
if (isNewOperation && field.adminGenerated !== true)
|
|
369
|
+
continue;
|
|
370
|
+
// For edit operations, exclude fields with adminGenerated set to false
|
|
371
|
+
if (isEditOperation && field.adminGenerated === false)
|
|
372
|
+
continue;
|
|
373
|
+
// For category sections, exclude parent_id and level fields
|
|
374
|
+
if (isCategorySection && (field.name === 'parent_id' || field.name === 'level'))
|
|
375
|
+
continue;
|
|
376
|
+
/**
|
|
377
|
+
* Add the field to the valid fields array
|
|
378
|
+
*/
|
|
379
|
+
validFields.push(field);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Only return the group if it has valid fields
|
|
383
|
+
*/
|
|
384
|
+
if (validFields.length > 0) {
|
|
385
|
+
processedGroups.push({
|
|
386
|
+
groupId: group.id,
|
|
387
|
+
groupTitle: group.title ?? '',
|
|
388
|
+
// Normalize order value here to avoid checking again in sort function
|
|
389
|
+
groupOrder: group.order ?? FieldFactory.DEFAULT_ORDER,
|
|
390
|
+
inputs: this.sortFieldsByOrder(validFields).map((field) => field.exportForClient()),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Sort groups by order using the optimized function
|
|
395
|
+
this.sortGroupsByOrder(processedGroups);
|
|
396
|
+
/**
|
|
397
|
+
* Unset the section info to be garbage collected
|
|
398
|
+
*/
|
|
399
|
+
this._sectionInfo = null;
|
|
400
|
+
return processedGroups;
|
|
401
|
+
}
|
|
402
|
+
get sectionInfo() {
|
|
403
|
+
return this._sectionInfo ?? undefined;
|
|
404
|
+
}
|
|
405
|
+
get errorMessage() {
|
|
406
|
+
return this._errorMessage;
|
|
407
|
+
}
|
|
408
|
+
get error() {
|
|
409
|
+
return this._error;
|
|
410
|
+
}
|
|
411
|
+
}
|