@reldens/cms 0.57.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;
@@ -177,8 +177,7 @@ window.addEventListener('DOMContentLoaded', () => {
177
177
  params.set('sortBy', columnName);
178
178
  params.set('sortDirection', sortDirection);
179
179
  }
180
- let newUrl = url.pathname+'?'+params;
181
- window.location.href = newUrl;
180
+ window.location.href = url.pathname+'?'+params;
182
181
  return false;
183
182
  });
184
183
  }
@@ -190,44 +189,35 @@ window.addEventListener('DOMContentLoaded', () => {
190
189
  if(sortableHeaders){
191
190
  for(let header of sortableHeaders){
192
191
  header.addEventListener('click', () => {
192
+ let sortForm = header.querySelector('.sort-form');
193
+ if(!sortForm){
194
+ return;
195
+ }
193
196
  let columnName = header.getAttribute('data-column');
194
- let currentSortDirection = header.classList.contains('sorted-asc') ? 'asc' : header.classList.contains('sorted-desc') ? 'desc' : '';
197
+ let currentSortDirection = header.classList.contains('sorted-asc')
198
+ ? 'asc'
199
+ : header.classList.contains('sorted-desc') ? 'desc' : '';
195
200
  let newSortDirection = 'asc';
196
201
  if('asc' === currentSortDirection){
197
202
  newSortDirection = 'desc';
198
203
  }
199
- let sortForm = document.createElement('form');
200
- sortForm.method = 'POST';
201
- sortForm.action = window.location.pathname;
202
- let sortByInput = document.createElement('input');
203
- sortByInput.type = 'hidden';
204
- sortByInput.name = 'sortBy';
204
+ let sortByInput = sortForm.querySelector('input[name="sortBy"]');
205
+ let sortDirectionInput = sortForm.querySelector('input[name="sortDirection"]');
205
206
  sortByInput.value = columnName;
206
- sortForm.appendChild(sortByInput);
207
- let sortDirectionInput = document.createElement('input');
208
- sortDirectionInput.type = 'hidden';
209
- sortDirectionInput.name = 'sortDirection';
210
207
  sortDirectionInput.value = newSortDirection;
211
- sortForm.appendChild(sortDirectionInput);
212
208
  let entitySearchInput = document.querySelector('#entityFilterTerm');
213
- if(entitySearchInput && entitySearchInput.value){
214
- let filterTermInput = document.createElement('input');
215
- filterTermInput.type = 'hidden';
216
- filterTermInput.name = 'entityFilterTerm';
217
- filterTermInput.value = entitySearchInput.value;
218
- sortForm.appendChild(filterTermInput);
209
+ let entityFilterTermInput = sortForm.querySelector('input[name="entityFilterTerm"]');
210
+ if(entityFilterTermInput){
211
+ entityFilterTermInput.value = entitySearchInput?.value || '';
219
212
  }
220
213
  let allFilters = document.querySelectorAll('.filters-toggle-content .filter input');
221
214
  for(let filterInput of allFilters){
222
- if(filterInput.value){
223
- let filterFieldInput = document.createElement('input');
224
- filterFieldInput.type = 'hidden';
225
- filterFieldInput.name = filterInput.name;
226
- filterFieldInput.value = filterInput.value;
227
- sortForm.appendChild(filterFieldInput);
215
+ let filterName = filterInput.name.replace(/^filters\[/, '').replace(/\]$/, '');
216
+ let sortFormFilterInput = sortForm.querySelector('input[data-filter-key="'+filterName+'"]');
217
+ if(sortFormFilterInput){
218
+ sortFormFilterInput.value = filterInput.value;
228
219
  }
229
220
  }
230
- document.body.appendChild(sortForm);
231
221
  sortForm.submit();
232
222
  });
233
223
  }
@@ -349,26 +339,60 @@ window.addEventListener('DOMContentLoaded', () => {
349
339
  button.addEventListener('click', (event) => {
350
340
  event.preventDefault();
351
341
  let fieldName = button.getAttribute('data-field');
342
+ let fileName = button.getAttribute('data-filename');
352
343
  let fileInput = document.getElementById(fieldName);
353
- 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
+ }
354
363
  if(currentFileDisplay){
355
- currentFileDisplay.style.display = 'none';
364
+ currentFileDisplay.remove();
356
365
  }
357
- if(fileInput){
358
- fileInput.value = '';
359
- let form = fileInput.closest('form');
360
- if(form){
361
- let clearFieldName = 'clear_'+fieldName;
362
- let existingClearInput = form.querySelector('input[name="'+clearFieldName+'"]');
363
- if(!existingClearInput){
364
- let clearInput = document.createElement('input');
365
- clearInput.type = 'hidden';
366
- clearInput.name = clearFieldName;
367
- clearInput.value = '1';
368
- 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);
369
374
  }
375
+ existingHiddenInput.value = filesArray.join(',');
376
+ return;
370
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;
371
390
  }
391
+ let clearInput = document.createElement('input');
392
+ clearInput.type = 'hidden';
393
+ clearInput.name = clearFieldName;
394
+ clearInput.value = '1';
395
+ form.appendChild(clearInput);
372
396
  });
373
397
  }
374
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}}/>
@@ -27,6 +27,14 @@
27
27
  </svg>
28
28
  </span>
29
29
  </div>
30
+ <form class="sort-form hidden" method="POST" action="{{&listRoute}}">
31
+ <input type="hidden" name="sortBy" value=""/>
32
+ <input type="hidden" name="sortDirection" value=""/>
33
+ <input type="hidden" name="entityFilterTerm" value=""/>
34
+ {{#filters}}
35
+ <input type="hidden" name="filters[{{&propertyKey}}]" value="" data-filter-key="{{&propertyKey}}"/>
36
+ {{/filters}}
37
+ </form>
30
38
  </th>
31
39
  {{/fieldsHeaders}}
32
40
  <th class="field field-edit"></th>
@@ -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)
@@ -611,7 +667,7 @@ class RouterContents
611
667
  fieldValue = '' !== fieldValue ? sc.formatDate(new Date(fieldValue)) : '';
612
668
  }
613
669
  if('reference' === resourceProperty.type){
614
- let relationEntity = entity[resourceProperty.alias || resourceProperty.reference];
670
+ let relationEntity = entity[resourceProperty.alias || 'related_'+resourceProperty.reference];
615
671
  if(relationEntity){
616
672
  let relation = this.relations()[resourceProperty.reference];
617
673
  if(relation){
@@ -123,7 +123,7 @@ class ContentRenderer
123
123
  return layoutContent;
124
124
  }
125
125
  let routerKey = route?.router;
126
- let categoryName = currentEntityData?.cms_categories?.name;
126
+ let categoryName = currentEntityData?.related_cms_categories?.name;
127
127
  let siteHandle = this.templateResolver.resolveDomainToSiteKey(domain)
128
128
  + (routerKey ? ' '+routerKey : '')
129
129
  + (categoryName ? ' cat-'+categoryName : '');
@@ -65,14 +65,11 @@ class SitemapLoader
65
65
  try {
66
66
  let routesWithPages = await this.routesRepository.loadWithRelations(filters, 'cms_pages');
67
67
  let sitemapPages = routesWithPages.filter(route => {
68
- if(!route.cms_pages || 0 === route.cms_pages.length){
68
+ if(!route.related_cms_pages || 0 === route.related_cms_pages.length){
69
69
  return false;
70
70
  }
71
- let page = route.cms_pages[0];
72
- if('noindex,nofollow' === page.meta_robots){
73
- return false;
74
- }
75
- return true;
71
+ return 'noindex,nofollow' !== route.related_cms_pages[0].meta_robots;
72
+
76
73
  });
77
74
  req.sitemapPages = sitemapPages;
78
75
  eventData.variables.sitemapPages = sitemapPages;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/cms",
3
3
  "scope": "@reldens",
4
- "version": "0.57.0",
4
+ "version": "0.59.0",
5
5
  "description": "Reldens - CMS",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",
@@ -35,9 +35,9 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@reldens/server-utils": "^0.46.0",
38
- "@reldens/storage": "^0.90.0",
38
+ "@reldens/storage": "^0.91.0",
39
39
  "@reldens/utils": "^0.54.0",
40
- "dotenv": "17.2.3",
40
+ "dotenv": "17.3.1",
41
41
  "mustache": "4.2.0"
42
42
  }
43
43
  }