@reldens/cms 0.48.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/advanced-usage-guide.md +275 -0
- package/.claude/templating-system-guide.md +81 -3
- package/CLAUDE.md +77 -2
- package/admin/reldens-admin-client.css +19 -11
- package/admin/reldens-admin-client.js +34 -0
- package/admin/templates/cache-clean-button.html +1 -0
- package/admin/templates/edit.html +3 -0
- package/admin/templates/fields/edit/file.html +1 -1
- package/admin/templates/view.html +5 -2
- package/lib/admin-manager/router-contents.js +11 -4
- package/lib/cache/add-cache-button-subscriber.js +17 -3
- package/lib/cache/cache-manager.js +36 -18
- package/lib/cache/cache-routes-handler.js +4 -1
- package/lib/entities-config-processor.js +59 -0
- package/lib/manager.js +13 -2
- package/lib/template-reloader.js +10 -10
- package/package.json +1 -1
- package/templates/page.html +15 -11
|
@@ -96,3 +96,278 @@ cms.events.on('adminEntityExtraData', ({entitySerializedData, entity}) => {
|
|
|
96
96
|
entitySerializedData.customField = 'Custom Value';
|
|
97
97
|
});
|
|
98
98
|
```
|
|
99
|
+
|
|
100
|
+
## Event-Driven Customizations
|
|
101
|
+
|
|
102
|
+
The CMS uses an event system for extensibility. This allows you to add custom behavior without modifying core CMS files.
|
|
103
|
+
|
|
104
|
+
### Admin UI Customizations
|
|
105
|
+
|
|
106
|
+
Add buttons and content to admin views using events. Always use templates and renderCallback - never hardcode HTML in classes.
|
|
107
|
+
|
|
108
|
+
**Step 1: Create template file** (`admin/templates/custom-button.html`):
|
|
109
|
+
```html
|
|
110
|
+
<button class="button button-primary" type="submit" form="edit-form"
|
|
111
|
+
name="customAction" value="customValue">
|
|
112
|
+
{{&buttonText}}
|
|
113
|
+
</button>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Step 2: Register template** in `lib/templates-list.js`:
|
|
117
|
+
```javascript
|
|
118
|
+
module.exports.TemplatesList = {
|
|
119
|
+
// ... existing templates
|
|
120
|
+
customButton: 'custom-button.html',
|
|
121
|
+
};
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Step 3: Create subscriber class**:
|
|
125
|
+
```javascript
|
|
126
|
+
const { Logger, sc } = require('@reldens/utils');
|
|
127
|
+
|
|
128
|
+
class CustomButtonSubscriber
|
|
129
|
+
{
|
|
130
|
+
|
|
131
|
+
constructor(props)
|
|
132
|
+
{
|
|
133
|
+
this.events = sc.get(props, 'events', false);
|
|
134
|
+
this.renderCallback = sc.get(props, 'renderCallback', false);
|
|
135
|
+
this.customButtonTemplate = sc.get(props, 'customButtonTemplate', '');
|
|
136
|
+
this.translations = sc.get(props, 'translations', {});
|
|
137
|
+
this.setupEvents();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setupEvents()
|
|
141
|
+
{
|
|
142
|
+
if(!this.events){
|
|
143
|
+
Logger.error('Events Manager not found on CustomButtonSubscriber.');
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
this.events.on('reldens.adminEditPropertiesPopulation', this.addCustomButton.bind(this));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async addCustomButton(event)
|
|
150
|
+
{
|
|
151
|
+
if(!this.customButtonTemplate){
|
|
152
|
+
Logger.error('Custom button template not found');
|
|
153
|
+
return '';
|
|
154
|
+
}
|
|
155
|
+
if(!this.renderCallback){
|
|
156
|
+
Logger.error('Render callback not available');
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
if(!event.renderedEditProperties.extraContentForEdit){
|
|
160
|
+
event.renderedEditProperties.extraContentForEdit = '';
|
|
161
|
+
}
|
|
162
|
+
let buttonHtml = await this.renderCallback(
|
|
163
|
+
this.customButtonTemplate,
|
|
164
|
+
{
|
|
165
|
+
buttonText: sc.get(this.translations.labels, 'customAction', 'Custom Action')
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
event.renderedEditProperties.extraContentForEdit += buttonHtml;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Step 4: Initialize in AdminManager**:
|
|
175
|
+
```javascript
|
|
176
|
+
this.customButtonSubscriber = new CustomButtonSubscriber({
|
|
177
|
+
events: this.events,
|
|
178
|
+
renderCallback: this.renderCallback,
|
|
179
|
+
customButtonTemplate: this.adminFilesContents.customButton,
|
|
180
|
+
translations: this.translations
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**See `lib/cache/save-and-clear-cache-subscriber.js` for complete working example.**
|
|
185
|
+
|
|
186
|
+
### Form Processing Customizations
|
|
187
|
+
|
|
188
|
+
Handle custom save actions and form submissions:
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
class CustomFormHandler
|
|
192
|
+
{
|
|
193
|
+
|
|
194
|
+
constructor(props)
|
|
195
|
+
{
|
|
196
|
+
this.events = props.events;
|
|
197
|
+
this.dataServer = props.dataServer;
|
|
198
|
+
this.setupEvents();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
setupEvents()
|
|
202
|
+
{
|
|
203
|
+
this.events.on('reldens.dynamicFormRequestHandler.beforeSave', this.beforeSave.bind(this));
|
|
204
|
+
this.events.on('reldens.dynamicFormRequestHandler.afterSave', this.afterSave.bind(this));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async beforeSave(event)
|
|
208
|
+
{
|
|
209
|
+
if(event.validationResult.errors){
|
|
210
|
+
event.validationResult.extraErrors = this.customValidation(event.formData);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async afterSave(event)
|
|
215
|
+
{
|
|
216
|
+
let customAction = event.req?.body?.customAction;
|
|
217
|
+
if('customValue' === customAction){
|
|
218
|
+
await this.performCustomAction(event.result);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
customValidation(formData)
|
|
223
|
+
{
|
|
224
|
+
let errors = [];
|
|
225
|
+
if(formData.customField && formData.customField.length < 10){
|
|
226
|
+
errors.push('customField must be at least 10 characters');
|
|
227
|
+
}
|
|
228
|
+
return errors;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async performCustomAction(savedEntity)
|
|
232
|
+
{
|
|
233
|
+
Logger.info('Performing custom action for entity:', savedEntity.id);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let manager = new Manager(managerConfiguration);
|
|
239
|
+
new CustomFormHandler({
|
|
240
|
+
events: manager.events,
|
|
241
|
+
dataServer: manager.dataServer
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Entity Config Overrides
|
|
246
|
+
|
|
247
|
+
Customize generated entity configurations without editing generated files:
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
class CmsPagesOverride
|
|
251
|
+
{
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param {Object} baseConfig
|
|
255
|
+
* @param {Object} props
|
|
256
|
+
* @returns {Object}
|
|
257
|
+
*/
|
|
258
|
+
static propertiesConfig(baseConfig, props)
|
|
259
|
+
{
|
|
260
|
+
baseConfig.properties.meta_og_image = {
|
|
261
|
+
...baseConfig.properties.meta_og_image,
|
|
262
|
+
isUpload: true,
|
|
263
|
+
allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
264
|
+
bucket: 'cms-og-images',
|
|
265
|
+
bucketPath: '/og-images/'
|
|
266
|
+
};
|
|
267
|
+
baseConfig.properties.status = {
|
|
268
|
+
...baseConfig.properties.status,
|
|
269
|
+
type: 'select',
|
|
270
|
+
options: [
|
|
271
|
+
{ value: 'draft', label: 'Draft' },
|
|
272
|
+
{ value: 'published', label: 'Published' },
|
|
273
|
+
{ value: 'archived', label: 'Archived' }
|
|
274
|
+
]
|
|
275
|
+
};
|
|
276
|
+
return baseConfig;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let manager = new Manager({
|
|
282
|
+
...managerConfiguration,
|
|
283
|
+
entitiesConfigOverride: {
|
|
284
|
+
'cmsPages': CmsPagesOverride // CRITICAL: Use camelCase key from entities-config.js
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**CRITICAL: Entity keys must match exactly as defined in `generated-entities/entities-config.js`:**
|
|
290
|
+
- Use `'cmsPages'` NOT `'cms-pages'`
|
|
291
|
+
- Use `'cmsBlocks'` NOT `'cms-blocks'`
|
|
292
|
+
- Use `'cmsForms'` NOT `'cms-forms'`
|
|
293
|
+
- Keys are camelCase, not kebab-case
|
|
294
|
+
|
|
295
|
+
### Conditional Event Handling
|
|
296
|
+
|
|
297
|
+
Control when customizations apply based on conditions:
|
|
298
|
+
|
|
299
|
+
```javascript
|
|
300
|
+
const { Logger, sc } = require('@reldens/utils');
|
|
301
|
+
|
|
302
|
+
class ConditionalCustomization
|
|
303
|
+
{
|
|
304
|
+
|
|
305
|
+
constructor(props)
|
|
306
|
+
{
|
|
307
|
+
this.events = sc.get(props, 'events', false);
|
|
308
|
+
this.cacheManager = sc.get(props, 'cacheManager', false);
|
|
309
|
+
this.renderCallback = sc.get(props, 'renderCallback', false);
|
|
310
|
+
this.contentTemplate = sc.get(props, 'contentTemplate', '');
|
|
311
|
+
this.setupEvents();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
setupEvents()
|
|
315
|
+
{
|
|
316
|
+
if(!this.cacheManager?.isEnabled()){
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
if(!this.events){
|
|
320
|
+
Logger.error('Events Manager not found.');
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
this.events.on('reldens.adminEditPropertiesPopulation', this.addContent.bind(this));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async addContent(event)
|
|
327
|
+
{
|
|
328
|
+
if('routes' !== event.driverResource.id()){
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
if(!this.contentTemplate || !this.renderCallback){
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
if(!event.renderedEditProperties.extraContentForEdit){
|
|
335
|
+
event.renderedEditProperties.extraContentForEdit = '';
|
|
336
|
+
}
|
|
337
|
+
let contentHtml = await this.renderCallback(this.contentTemplate, {});
|
|
338
|
+
event.renderedEditProperties.extraContentForEdit += contentHtml;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let manager = new Manager(managerConfiguration);
|
|
344
|
+
new ConditionalCustomization({
|
|
345
|
+
events: manager.events,
|
|
346
|
+
cacheManager: manager.cacheManager,
|
|
347
|
+
renderCallback: manager.renderCallback,
|
|
348
|
+
contentTemplate: manager.adminFilesContents.conditionalContent
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**CRITICAL: Never hardcode HTML strings in subscriber classes. Always use templates with renderCallback and translations.**
|
|
353
|
+
|
|
354
|
+
### Available Admin Events
|
|
355
|
+
|
|
356
|
+
**View/Edit/List Property Population**:
|
|
357
|
+
- `reldens.adminViewPropertiesPopulation` - Add content to view pages
|
|
358
|
+
- `reldens.adminEditPropertiesPopulation` - Add content to edit pages
|
|
359
|
+
- `reldens.adminListPropertiesPopulation` - Add content to list pages
|
|
360
|
+
|
|
361
|
+
**Entity Operations**:
|
|
362
|
+
- `reldens.adminBeforeEntitySave` - Before entity save
|
|
363
|
+
- `reldens.adminAfterEntitySave` - After entity save
|
|
364
|
+
|
|
365
|
+
**Form Processing**:
|
|
366
|
+
- `reldens.dynamicFormRequestHandler.beforeValidation` - Before form validation
|
|
367
|
+
- `reldens.dynamicFormRequestHandler.beforeSave` - Before form save
|
|
368
|
+
- `reldens.dynamicFormRequestHandler.afterSave` - After form save
|
|
369
|
+
|
|
370
|
+
**Setup Events**:
|
|
371
|
+
- `reldens.setupAdminRouter` - Setup admin router
|
|
372
|
+
- `reldens.setupAdminRoutes` - Setup admin routes
|
|
373
|
+
- `reldens.setupAdminManagers` - Setup admin managers
|
|
@@ -30,7 +30,13 @@ Every template has access to system variables providing context about the curren
|
|
|
30
30
|
|
|
31
31
|
## Template Functions
|
|
32
32
|
|
|
33
|
-
### URL
|
|
33
|
+
### URL Transformers
|
|
34
|
+
|
|
35
|
+
The CMS provides three URL transformers with different behaviors for CDN support:
|
|
36
|
+
|
|
37
|
+
#### [url()] - Public URL (No CDN)
|
|
38
|
+
|
|
39
|
+
Generates URLs using the **public base URL** without CDN support.
|
|
34
40
|
|
|
35
41
|
```html
|
|
36
42
|
[url(/articles)]
|
|
@@ -38,12 +44,84 @@ Every template has access to system variables providing context about the curren
|
|
|
38
44
|
[url(/css/styles.css)]
|
|
39
45
|
```
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
**Output:**
|
|
48
|
+
- Without CDN: `https://example.com/css/styles.css`
|
|
49
|
+
- With CDN configured: `https://example.com/css/styles.css` (same, CDN ignored)
|
|
50
|
+
|
|
51
|
+
**Use for:** Internal application routes, API endpoints, form actions.
|
|
52
|
+
|
|
53
|
+
#### [cdn()] - CDN or Public URL
|
|
54
|
+
|
|
55
|
+
Generates URLs using the **CDN URL if configured**, otherwise falls back to public URL.
|
|
42
56
|
|
|
43
57
|
```html
|
|
44
|
-
[
|
|
58
|
+
[cdn(/css/styles.css)]
|
|
59
|
+
[cdn(/js/scripts.js)]
|
|
60
|
+
[cdn(/assets/web/logo.png)]
|
|
45
61
|
```
|
|
46
62
|
|
|
63
|
+
**Output:**
|
|
64
|
+
- Without CDN: `https://example.com/css/styles.css`
|
|
65
|
+
- With CDN configured: `https://cdn.example.com/css/styles.css`
|
|
66
|
+
|
|
67
|
+
**Use for:** Static assets outside `/assets` folder (CSS, JS, fonts, etc.).
|
|
68
|
+
|
|
69
|
+
#### [asset()] - CDN or Public URL + /assets Prefix
|
|
70
|
+
|
|
71
|
+
Generates URLs using the **CDN URL if configured** and **automatically prepends `/assets`** to the path.
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
[asset(/web/logo.png)]
|
|
75
|
+
[asset(/images/hero.jpg)]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Output:**
|
|
79
|
+
- Without CDN: `https://example.com/assets/web/logo.png`
|
|
80
|
+
- With CDN configured: `https://cdn.example.com/assets/web/logo.png`
|
|
81
|
+
|
|
82
|
+
**Use for:** Static assets inside `/assets` folder (images, downloads, media files).
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### URL Transformer Best Practices
|
|
87
|
+
|
|
88
|
+
**Based on folder structure:**
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
public/
|
|
92
|
+
assets/ # Use [asset(/file)] OR [cdn(/assets/file)]
|
|
93
|
+
web/
|
|
94
|
+
images/
|
|
95
|
+
downloads/
|
|
96
|
+
css/ # Use [cdn(/css/file)]
|
|
97
|
+
js/ # Use [cdn(/js/file)]
|
|
98
|
+
fonts/ # Use [cdn(/fonts/file)]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Examples:**
|
|
102
|
+
|
|
103
|
+
```html
|
|
104
|
+
<!-- Files in public/assets/ -->
|
|
105
|
+
<meta property="og:image" content="[asset(/web/logo.png)]"/>
|
|
106
|
+
<img src="[asset(/images/hero.jpg)]" alt="Hero"/>
|
|
107
|
+
|
|
108
|
+
<!-- Files in public/css/, public/js/, etc. -->
|
|
109
|
+
<link rel="stylesheet" href="[cdn(/css/styles.css)]"/>
|
|
110
|
+
<script src="[cdn(/js/scripts.js)]"></script>
|
|
111
|
+
|
|
112
|
+
<!-- Internal routes -->
|
|
113
|
+
<form action="[url(/api/submit)]" method="post">
|
|
114
|
+
<a href="[url(/articles)]">Articles</a>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Key Rules:**
|
|
118
|
+
- `[asset(/file)]` adds `/assets` prefix automatically → Use for files in `/assets` folder
|
|
119
|
+
- `[cdn(/assets/file)]` requires full path → Alternative for files in `/assets` folder
|
|
120
|
+
- `[cdn(/css/file)]` requires full path → Use for files outside `/assets` (css, js, fonts)
|
|
121
|
+
- `[url(/path)]` never uses CDN → Use for application routes only
|
|
122
|
+
|
|
123
|
+
**Recommendation:** Use `[asset()]` for `/assets` files (cleaner syntax), `[cdn()]` for everything else.
|
|
124
|
+
|
|
47
125
|
### Date Formatting
|
|
48
126
|
|
|
49
127
|
```html
|
package/CLAUDE.md
CHANGED
|
@@ -407,13 +407,21 @@ templates/
|
|
|
407
407
|
|
|
408
408
|
**Template Functions:**
|
|
409
409
|
```html
|
|
410
|
-
[url(/articles)] <!-- URL generation -->
|
|
411
|
-
[
|
|
410
|
+
[url(/articles)] <!-- URL generation (no CDN) -->
|
|
411
|
+
[cdn(/css/styles.css)] <!-- CDN URL (or public URL if no CDN) -->
|
|
412
|
+
[asset(/web/logo.png)] <!-- Asset URL with /assets prefix (uses CDN if available) -->
|
|
412
413
|
[date(now, Y-m-d)] <!-- Date formatting -->
|
|
413
414
|
[translate(welcome.message)] <!-- i18n -->
|
|
414
415
|
[t(key, Default, {var: value})] <!-- i18n with interpolation -->
|
|
415
416
|
```
|
|
416
417
|
|
|
418
|
+
**URL Transformer Selection:**
|
|
419
|
+
- `[url()]` - Application routes (never uses CDN)
|
|
420
|
+
- `[cdn()]` - Static assets outside `/assets` (CSS, JS, fonts)
|
|
421
|
+
- `[asset()]` - Static assets inside `/assets` folder (images, media)
|
|
422
|
+
|
|
423
|
+
See `.claude/templating-system-guide.md` for detailed URL transformer documentation.
|
|
424
|
+
|
|
417
425
|
## Configuration
|
|
418
426
|
|
|
419
427
|
### Environment Variables (.env)
|
|
@@ -513,10 +521,16 @@ The CMS provides extensive event hooks for customization:
|
|
|
513
521
|
- `reldens.setupAdminRoutes` - After route setup
|
|
514
522
|
- `reldens.setupAdminManagers` - After manager setup
|
|
515
523
|
- `reldens.adminBeforeEntitySave` - Before entity save (used by password encryption handler)
|
|
524
|
+
- `reldens.adminAfterEntitySave` - After entity save
|
|
525
|
+
- `reldens.adminViewPropertiesPopulation` - Add content to view pages
|
|
526
|
+
- `reldens.adminEditPropertiesPopulation` - Add content to edit pages
|
|
527
|
+
- `reldens.adminListPropertiesPopulation` - Add content to list pages
|
|
516
528
|
|
|
517
529
|
### Template Reloading Events
|
|
518
530
|
- `reldens.templateReloader.templatesChanged` - Templates changed
|
|
519
531
|
|
|
532
|
+
See `.claude/advanced-usage-guide.md` for comprehensive event-driven customization patterns and examples.
|
|
533
|
+
|
|
520
534
|
## Development Workflow
|
|
521
535
|
|
|
522
536
|
### Template Reloading
|
|
@@ -561,6 +575,67 @@ Define custom entity configurations in `entitiesConfig`:
|
|
|
561
575
|
}
|
|
562
576
|
```
|
|
563
577
|
|
|
578
|
+
### Entity Config Overrides
|
|
579
|
+
Override generated entity configurations without editing generated files using `entitiesConfigOverride`:
|
|
580
|
+
```javascript
|
|
581
|
+
class CmsPagesOverride
|
|
582
|
+
{
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* @param {Object} baseConfig
|
|
586
|
+
* @param {Object} props
|
|
587
|
+
* @returns {Object}
|
|
588
|
+
*/
|
|
589
|
+
static propertiesConfig(baseConfig, props)
|
|
590
|
+
{
|
|
591
|
+
baseConfig.properties.meta_og_image = {
|
|
592
|
+
dbType: 'varchar',
|
|
593
|
+
isUpload: true,
|
|
594
|
+
allowedTypes: 'image',
|
|
595
|
+
bucket: 'public/assets/media',
|
|
596
|
+
bucketPath: '/assets/media/'
|
|
597
|
+
};
|
|
598
|
+
return baseConfig;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const manager = new Manager({
|
|
604
|
+
entitiesConfigOverride: {
|
|
605
|
+
'cmsPages': CmsPagesOverride // Use camelCase key from entities-config.js
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
**CRITICAL: Entity keys must match exactly as defined in `generated-entities/entities-config.js`**
|
|
611
|
+
- Use camelCase: `'cmsPages'`, `'cmsBlocks'`, `'cmsForms'`
|
|
612
|
+
- NOT kebab-case: `'cms-pages'`, `'cms-blocks'`, `'cms-forms'`
|
|
613
|
+
|
|
614
|
+
Supports three override patterns:
|
|
615
|
+
- **Class-based**: Class with static `propertiesConfig(baseConfig, props)` method
|
|
616
|
+
- **Function-based**: Function that receives `(baseConfig, props)` and returns modified config
|
|
617
|
+
- **Object-based**: Plain object that gets deep merged with base config
|
|
618
|
+
|
|
619
|
+
**Upload Field Configuration:**
|
|
620
|
+
- `allowedTypes`: Must be a STRING ('image', 'audio', 'text'), NOT an array
|
|
621
|
+
- `bucket`: Relative path from projectRoot (e.g., 'public/assets/media')
|
|
622
|
+
- `bucketPath`: URL path for display (e.g., '/assets/media/')
|
|
623
|
+
- Do NOT use spread syntax - replace entire property object
|
|
624
|
+
- Do NOT use FileHandler.joinPaths() - bucket paths are relative to projectRoot
|
|
625
|
+
|
|
626
|
+
**Upload Field Removal:**
|
|
627
|
+
- Admin edit pages automatically show an "X" button before uploaded filenames
|
|
628
|
+
- Clicking X creates hidden input `clear_fieldname=1` in form
|
|
629
|
+
- Server detects this and sets field to null in database
|
|
630
|
+
- File remains on disk - only database field is cleared
|
|
631
|
+
- Template: `admin/templates/fields/edit/file-claude.html`
|
|
632
|
+
- Client JS: `admin/reldens-admin-client-claude.js` (remove upload functionality)
|
|
633
|
+
- Server handling: `lib/admin-manager/router-contents-claude.js` (clear_ parameter detection)
|
|
634
|
+
|
|
635
|
+
**Example subscribers**: See `lib/cache/add-cache-button-subscriber.js` and `lib/cache/save-and-clear-cache-subscriber.js` for complete working examples of event-driven UI customizations.
|
|
636
|
+
|
|
637
|
+
See `.claude/advanced-usage-guide.md` for detailed examples.
|
|
638
|
+
|
|
564
639
|
## Security Features
|
|
565
640
|
|
|
566
641
|
- **Authentication** - Role-based admin access
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
background-color: var(--lightGrey);
|
|
26
26
|
margin: 0;
|
|
27
27
|
padding: 0;
|
|
28
|
-
font-size:
|
|
28
|
+
font-size: 0.75rem;
|
|
29
29
|
|
|
30
30
|
.wrapper {
|
|
31
31
|
display: flex;
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
right: 0;
|
|
41
41
|
padding: 1rem 5rem 1rem 2rem;
|
|
42
42
|
border-radius: 8px 0 0 8px;
|
|
43
|
-
font-size:
|
|
43
|
+
font-size: 0.875rem;
|
|
44
44
|
|
|
45
45
|
&.success, &.error {
|
|
46
46
|
display: block;
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
padding: 0.5rem 1rem;
|
|
92
92
|
border: none;
|
|
93
93
|
border-radius: 4px;
|
|
94
|
-
font-size:
|
|
94
|
+
font-size: 0.875rem;
|
|
95
95
|
cursor: pointer;
|
|
96
96
|
text-decoration: none;
|
|
97
97
|
|
|
@@ -211,7 +211,7 @@
|
|
|
211
211
|
color: var(--lightGrey);
|
|
212
212
|
text-decoration: none;
|
|
213
213
|
border-left: 3px solid transparent;
|
|
214
|
-
font-size:
|
|
214
|
+
font-size: 0.75rem;
|
|
215
215
|
border-left: 3px solid #0000;
|
|
216
216
|
padding: 0.1rem 0.1rem 0.1rem 1rem;
|
|
217
217
|
margin-top: 0.3rem;
|
|
@@ -242,7 +242,7 @@
|
|
|
242
242
|
color: var(--lightGrey);
|
|
243
243
|
text-decoration: none;
|
|
244
244
|
cursor: pointer;
|
|
245
|
-
font-size:
|
|
245
|
+
font-size: 0.875rem;
|
|
246
246
|
font-weight: var(--font-semi-bold);
|
|
247
247
|
border-bottom: 1px solid var(--darkBlue);
|
|
248
248
|
|
|
@@ -274,6 +274,10 @@
|
|
|
274
274
|
justify-content: end;
|
|
275
275
|
margin-bottom: 1rem;
|
|
276
276
|
}
|
|
277
|
+
|
|
278
|
+
.extra-actions {
|
|
279
|
+
justify-content: end;
|
|
280
|
+
}
|
|
277
281
|
}
|
|
278
282
|
|
|
279
283
|
.forms-container {
|
|
@@ -283,7 +287,7 @@
|
|
|
283
287
|
}
|
|
284
288
|
|
|
285
289
|
.form-title {
|
|
286
|
-
font-size:
|
|
290
|
+
font-size: 1.375rem;
|
|
287
291
|
margin-bottom: 2%;
|
|
288
292
|
color: var(--darkGrey);
|
|
289
293
|
text-align: center;
|
|
@@ -329,7 +333,7 @@
|
|
|
329
333
|
}
|
|
330
334
|
|
|
331
335
|
& h2 {
|
|
332
|
-
font-size:
|
|
336
|
+
font-size: 1.375rem;
|
|
333
337
|
margin: 0 0 2rem;
|
|
334
338
|
color: var(--darkGrey);
|
|
335
339
|
text-align: center;
|
|
@@ -533,7 +537,7 @@
|
|
|
533
537
|
vertical-align: middle;
|
|
534
538
|
align-items: center;
|
|
535
539
|
margin: 0 1rem 1rem 0;
|
|
536
|
-
font-size:
|
|
540
|
+
font-size: 0.875rem;
|
|
537
541
|
color: var(--darkGrey);
|
|
538
542
|
|
|
539
543
|
&.filters-toggle {
|
|
@@ -650,7 +654,7 @@
|
|
|
650
654
|
|
|
651
655
|
.entity-view, .entity-edit {
|
|
652
656
|
& h2 {
|
|
653
|
-
font-size:
|
|
657
|
+
font-size: 1.375rem;
|
|
654
658
|
margin-bottom: 2rem;
|
|
655
659
|
color: var(--darkGrey);
|
|
656
660
|
text-align: center;
|
|
@@ -691,6 +695,10 @@
|
|
|
691
695
|
margin-left: 0.5rem;
|
|
692
696
|
margin-top: 0.5rem;
|
|
693
697
|
}
|
|
698
|
+
|
|
699
|
+
.remove-upload-btn {
|
|
700
|
+
cursor: pointer;
|
|
701
|
+
}
|
|
694
702
|
}
|
|
695
703
|
}
|
|
696
704
|
}
|
|
@@ -757,7 +765,7 @@
|
|
|
757
765
|
.extra-actions {
|
|
758
766
|
display: flex;
|
|
759
767
|
width: 100%;
|
|
760
|
-
justify-content:
|
|
768
|
+
justify-content: center;
|
|
761
769
|
margin: 1rem 0;
|
|
762
770
|
}
|
|
763
771
|
|
|
@@ -804,7 +812,7 @@
|
|
|
804
812
|
|
|
805
813
|
.dialog-content h5 {
|
|
806
814
|
margin: 0 0 1rem 0;
|
|
807
|
-
font-size:
|
|
815
|
+
font-size: 1.125rem;
|
|
808
816
|
color: var(--darkGrey);
|
|
809
817
|
}
|
|
810
818
|
|
|
@@ -242,6 +242,9 @@ window.addEventListener('DOMContentLoaded', () => {
|
|
|
242
242
|
? 'Success!'
|
|
243
243
|
: 'There was an error: '+escapeHTML(errorMessages[result] || result);
|
|
244
244
|
deleteCookie('result');
|
|
245
|
+
queryParams.delete('result');
|
|
246
|
+
let newUrl = location.pathname + (queryParams.toString() ? '?' + queryParams.toString() : '');
|
|
247
|
+
window.history.replaceState({}, '', newUrl);
|
|
245
248
|
}
|
|
246
249
|
}
|
|
247
250
|
|
|
@@ -282,4 +285,35 @@ window.addEventListener('DOMContentLoaded', () => {
|
|
|
282
285
|
});
|
|
283
286
|
}
|
|
284
287
|
|
|
288
|
+
// remove upload button functionality:
|
|
289
|
+
let removeUploadButtons = document.querySelectorAll('.remove-upload-btn');
|
|
290
|
+
if(removeUploadButtons){
|
|
291
|
+
for(let button of removeUploadButtons){
|
|
292
|
+
button.addEventListener('click', (event) => {
|
|
293
|
+
event.preventDefault();
|
|
294
|
+
let fieldName = button.getAttribute('data-field');
|
|
295
|
+
let fileInput = document.getElementById(fieldName);
|
|
296
|
+
let currentFileDisplay = document.querySelector('.upload-current-file[data-field="'+fieldName+'"]');
|
|
297
|
+
if(currentFileDisplay){
|
|
298
|
+
currentFileDisplay.style.display = 'none';
|
|
299
|
+
}
|
|
300
|
+
if(fileInput){
|
|
301
|
+
fileInput.value = '';
|
|
302
|
+
let form = fileInput.closest('form');
|
|
303
|
+
if(form){
|
|
304
|
+
let clearFieldName = 'clear_'+fieldName;
|
|
305
|
+
let existingClearInput = form.querySelector('input[name="'+clearFieldName+'"]');
|
|
306
|
+
if(!existingClearInput){
|
|
307
|
+
let clearInput = document.createElement('input');
|
|
308
|
+
clearInput.type = 'hidden';
|
|
309
|
+
clearInput.name = clearFieldName;
|
|
310
|
+
clearInput.value = '1';
|
|
311
|
+
form.appendChild(clearInput);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
285
319
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<form class="cache-clean-form" method="POST" action="{{&cacheCleanRoute}}">
|
|
2
2
|
<input type="hidden" name="routeId" value="{{routeId}}"/>
|
|
3
|
+
<input type="hidden" name="refererUrl" value="{{&refererUrl}}"/>
|
|
3
4
|
<button class="button button-warning cache-clean-btn" type="submit">{{&buttonText}}</button>
|
|
4
5
|
</form>
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
<button class="button button-primary button-submit" type="submit" form="edit-form" name="saveAction" value="saveAndGoBack">Save and go back</button>
|
|
7
7
|
<a class="button button-secondary button-back" href="{{&entityViewRoute}}">Cancel</a>
|
|
8
8
|
</div>
|
|
9
|
+
<div class="extra-actions">
|
|
10
|
+
{{&extraContent}}
|
|
11
|
+
</div>
|
|
9
12
|
<form name="edit-form" id="edit-form" action="{{&entitySaveRoute}}" method="post"{{&multipartFormData}}>
|
|
10
13
|
<input type="hidden" name="{{&idProperty}}" value="{{&idValue}}"/>
|
|
11
14
|
<div class="edit-field">
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
<p>{{&fieldValue}}</p>
|
|
1
|
+
{{#fieldValue}}<p class="upload-current-file" data-field="{{&fieldName}}"><button type="button" class="remove-upload-btn" data-field="{{&fieldName}}" title="REMOVE">X</button> - {{&fieldValue}}</p>{{/fieldValue}}
|
|
2
2
|
<input type="file" name="{{&fieldName}}" id="{{&fieldName}}"{{&required}}{{&fieldDisabled}}{{&multiple}}/>
|
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
<a class="button button-primary" href="{{&entityNewRoute}}">Create New</a>
|
|
5
5
|
<a class="button button-primary" href="{{&entityEditRoute}}">Edit</a>
|
|
6
6
|
<a class="button button-secondary" href="{{&entityListRoute}}">Back</a>
|
|
7
|
-
<form class="form-delete" name="delete-form-top-{{&id}}"
|
|
7
|
+
<form class="form-delete" name="delete-form-top-{{&id}}" action="{{&entityDeleteRoute}}" method="post">
|
|
8
8
|
<span>
|
|
9
9
|
<input type="hidden" name="ids[]" value="{{&id}}"/>
|
|
10
10
|
<button class="button button-danger" type="submit">Delete</button>
|
|
11
11
|
</span>
|
|
12
12
|
</form>
|
|
13
13
|
</div>
|
|
14
|
+
<div class="extra-actions">
|
|
15
|
+
{{&extraContent}}
|
|
16
|
+
</div>
|
|
14
17
|
{{#fields}}
|
|
15
18
|
<div class="view-field">
|
|
16
19
|
<span class="field-name">{{&name}}</span>
|
|
@@ -22,7 +25,7 @@
|
|
|
22
25
|
<a class="button button-primary" href="{{&entityNewRoute}}">Create New</a>
|
|
23
26
|
<a class="button button-primary" href="{{&entityEditRoute}}">Edit</a>
|
|
24
27
|
<a class="button button-secondary" href="{{&entityListRoute}}">Back</a>
|
|
25
|
-
<form class="form-delete" name="delete-form-{{&id}}"
|
|
28
|
+
<form class="form-delete" name="delete-form-{{&id}}" action="{{&entityDeleteRoute}}" method="post">
|
|
26
29
|
<span>
|
|
27
30
|
<input type="hidden" name="ids[]" value="{{&id}}"/>
|
|
28
31
|
<button class="button button-danger" type="submit">Delete</button>
|
|
@@ -473,6 +473,8 @@ class RouterContents
|
|
|
473
473
|
if(!shouldProcess){
|
|
474
474
|
continue;
|
|
475
475
|
}
|
|
476
|
+
let clearFieldParam = sc.get(req.body, 'clear_'+i, false);
|
|
477
|
+
let isExplicitClear = '1' === clearFieldParam;
|
|
476
478
|
let propertyUpdateValue = sc.get(req.body, i, null);
|
|
477
479
|
if('null' === propertyUpdateValue){
|
|
478
480
|
propertyUpdateValue = null;
|
|
@@ -480,7 +482,12 @@ class RouterContents
|
|
|
480
482
|
let propertyType = sc.get(property, 'type', 'string');
|
|
481
483
|
if(property.isUpload){
|
|
482
484
|
propertyType = 'upload';
|
|
483
|
-
|
|
485
|
+
if(isExplicitClear){
|
|
486
|
+
propertyUpdateValue = null;
|
|
487
|
+
}
|
|
488
|
+
if(!isExplicitClear){
|
|
489
|
+
propertyUpdateValue = this.prepareUploadPatchData(req, i, propertyUpdateValue, property);
|
|
490
|
+
}
|
|
484
491
|
}
|
|
485
492
|
if('boolean' === propertyType){
|
|
486
493
|
propertyUpdateValue = '1' === propertyUpdateValue || 'on' === propertyUpdateValue;
|
|
@@ -518,7 +525,7 @@ class RouterContents
|
|
|
518
525
|
continue;
|
|
519
526
|
}
|
|
520
527
|
}
|
|
521
|
-
if(!property.isUpload || (property.isUpload && !isNull)){
|
|
528
|
+
if(!property.isUpload || (property.isUpload && (!isNull || isExplicitClear))){
|
|
522
529
|
entityDataPatch[i] = propertyUpdateValue;
|
|
523
530
|
}
|
|
524
531
|
}
|
|
@@ -546,13 +553,13 @@ class RouterContents
|
|
|
546
553
|
if(resourceProperty.isArray){
|
|
547
554
|
fieldValue = fieldValue.split(resourceProperty.isArray).map((value) => {
|
|
548
555
|
let target = resourceProperty.isUpload ? ' target="_blank"' : '';
|
|
549
|
-
let fieldValuePart = resourceProperty.isUpload && resourceProperty.bucketPath
|
|
556
|
+
let fieldValuePart = resourceProperty.isUpload && resourceProperty.bucketPath && value
|
|
550
557
|
? resourceProperty.bucketPath+value
|
|
551
558
|
: value;
|
|
552
559
|
return {fieldValuePart, fieldOriginalValuePart: value, target};
|
|
553
560
|
});
|
|
554
561
|
}
|
|
555
|
-
if(!resourceProperty.isArray && resourceProperty.isUpload){
|
|
562
|
+
if(!resourceProperty.isArray && resourceProperty.isUpload && fieldValue){
|
|
556
563
|
fieldValue = resourceProperty.bucketPath+fieldValue;
|
|
557
564
|
}
|
|
558
565
|
}
|
|
@@ -87,7 +87,8 @@ class AddCacheButtonSubscriber
|
|
|
87
87
|
|
|
88
88
|
async generateCacheCleanButton(event)
|
|
89
89
|
{
|
|
90
|
-
|
|
90
|
+
let entityId = event.driverResource.id();
|
|
91
|
+
if('routes' !== entityId && 'cms_pages' !== entityId){
|
|
91
92
|
return false;
|
|
92
93
|
}
|
|
93
94
|
if(!event.loadedEntity){
|
|
@@ -98,6 +99,13 @@ class AddCacheButtonSubscriber
|
|
|
98
99
|
Logger.error('Missing loaded entity ID on AddCacheButtonSubscriber.');
|
|
99
100
|
return false;
|
|
100
101
|
}
|
|
102
|
+
let routeId = event.loadedEntity.id;
|
|
103
|
+
if('cms_pages' === entityId){
|
|
104
|
+
if(!event.loadedEntity.route_id){
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
routeId = event.loadedEntity.route_id;
|
|
108
|
+
}
|
|
101
109
|
if(!this.cacheCleanButton){
|
|
102
110
|
Logger.error('Cache clean button template content not found');
|
|
103
111
|
return '';
|
|
@@ -106,11 +114,13 @@ class AddCacheButtonSubscriber
|
|
|
106
114
|
Logger.error('Render callback not available for cache button');
|
|
107
115
|
return '';
|
|
108
116
|
}
|
|
117
|
+
let refererUrl = sc.get(event.req, 'originalUrl', '');
|
|
109
118
|
return await this.renderCallback(
|
|
110
119
|
this.cacheCleanButton,
|
|
111
120
|
{
|
|
112
121
|
cacheCleanRoute: this.cacheCleanRoute,
|
|
113
|
-
routeId:
|
|
122
|
+
routeId: routeId,
|
|
123
|
+
refererUrl: refererUrl,
|
|
114
124
|
buttonText: sc.get(this.translations.labels, 'cleanCache', 'cleanCache')
|
|
115
125
|
}
|
|
116
126
|
);
|
|
@@ -135,7 +145,11 @@ class AddCacheButtonSubscriber
|
|
|
135
145
|
buttonText: sc.get(this.translations.labels, 'clearAllCache', 'clearAllCache'),
|
|
136
146
|
clearAllCacheRoute: this.clearAllCacheRoute,
|
|
137
147
|
confirmTitle: sc.get(this.translations.labels, 'confirmClearCache', 'confirmClearCache'),
|
|
138
|
-
confirmMessage: sc.get(
|
|
148
|
+
confirmMessage: sc.get(
|
|
149
|
+
this.translations.messages,
|
|
150
|
+
'confirmClearCacheMessage',
|
|
151
|
+
'confirmClearCacheMessage'
|
|
152
|
+
),
|
|
139
153
|
warningText: sc.get(this.translations.labels, 'warning', 'warning'),
|
|
140
154
|
warningMessage: sc.get(this.translations.messages, 'clearCacheWarning', 'clearCacheWarning'),
|
|
141
155
|
cancelText: sc.get(this.translations.labels, 'cancel', 'cancel'),
|
|
@@ -15,6 +15,21 @@ class CacheManager
|
|
|
15
15
|
this.projectRoot = sc.get(props, 'projectRoot', './');
|
|
16
16
|
this.cacheBasePath = FileHandler.joinPaths(this.projectRoot, '.reldens_cms_cache');
|
|
17
17
|
this.enabled = sc.get(props, 'enabled', true);
|
|
18
|
+
this.domainMapping = sc.get(props, 'domainMapping', {});
|
|
19
|
+
this.reverseDomainMapping = this.buildReverseDomainMapping();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
buildReverseDomainMapping()
|
|
23
|
+
{
|
|
24
|
+
let reverse = {};
|
|
25
|
+
for(let domain in this.domainMapping){
|
|
26
|
+
let canonical = this.domainMapping[domain];
|
|
27
|
+
if(!reverse[canonical]){
|
|
28
|
+
reverse[canonical] = [];
|
|
29
|
+
}
|
|
30
|
+
reverse[canonical].push(domain);
|
|
31
|
+
}
|
|
32
|
+
return reverse;
|
|
18
33
|
}
|
|
19
34
|
|
|
20
35
|
generateCacheKey(domain, path)
|
|
@@ -93,22 +108,22 @@ class CacheManager
|
|
|
93
108
|
|
|
94
109
|
async delete(domain, path)
|
|
95
110
|
{
|
|
96
|
-
let cacheByDomains = this.
|
|
97
|
-
|
|
98
|
-
|
|
111
|
+
let cacheByDomains = this.resolveCacheDomains(domain);
|
|
112
|
+
if(0 === cacheByDomains.length){
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
for(let domainInfo of cacheByDomains){
|
|
116
|
+
let actualDomain = domainInfo.domain;
|
|
117
|
+
let allCacheFiles = this.findAllCacheFilesForPath(actualDomain, path);
|
|
99
118
|
if(0 === allCacheFiles.length){
|
|
100
|
-
let singleCacheInfo = this.generateCacheKey(
|
|
119
|
+
let singleCacheInfo = this.generateCacheKey(actualDomain, path);
|
|
101
120
|
if(!FileHandler.exists(singleCacheInfo.fullPath)){
|
|
102
|
-
Logger.debug('No cache files found for: '+path);
|
|
121
|
+
//Logger.debug('No cache files found for: '+path+' in domain: '+actualDomain);
|
|
103
122
|
continue;
|
|
104
123
|
}
|
|
105
124
|
allCacheFiles = [singleCacheInfo.fullPath];
|
|
106
125
|
}
|
|
107
126
|
for(let cacheFilePath of allCacheFiles){
|
|
108
|
-
if(!FileHandler.exists(cacheFilePath)){
|
|
109
|
-
Logger.debug('File does not exist: '+cacheFilePath);
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
127
|
if(!FileHandler.remove(cacheFilePath)){
|
|
113
128
|
Logger.error('Failed to delete cache file: '+cacheFilePath);
|
|
114
129
|
return false;
|
|
@@ -119,19 +134,22 @@ class CacheManager
|
|
|
119
134
|
return true;
|
|
120
135
|
}
|
|
121
136
|
|
|
122
|
-
|
|
137
|
+
resolveCacheDomains(domain)
|
|
123
138
|
{
|
|
124
139
|
let cacheByDomains = [];
|
|
125
|
-
if(domain
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
if(!domain || 'default' === domain){
|
|
141
|
+
if(!FileHandler.exists(this.cacheBasePath)){
|
|
142
|
+
return cacheByDomains;
|
|
143
|
+
}
|
|
144
|
+
let cachedDomainFolders = FileHandler.readFolder(this.cacheBasePath);
|
|
145
|
+
for(let cachedDomainFolder of cachedDomainFolders) {
|
|
146
|
+
cacheByDomains.push({domain: cachedDomainFolder});
|
|
147
|
+
}
|
|
130
148
|
return cacheByDomains;
|
|
131
149
|
}
|
|
132
|
-
let
|
|
133
|
-
for(let
|
|
134
|
-
cacheByDomains.push({domain:
|
|
150
|
+
let domainsToDelete = this.reverseDomainMapping[domain] || [domain];
|
|
151
|
+
for(let mappedDomain of domainsToDelete){
|
|
152
|
+
cacheByDomains.push({domain: mappedDomain});
|
|
135
153
|
}
|
|
136
154
|
return cacheByDomains;
|
|
137
155
|
}
|
|
@@ -77,7 +77,10 @@ class CacheRoutesHandler
|
|
|
77
77
|
if(!cleanResult){
|
|
78
78
|
return res.json({error: 'Failed to clean cache'});
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
let defaultRedirect = this.rootPath+'/routes/view?id='+routeId;
|
|
81
|
+
let refererUrl = sc.get(req.body, 'refererUrl', defaultRedirect);
|
|
82
|
+
let redirectUrl = refererUrl+(refererUrl.includes('?') ? '&' : '?')+'result=success';
|
|
83
|
+
return res.redirect(redirectUrl);
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
async processClearAllCache(req, res)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - EntitiesConfigProcessor
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { Logger, sc } = require('@reldens/utils');
|
|
8
|
+
|
|
9
|
+
class EntitiesConfigProcessor
|
|
10
|
+
{
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {Object} baseEntitiesConfig
|
|
14
|
+
* @param {Object} overrides
|
|
15
|
+
* @param {Object} props
|
|
16
|
+
* @returns {Object}
|
|
17
|
+
*/
|
|
18
|
+
static applyOverrides(baseEntitiesConfig, overrides, props = {})
|
|
19
|
+
{
|
|
20
|
+
if(!overrides || 'object' !== typeof overrides || 0 === Object.keys(overrides).length){
|
|
21
|
+
return baseEntitiesConfig;
|
|
22
|
+
}
|
|
23
|
+
let mergedConfig = sc.deepMergeProperties({}, baseEntitiesConfig);
|
|
24
|
+
for(let entityKey of Object.keys(overrides)){
|
|
25
|
+
let override = overrides[entityKey];
|
|
26
|
+
if(!override){
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
let baseConfig = mergedConfig[entityKey] || {};
|
|
30
|
+
mergedConfig[entityKey] = this.applyEntityOverride(entityKey, baseConfig, override, props);
|
|
31
|
+
}
|
|
32
|
+
return mergedConfig;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} entityKey
|
|
37
|
+
* @param {Object} baseConfig
|
|
38
|
+
* @param {*} override
|
|
39
|
+
* @param {Object} props
|
|
40
|
+
* @returns {Object}
|
|
41
|
+
*/
|
|
42
|
+
static applyEntityOverride(entityKey, baseConfig, override, props)
|
|
43
|
+
{
|
|
44
|
+
if('function' === typeof override && sc.hasOwn(override, 'propertiesConfig')){
|
|
45
|
+
return override.propertiesConfig(baseConfig, props);
|
|
46
|
+
}
|
|
47
|
+
if(sc.isFunction(override)){
|
|
48
|
+
return override(baseConfig, props);
|
|
49
|
+
}
|
|
50
|
+
if('object' === typeof override){
|
|
51
|
+
return sc.deepMergeProperties({}, baseConfig, override);
|
|
52
|
+
}
|
|
53
|
+
Logger.warning('Invalid override type for entity: '+entityKey);
|
|
54
|
+
return baseConfig;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports.EntitiesConfigProcessor = EntitiesConfigProcessor;
|
package/lib/manager.js
CHANGED
|
@@ -13,6 +13,7 @@ const { AllowedExtensions } = require('./allowed-extensions');
|
|
|
13
13
|
const { TemplatesToPathMapper } = require('./templates-to-path-mapper');
|
|
14
14
|
const { AdminEntitiesGenerator } = require('./admin-entities-generator');
|
|
15
15
|
const { LoadedEntitiesProcessor } = require('./loaded-entities-processor');
|
|
16
|
+
const { EntitiesConfigProcessor } = require('./entities-config-processor');
|
|
16
17
|
const { AdminManager } = require('./admin-manager');
|
|
17
18
|
const { CmsPagesRouteManager } = require('./cms-pages-route-manager');
|
|
18
19
|
const { Installer } = require('./installer');
|
|
@@ -41,6 +42,7 @@ class Manager
|
|
|
41
42
|
this.rawRegisteredEntities = sc.get(props, 'rawRegisteredEntities', {});
|
|
42
43
|
this.entitiesTranslations = sc.get(props, 'entitiesTranslations', {});
|
|
43
44
|
this.entitiesConfig = sc.get(props, 'entitiesConfig', {});
|
|
45
|
+
this.entitiesConfigOverride = sc.get(props, 'entitiesConfigOverride', {});
|
|
44
46
|
this.processedEntities = sc.get(props, 'processedEntities', {});
|
|
45
47
|
this.entityAccess = sc.get(props, 'entityAccess', {});
|
|
46
48
|
this.authenticationMethod = sc.get(props, 'authenticationMethod', 'db-users');
|
|
@@ -92,7 +94,11 @@ class Manager
|
|
|
92
94
|
this.developmentExternalDomains = sc.get(props, 'developmentExternalDomains', {});
|
|
93
95
|
this.appServerFactory = new AppServerFactory();
|
|
94
96
|
this.adminEntitiesGenerator = new AdminEntitiesGenerator();
|
|
95
|
-
this.cacheManager = new CacheManager({
|
|
97
|
+
this.cacheManager = new CacheManager({
|
|
98
|
+
projectRoot: this.projectRoot,
|
|
99
|
+
enabled: this.cache,
|
|
100
|
+
domainMapping: this.domainMapping
|
|
101
|
+
});
|
|
96
102
|
this.templateReloader = new TemplateReloader({
|
|
97
103
|
reloadTime: this.reloadTime,
|
|
98
104
|
events: this.events,
|
|
@@ -439,10 +445,15 @@ class Manager
|
|
|
439
445
|
loadProcessedEntities()
|
|
440
446
|
{
|
|
441
447
|
if(0 === Object.keys(this.processedEntities).length){
|
|
448
|
+
let mergedConfig = EntitiesConfigProcessor.applyOverrides(
|
|
449
|
+
this.entitiesConfig,
|
|
450
|
+
this.entitiesConfigOverride,
|
|
451
|
+
{projectRoot: this.projectRoot}
|
|
452
|
+
);
|
|
442
453
|
this.processedEntities = LoadedEntitiesProcessor.process(
|
|
443
454
|
this.rawRegisteredEntities,
|
|
444
455
|
this.entitiesTranslations,
|
|
445
|
-
|
|
456
|
+
mergedConfig
|
|
446
457
|
);
|
|
447
458
|
}
|
|
448
459
|
if(!this.processedEntities?.entities){
|
package/lib/template-reloader.js
CHANGED
|
@@ -247,12 +247,12 @@ class TemplateReloader
|
|
|
247
247
|
if(!this.shouldReloadAdminTemplates(this.mappedAdminTemplates)){
|
|
248
248
|
return false;
|
|
249
249
|
}
|
|
250
|
-
let
|
|
251
|
-
if(!
|
|
250
|
+
let adminFilesContents = await this.adminTemplatesLoader.fetchAdminFilesContents(this.mappedAdminTemplates);
|
|
251
|
+
if(!adminFilesContents){
|
|
252
252
|
return false;
|
|
253
253
|
}
|
|
254
254
|
this.markTemplatesAsReloaded(this.mappedAdminTemplates);
|
|
255
|
-
return
|
|
255
|
+
return adminFilesContents;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
async checkAndReloadFrontendTemplates()
|
|
@@ -265,16 +265,16 @@ class TemplateReloader
|
|
|
265
265
|
return true;
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
async updateAdminContentsAfterReload(
|
|
268
|
+
async updateAdminContentsAfterReload(adminFilesContents, adminManager)
|
|
269
269
|
{
|
|
270
|
-
if(!
|
|
270
|
+
if(!adminFilesContents || !adminManager){
|
|
271
271
|
return false;
|
|
272
272
|
}
|
|
273
|
-
adminManager.adminFilesContents =
|
|
274
|
-
adminManager.contentsBuilder.adminFilesContents =
|
|
275
|
-
adminManager.routerContents.adminFilesContents =
|
|
276
|
-
adminManager.addCacheButtonSubscriber.cacheCleanButton =
|
|
277
|
-
adminManager.addCacheButtonSubscriber.clearAllCacheButton =
|
|
273
|
+
adminManager.adminFilesContents = adminFilesContents;
|
|
274
|
+
adminManager.contentsBuilder.adminFilesContents = adminFilesContents;
|
|
275
|
+
adminManager.routerContents.adminFilesContents = adminFilesContents;
|
|
276
|
+
adminManager.addCacheButtonSubscriber.cacheCleanButton = adminFilesContents.cacheCleanButton;
|
|
277
|
+
adminManager.addCacheButtonSubscriber.clearAllCacheButton = adminFilesContents.clearAllCacheButton;
|
|
278
278
|
await adminManager.contentsBuilder.buildAdminContents();
|
|
279
279
|
return true;
|
|
280
280
|
}
|
package/package.json
CHANGED
package/templates/page.html
CHANGED
|
@@ -7,19 +7,24 @@
|
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes, viewport-fit=cover"/>
|
|
8
8
|
<meta name="robots" content="{{meta_robots}}">
|
|
9
9
|
<meta name="theme-color" content="{{meta_theme_color}}"/>
|
|
10
|
+
<meta property="og:title" content="{{meta_og_title}}"/>
|
|
11
|
+
<meta property="og:type" content="website"/>
|
|
12
|
+
<meta property="og:site_name" content="{{siteHandle}}"/>
|
|
13
|
+
<meta property="og:url" content="{{currentRequest.fullUrl}}"/>
|
|
14
|
+
<meta property="og:locale" content="{{locale}}"/>
|
|
10
15
|
{{#meta_description}}
|
|
11
16
|
<meta name="description" content="{{meta_description}}"/>
|
|
17
|
+
<meta property="og:description" content="{{meta_description}}"/>
|
|
12
18
|
{{/meta_description}}
|
|
13
|
-
<meta property="og:title" content="{{meta_og_title}}"/>
|
|
14
|
-
{{#meta_og_description}}
|
|
15
|
-
<meta property="og:description" content="{{meta_og_description}}"/>
|
|
16
|
-
{{/meta_og_description}}
|
|
17
19
|
{{#meta_og_image}}
|
|
18
20
|
<meta property="og:image" content="{{meta_og_image}}"/>
|
|
19
21
|
{{/meta_og_image}}
|
|
20
22
|
{{#meta_twitter_card_type}}
|
|
21
23
|
<meta name="twitter:card" content="{{meta_twitter_card_type}}"/>
|
|
22
24
|
{{/meta_twitter_card_type}}
|
|
25
|
+
{{#article_author}}
|
|
26
|
+
<meta property="article:author" content="{{article_author}}"/>
|
|
27
|
+
{{/article_author}}
|
|
23
28
|
{{#publish_date}}
|
|
24
29
|
<meta name="publish-date" content="{{publish_date}}"/>
|
|
25
30
|
{{/publish_date}}
|
|
@@ -34,14 +39,13 @@
|
|
|
34
39
|
<link rel="icon" href="[asset(/favicons/default/favicon.ico)]" type="image/x-icon"/>
|
|
35
40
|
<link rel="shortcut icon" href="[asset(/favicons/default/favicon.ico)]" type="image/x-icon">
|
|
36
41
|
<link rel="manifest" href="[asset(/favicons/default/site.webmanifest)]"/>
|
|
37
|
-
|
|
38
|
-
<link rel="stylesheet" href="[url(/css/styles.css)]"/>
|
|
39
|
-
<script defer src="[url(/js/cookie-consent.js)]"></script>
|
|
42
|
+
<link rel="stylesheet" href="[cdn(/css/styles.css)]"/>
|
|
40
43
|
</head>
|
|
41
44
|
<body class="{{siteHandle}}">
|
|
42
|
-
{{&content}}
|
|
43
|
-
{{>cookie-consent}}
|
|
44
|
-
<script defer type="text/javascript" src="[
|
|
45
|
-
<script defer type="text/javascript" src="[
|
|
45
|
+
{{&content}}
|
|
46
|
+
{{>cookie-consent}}
|
|
47
|
+
<script defer type="text/javascript" src="[cdn(/js/functions.js)]"></script>
|
|
48
|
+
<script defer type="text/javascript" src="[cdn(/js/scripts.js)]"></script>
|
|
49
|
+
<script defer src="[cdn(/js/cookie-consent.js)]"></script>
|
|
46
50
|
</body>
|
|
47
51
|
</html>
|