@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.
Binary file
@@ -832,6 +832,8 @@
832
832
  .extra-content-container, .default-room-container {
833
833
  display: flex;
834
834
  flex-direction: column;
835
+ width: 100%;
836
+ box-sizing: border-box;
835
837
  padding: 1rem 0;
836
838
  margin-top: 1rem;
837
839
  text-align: left;
@@ -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('filters[', '').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 currentFileDisplay = document.querySelector('.upload-current-file[data-field="'+fieldName+'"]');
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.style.display = 'none';
364
+ currentFileDisplay.remove();
346
365
  }
347
- if(fileInput){
348
- fileInput.value = '';
349
- let form = fileInput.closest('form');
350
- if(form){
351
- let clearFieldName = 'clear_'+fieldName;
352
- let existingClearInput = form.querySelector('input[name="'+clearFieldName+'"]');
353
- if(!existingClearInput){
354
- let clearInput = document.createElement('input');
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
- {{#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}}
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
- {{&extraContent}}
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
- {{&extraContent}}
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
- extraContent: '{{&extraContentForView}}'+extraContentForView,
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, '&quot;');
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
- return {success: false, error: {message: 'Required file upload missing: '+uploadPropertyKey}};
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
- propertyUpdateValue = this.prepareUploadPatchData(req, i, propertyUpdateValue, property);
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
- if(null === filesData){
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 fileNames = [];
619
+ let newFileNames = [];
569
620
  for(let file of filesData){
570
- fileNames.push(file.filename);
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 fileNames.join(property.isArray);
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.58.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.2.4",
40
+ "dotenv": "17.3.1",
41
41
  "mustache": "4.2.0"
42
42
  }
43
43
  }