@reldens/cms 0.58.0 → 0.59.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/admin/assets/admin/alert.png +0 -0
- package/admin/reldens-admin-client.css +2 -0
- package/admin/reldens-admin-client.js +49 -15
- package/admin/templates/fields/edit/file.html +16 -1
- package/admin/templates/view.html +2 -2
- package/lib/admin-manager/contents-builder.js +2 -1
- package/lib/admin-manager/router-contents.js +84 -28
- package/package.json +2 -2
|
Binary file
|
|
@@ -212,7 +212,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
|
|
212
212
|
}
|
|
213
213
|
let allFilters = document.querySelectorAll('.filters-toggle-content .filter input');
|
|
214
214
|
for(let filterInput of allFilters){
|
|
215
|
-
let filterName = filterInput.name.replace(
|
|
215
|
+
let filterName = filterInput.name.replace(/^filters\[/, '').replace(/\]$/, '');
|
|
216
216
|
let sortFormFilterInput = sortForm.querySelector('input[data-filter-key="'+filterName+'"]');
|
|
217
217
|
if(sortFormFilterInput){
|
|
218
218
|
sortFormFilterInput.value = filterInput.value;
|
|
@@ -339,26 +339,60 @@ window.addEventListener('DOMContentLoaded', () => {
|
|
|
339
339
|
button.addEventListener('click', (event) => {
|
|
340
340
|
event.preventDefault();
|
|
341
341
|
let fieldName = button.getAttribute('data-field');
|
|
342
|
+
let fileName = button.getAttribute('data-filename');
|
|
342
343
|
let fileInput = document.getElementById(fieldName);
|
|
343
|
-
let
|
|
344
|
+
let form = fileInput?.closest('form');
|
|
345
|
+
if(!fileInput){
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if(!form){
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
let currentFileDisplay = button.closest('.upload-current-file');
|
|
352
|
+
let container = button.closest('.upload-files-container');
|
|
353
|
+
let isRequired = container && 'true' === container.dataset.required;
|
|
354
|
+
if(isRequired){
|
|
355
|
+
let remainingFiles = container.querySelectorAll('.upload-current-file');
|
|
356
|
+
if(2 === remainingFiles.length){
|
|
357
|
+
let allRemoveButtons = container.querySelectorAll('.remove-upload-btn');
|
|
358
|
+
for(let removeBtn of allRemoveButtons){
|
|
359
|
+
removeBtn.remove();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
344
363
|
if(currentFileDisplay){
|
|
345
|
-
currentFileDisplay.
|
|
364
|
+
currentFileDisplay.remove();
|
|
346
365
|
}
|
|
347
|
-
if(
|
|
348
|
-
|
|
349
|
-
let
|
|
350
|
-
if(
|
|
351
|
-
let
|
|
352
|
-
let
|
|
353
|
-
if(
|
|
354
|
-
|
|
355
|
-
clearInput.type = 'hidden';
|
|
356
|
-
clearInput.name = clearFieldName;
|
|
357
|
-
clearInput.value = '1';
|
|
358
|
-
form.appendChild(clearInput);
|
|
366
|
+
if(fileName){
|
|
367
|
+
let hiddenFieldName = 'removed_'+fieldName;
|
|
368
|
+
let existingHiddenInput = form.querySelector('input[name="'+hiddenFieldName+'"]');
|
|
369
|
+
if(existingHiddenInput){
|
|
370
|
+
let currentValue = existingHiddenInput.value;
|
|
371
|
+
let filesArray = currentValue ? currentValue.split(',') : [];
|
|
372
|
+
if(-1 === filesArray.indexOf(fileName)){
|
|
373
|
+
filesArray.push(fileName);
|
|
359
374
|
}
|
|
375
|
+
existingHiddenInput.value = filesArray.join(',');
|
|
376
|
+
return;
|
|
360
377
|
}
|
|
378
|
+
let hiddenInput = document.createElement('input');
|
|
379
|
+
hiddenInput.type = 'hidden';
|
|
380
|
+
hiddenInput.name = hiddenFieldName;
|
|
381
|
+
hiddenInput.value = fileName;
|
|
382
|
+
form.appendChild(hiddenInput);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
fileInput.value = '';
|
|
386
|
+
let clearFieldName = 'clear_'+fieldName;
|
|
387
|
+
let existingClearInput = form.querySelector('input[name="'+clearFieldName+'"]');
|
|
388
|
+
if(existingClearInput){
|
|
389
|
+
return;
|
|
361
390
|
}
|
|
391
|
+
let clearInput = document.createElement('input');
|
|
392
|
+
clearInput.type = 'hidden';
|
|
393
|
+
clearInput.name = clearFieldName;
|
|
394
|
+
clearInput.value = '1';
|
|
395
|
+
form.appendChild(clearInput);
|
|
362
396
|
});
|
|
363
397
|
}
|
|
364
398
|
}
|
|
@@ -1,2 +1,17 @@
|
|
|
1
|
-
{{
|
|
1
|
+
{{{renderedAlert}}}
|
|
2
|
+
{{{renderedFiles}}}
|
|
3
|
+
{{#fieldValue}}
|
|
4
|
+
{{^renderedFiles}}
|
|
5
|
+
<div class="upload-files-container" data-field="{{&fieldName}}" data-required="{{isRequired}}">
|
|
6
|
+
{{#filesArray}}
|
|
7
|
+
<p class="upload-current-file" data-field="{{&fieldName}}" data-filename="{{&filename}}">
|
|
8
|
+
{{#isRemovable}}
|
|
9
|
+
<button type="button" class="remove-upload-btn" data-field="{{&fieldName}}" data-filename="{{&filename}}" title="REMOVE">X</button> -
|
|
10
|
+
{{/isRemovable}}
|
|
11
|
+
{{&filename}}
|
|
12
|
+
</p>
|
|
13
|
+
{{/filesArray}}
|
|
14
|
+
</div>
|
|
15
|
+
{{/renderedFiles}}
|
|
16
|
+
{{/fieldValue}}
|
|
2
17
|
<input type="file" name="{{&fieldName}}" id="{{&fieldName}}"{{&required}}{{&fieldDisabled}}{{&multiple}}/>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
</form>
|
|
13
13
|
</div>
|
|
14
14
|
<div class="extra-actions">
|
|
15
|
-
{{&
|
|
15
|
+
{{&extraContentTop}}
|
|
16
16
|
</div>
|
|
17
17
|
{{#fields}}
|
|
18
18
|
<div class="view-field">
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
</span>
|
|
33
33
|
</form>
|
|
34
34
|
<div class="extra-actions">
|
|
35
|
-
{{&
|
|
35
|
+
{{&extraContentBottom}}
|
|
36
36
|
</div>
|
|
37
37
|
</div>
|
|
38
38
|
</div>
|
|
@@ -185,7 +185,8 @@ class ContentsBuilder
|
|
|
185
185
|
id: '{{&id}}',
|
|
186
186
|
entityEditRoute: '{{&entityEditRoute}}',
|
|
187
187
|
entityNewRoute: '{{&entityNewRoute}}',
|
|
188
|
-
|
|
188
|
+
extraContentTop: '{{&extraContentForViewTop}}',
|
|
189
|
+
extraContentBottom: '{{&extraContentForViewBottom}}'+extraContentForView,
|
|
189
190
|
extraFormContent: '{{&extraFormContentForView}}'+extraFormContentForView
|
|
190
191
|
}
|
|
191
192
|
),
|
|
@@ -197,6 +197,8 @@ class RouterContents
|
|
|
197
197
|
await this.emitEvent('adminEntityExtraData', extraDataEvent);
|
|
198
198
|
renderedViewProperties.entitySerializedData = JSON.stringify(extraDataEvent.entitySerializedData)
|
|
199
199
|
.replace(/"/g, '"');
|
|
200
|
+
renderedViewProperties.extraContentForViewTop = '';
|
|
201
|
+
renderedViewProperties.extraContentForViewBottom = '';
|
|
200
202
|
await this.emitEvent('reldens.adminViewPropertiesPopulation', {
|
|
201
203
|
idProperty,
|
|
202
204
|
req,
|
|
@@ -228,26 +230,53 @@ class RouterContents
|
|
|
228
230
|
req,
|
|
229
231
|
driverResource,
|
|
230
232
|
renderedEditProperties,
|
|
231
|
-
loadedEntity
|
|
233
|
+
loadedEntity,
|
|
234
|
+
entityId: driverResource.id(),
|
|
235
|
+
entityData: loadedEntity
|
|
232
236
|
});
|
|
233
237
|
for(let propertyKey of Object.keys(driverResource.options.properties)){
|
|
234
238
|
let property = driverResource.options.properties[propertyKey];
|
|
235
239
|
let fieldDisabled = -1 === driverResource.options.editProperties.indexOf(propertyKey);
|
|
240
|
+
let fieldValue = await this.generatePropertyEditRenderedValue(
|
|
241
|
+
loadedEntity,
|
|
242
|
+
propertyKey,
|
|
243
|
+
property,
|
|
244
|
+
fieldDisabled
|
|
245
|
+
);
|
|
246
|
+
let templateData = {
|
|
247
|
+
fieldName: propertyKey,
|
|
248
|
+
fieldValue,
|
|
249
|
+
fieldDisabled: fieldDisabled ? ' disabled="disabled"' : '',
|
|
250
|
+
required: (!property.isUpload || !loadedEntity) && property.isRequired ? ' required="required"' : '',
|
|
251
|
+
multiple: property.isArray ? ' multiple="multiple"' : '',
|
|
252
|
+
inputType: this.getInputType(property, propertyKey, fieldDisabled),
|
|
253
|
+
isArray: property.isArray || '',
|
|
254
|
+
arraySeparator: property.isArray || '',
|
|
255
|
+
isRequired: property.isRequired || false
|
|
256
|
+
};
|
|
257
|
+
if(property.isUpload && fieldValue && '' !== fieldValue){
|
|
258
|
+
let filesArray = property.isArray ? fieldValue.split(property.isArray) : [fieldValue];
|
|
259
|
+
templateData.filesArray = filesArray.map((filename) => {
|
|
260
|
+
return {
|
|
261
|
+
filename,
|
|
262
|
+
isRemovable: !property.isRequired || filesArray.length > 1
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
await this.emitEvent('reldens.adminBeforeFieldRender', {
|
|
267
|
+
req,
|
|
268
|
+
driverResource,
|
|
269
|
+
propertyKey,
|
|
270
|
+
property,
|
|
271
|
+
templateData,
|
|
272
|
+
loadedEntity,
|
|
273
|
+
renderedEditProperties,
|
|
274
|
+
adminContentsRender: this.adminContentsRender.bind(this),
|
|
275
|
+
adminFilesContents: this.adminFilesContents
|
|
276
|
+
});
|
|
236
277
|
renderedEditProperties[propertyKey] = await this.adminContentsRender(
|
|
237
278
|
this.adminFilesContents.fields.edit[this.propertyType(property, 'edit')],
|
|
238
|
-
|
|
239
|
-
fieldName: propertyKey,
|
|
240
|
-
fieldValue: await this.generatePropertyEditRenderedValue(
|
|
241
|
-
loadedEntity,
|
|
242
|
-
propertyKey,
|
|
243
|
-
property,
|
|
244
|
-
fieldDisabled
|
|
245
|
-
),
|
|
246
|
-
fieldDisabled: fieldDisabled ? ' disabled="disabled"' : '',
|
|
247
|
-
required: (!property.isUpload || !loadedEntity) && property.isRequired ? ' required="required"' : '',
|
|
248
|
-
multiple: property.isArray ? ' multiple="multiple"' : '',
|
|
249
|
-
inputType: this.getInputType(property, propertyKey, fieldDisabled)
|
|
250
|
-
}
|
|
279
|
+
templateData
|
|
251
280
|
);
|
|
252
281
|
}
|
|
253
282
|
return await this.adminContentsRenderRoute(
|
|
@@ -282,6 +311,10 @@ class RouterContents
|
|
|
282
311
|
|
|
283
312
|
async processSaveEntity(req, res, driverResource, entityPath)
|
|
284
313
|
{
|
|
314
|
+
let idProperty = this.fetchEntityIdPropertyKey(driverResource);
|
|
315
|
+
let id = (req?.body[idProperty] || '');
|
|
316
|
+
let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
|
|
317
|
+
let loadedEntity = id ? await this.loadEntityById(driverResource, id) : null;
|
|
285
318
|
let uploadProperties = this.fetchUploadProperties(driverResource);
|
|
286
319
|
if(0 < Object.keys(uploadProperties).length){
|
|
287
320
|
if(this.uploaderFactory?.error?.message){
|
|
@@ -289,22 +322,20 @@ class RouterContents
|
|
|
289
322
|
return this.rootPath+'/'+entityPath+'?result=uploadError&error='
|
|
290
323
|
+encodeURIComponent(this.uploaderFactory.error.message);
|
|
291
324
|
}
|
|
292
|
-
let uploadValidationResult = this.validateUploadedFiles(req, uploadProperties);
|
|
325
|
+
let uploadValidationResult = this.validateUploadedFiles(req, uploadProperties, loadedEntity);
|
|
293
326
|
if(!uploadValidationResult.success){
|
|
294
327
|
Logger.error('Upload validation failed', uploadValidationResult.error);
|
|
295
328
|
return this.rootPath+'/'+entityPath+'?result=uploadValidationError&error='
|
|
296
329
|
+encodeURIComponent(uploadValidationResult.error.message);
|
|
297
330
|
}
|
|
298
331
|
}
|
|
299
|
-
let idProperty = this.fetchEntityIdPropertyKey(driverResource);
|
|
300
|
-
let id = (req?.body[idProperty] || '');
|
|
301
|
-
let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
|
|
302
332
|
let entityDataPatch = this.preparePatchData(
|
|
303
333
|
driverResource,
|
|
304
334
|
idProperty,
|
|
305
335
|
req,
|
|
306
336
|
driverResource.options.properties,
|
|
307
|
-
id
|
|
337
|
+
id,
|
|
338
|
+
loadedEntity
|
|
308
339
|
);
|
|
309
340
|
if(!entityDataPatch){
|
|
310
341
|
Logger.error('Bad patch data.', entityDataPatch);
|
|
@@ -351,14 +382,21 @@ class RouterContents
|
|
|
351
382
|
}
|
|
352
383
|
}
|
|
353
384
|
|
|
354
|
-
validateUploadedFiles(req, uploadProperties)
|
|
385
|
+
validateUploadedFiles(req, uploadProperties, loadedEntity)
|
|
355
386
|
{
|
|
356
387
|
for(let uploadPropertyKey of Object.keys(uploadProperties)){
|
|
357
388
|
let property = uploadProperties[uploadPropertyKey];
|
|
358
389
|
let uploadedFiles = req.files?.[uploadPropertyKey];
|
|
359
390
|
if(!uploadedFiles || 0 === uploadedFiles.length){
|
|
360
391
|
if(property.isRequired){
|
|
361
|
-
|
|
392
|
+
let isExplicitClear = '1' === sc.get(req.body, 'clear_'+uploadPropertyKey, false);
|
|
393
|
+
if(isExplicitClear){
|
|
394
|
+
return {success: false, error: {message: 'Required file upload missing: '+uploadPropertyKey}};
|
|
395
|
+
}
|
|
396
|
+
let existingValue = loadedEntity ? sc.get(loadedEntity, uploadPropertyKey, '') : '';
|
|
397
|
+
if(!existingValue || '' === existingValue){
|
|
398
|
+
return {success: false, error: {message: 'Required file upload missing: '+uploadPropertyKey}};
|
|
399
|
+
}
|
|
362
400
|
}
|
|
363
401
|
continue;
|
|
364
402
|
}
|
|
@@ -484,7 +522,7 @@ class RouterContents
|
|
|
484
522
|
}
|
|
485
523
|
}
|
|
486
524
|
|
|
487
|
-
preparePatchData(driverResource, idProperty, req, resourceProperties, id)
|
|
525
|
+
preparePatchData(driverResource, idProperty, req, resourceProperties, id, loadedEntity)
|
|
488
526
|
{
|
|
489
527
|
let entityDataPatch = {};
|
|
490
528
|
for(let i of Object.keys(resourceProperties)){
|
|
@@ -512,7 +550,8 @@ class RouterContents
|
|
|
512
550
|
propertyUpdateValue = null;
|
|
513
551
|
}
|
|
514
552
|
if(!isExplicitClear){
|
|
515
|
-
|
|
553
|
+
let currentValue = loadedEntity ? sc.get(loadedEntity, i, '') : '';
|
|
554
|
+
propertyUpdateValue = this.prepareUploadPatchData(req, i, currentValue, property);
|
|
516
555
|
}
|
|
517
556
|
}
|
|
518
557
|
if('boolean' === propertyType){
|
|
@@ -539,7 +578,6 @@ class RouterContents
|
|
|
539
578
|
}
|
|
540
579
|
let isUploadCreate = property.isUpload && !id;
|
|
541
580
|
if(property.isRequired && (isNull || isEmpty) && (!property.isUpload || isUploadCreate)){
|
|
542
|
-
// missing required fields would break the update:
|
|
543
581
|
Logger.critical(
|
|
544
582
|
'Missing property on prepare patch data.',
|
|
545
583
|
{propertyKey: i, config: property, propertyUpdateValue, entity: driverResource.entityKey}
|
|
@@ -562,14 +600,32 @@ class RouterContents
|
|
|
562
600
|
prepareUploadPatchData(req, i, propertyUpdateValue, property)
|
|
563
601
|
{
|
|
564
602
|
let filesData = sc.get(req.files, i, null);
|
|
565
|
-
|
|
603
|
+
let removedFiles = sc.get(req.body, 'removed_'+i, '');
|
|
604
|
+
let existingFiles = propertyUpdateValue || '';
|
|
605
|
+
if(property.isArray && removedFiles && '' !== removedFiles){
|
|
606
|
+
let removedFilesArray = removedFiles.split(',');
|
|
607
|
+
let existingFilesArray = existingFiles ? existingFiles.split(property.isArray) : [];
|
|
608
|
+
existingFilesArray = existingFilesArray.filter(file => {
|
|
609
|
+
return -1 === removedFilesArray.indexOf(file);
|
|
610
|
+
});
|
|
611
|
+
existingFiles = existingFilesArray.join(property.isArray);
|
|
612
|
+
}
|
|
613
|
+
if(!filesData || 0 === filesData.length){
|
|
614
|
+
if(removedFiles && '' !== removedFiles){
|
|
615
|
+
return existingFiles;
|
|
616
|
+
}
|
|
566
617
|
return null;
|
|
567
618
|
}
|
|
568
|
-
let
|
|
619
|
+
let newFileNames = [];
|
|
569
620
|
for(let file of filesData){
|
|
570
|
-
|
|
621
|
+
newFileNames.push(file.filename);
|
|
622
|
+
}
|
|
623
|
+
if(property.isArray && existingFiles && '' !== existingFiles){
|
|
624
|
+
let existingFilesArray = existingFiles.split(property.isArray);
|
|
625
|
+
let allFiles = existingFilesArray.concat(newFileNames);
|
|
626
|
+
return allFiles.join(property.isArray);
|
|
571
627
|
}
|
|
572
|
-
return
|
|
628
|
+
return newFileNames.join(property.isArray);
|
|
573
629
|
}
|
|
574
630
|
|
|
575
631
|
async generatePropertyRenderedValue(fieldValue, fieldName, resourceProperty, templateType)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reldens/cms",
|
|
3
3
|
"scope": "@reldens",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.59.0",
|
|
5
5
|
"description": "Reldens - CMS",
|
|
6
6
|
"author": "Damian A. Pastorini",
|
|
7
7
|
"license": "MIT",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@reldens/server-utils": "^0.46.0",
|
|
38
38
|
"@reldens/storage": "^0.91.0",
|
|
39
39
|
"@reldens/utils": "^0.54.0",
|
|
40
|
-
"dotenv": "17.
|
|
40
|
+
"dotenv": "17.3.1",
|
|
41
41
|
"mustache": "4.2.0"
|
|
42
42
|
}
|
|
43
43
|
}
|