@reldens/cms 0.55.0 → 0.56.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.
@@ -134,6 +134,11 @@
134
134
  &:hover {
135
135
  background-color: var(--red);
136
136
  }
137
+
138
+ svg, path {
139
+ width: 22px;
140
+ fill: var(--white);
141
+ }
137
142
  }
138
143
 
139
144
  .icon {
@@ -267,8 +272,6 @@
267
272
  }
268
273
 
269
274
  .entity-list {
270
- overflow: auto;
271
-
272
275
  .actions {
273
276
  display: flex;
274
277
  justify-content: end;
@@ -280,6 +283,10 @@
280
283
  }
281
284
  }
282
285
 
286
+ .table-wrapper {
287
+ overflow-x: auto;
288
+ }
289
+
283
290
  .forms-container {
284
291
  width: 96%;
285
292
  max-width: 400px;
@@ -360,7 +367,7 @@
360
367
  }
361
368
 
362
369
  & th.field {
363
- padding: 1.5rem 0;
370
+ padding: 1rem 0;
364
371
 
365
372
  & span {
366
373
  color: var(--white);
@@ -383,18 +390,23 @@
383
390
 
384
391
  .field-actions-container {
385
392
  display: flex;
386
- flex-direction: column;
387
- justify-content: center;
393
+ flex-direction: row;
394
+ justify-content: space-around;
388
395
  align-items: center;
389
396
  margin: 0 1rem;
390
397
 
391
398
  .button {
392
- margin-bottom: 1rem;
399
+ border: 1px solid var(--white);
393
400
 
394
401
  &.list-select {
395
402
  margin-bottom: 0;
396
403
  }
397
404
  }
405
+
406
+ .list-delete-selection {
407
+ padding: 0.34rem 1rem;
408
+ margin: 0 0.2rem;
409
+ }
398
410
  }
399
411
 
400
412
  & a, a:visited {
@@ -422,7 +434,7 @@
422
434
 
423
435
  .field-edit {
424
436
  & span {
425
- & svg, path {
437
+ & svg, & path {
426
438
  width: 24px;
427
439
  color: var(--lightBlue);
428
440
  }
@@ -431,14 +443,58 @@
431
443
 
432
444
  .field-delete {
433
445
  & span {
434
- & svg, path {
446
+ & svg, & path {
435
447
  width: 24px;
436
- fill: var(--lightBlue);
448
+ fill: var(--red);
437
449
  }
438
450
  }
439
451
  }
440
452
  }
441
453
 
454
+ .sortable {
455
+ cursor: pointer;
456
+ user-select: none;
457
+ transition: background-color 0.2s;
458
+
459
+ &:hover {
460
+ background-color: var(--lightBlue);
461
+
462
+ .sort-icon {
463
+ opacity: 0.7;
464
+ }
465
+ }
466
+
467
+ .header-content {
468
+ display: flex;
469
+ align-items: center;
470
+ justify-content: space-between;
471
+ }
472
+ }
473
+
474
+ .sort-indicators {
475
+ display: flex;
476
+ flex-direction: column;
477
+ margin-left: 0.5rem;
478
+ gap: 2px;
479
+ }
480
+
481
+ .sort-icon {
482
+ width: 12px;
483
+ height: 12px;
484
+ fill: var(--grey);
485
+ opacity: 0.4;
486
+ transition: opacity 0.2s;
487
+
488
+ &.active {
489
+ fill: var(--white);
490
+ opacity: 1;
491
+ }
492
+ }
493
+
494
+ .sorted {
495
+ background-color: var(--darkBlue);
496
+ }
497
+
442
498
  .filters-toggle {
443
499
  cursor: pointer;
444
500
  }
@@ -595,7 +651,6 @@
595
651
  justify-content: space-between;
596
652
  align-items: center;
597
653
  width: 100%;
598
- margin-bottom: 1rem;
599
654
  flex-wrap: wrap;
600
655
  gap: 1rem;
601
656
  }
@@ -4,12 +4,14 @@
4
4
  *
5
5
  */
6
6
 
7
- if(window.trustedTypes?.createPolicy){
8
- trustedTypes.createPolicy('default', {
9
- createHTML: s => s,
10
- createScriptURL: s => s
7
+ let trustedTypesPolicy = null;
8
+ if(window.trustedTypes && window.trustedTypes.createPolicy){
9
+ trustedTypesPolicy = window.trustedTypes.createPolicy('default', {
10
+ createHTML: (s) => s,
11
+ createScriptURL: (s) => s
11
12
  });
12
13
  }
14
+ window.trustedTypesPolicy = trustedTypesPolicy;
13
15
 
14
16
  window.addEventListener('DOMContentLoaded', () => {
15
17
 
@@ -168,7 +170,14 @@ window.addEventListener('DOMContentLoaded', () => {
168
170
  params.set(filterName, filterInput.value);
169
171
  }
170
172
  }
171
- let newUrl = url.pathname + '?' + params;
173
+ let sortedHeader = document.querySelector('th.sorted');
174
+ if(sortedHeader){
175
+ let columnName = sortedHeader.getAttribute('data-column');
176
+ let sortDirection = sortedHeader.classList.contains('sorted-asc') ? 'asc' : 'desc';
177
+ params.set('sortBy', columnName);
178
+ params.set('sortDirection', sortDirection);
179
+ }
180
+ let newUrl = url.pathname+'?'+params;
172
181
  window.location.href = newUrl;
173
182
  return false;
174
183
  });
@@ -176,6 +185,54 @@ window.addEventListener('DOMContentLoaded', () => {
176
185
  }
177
186
  }
178
187
 
188
+ // column sorting functionality:
189
+ let sortableHeaders = document.querySelectorAll('th.sortable');
190
+ if(sortableHeaders){
191
+ for(let header of sortableHeaders){
192
+ header.addEventListener('click', () => {
193
+ let columnName = header.getAttribute('data-column');
194
+ let currentSortDirection = header.classList.contains('sorted-asc') ? 'asc' : header.classList.contains('sorted-desc') ? 'desc' : '';
195
+ let newSortDirection = 'asc';
196
+ if('asc' === currentSortDirection){
197
+ newSortDirection = 'desc';
198
+ }
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';
205
+ sortByInput.value = columnName;
206
+ sortForm.appendChild(sortByInput);
207
+ let sortDirectionInput = document.createElement('input');
208
+ sortDirectionInput.type = 'hidden';
209
+ sortDirectionInput.name = 'sortDirection';
210
+ sortDirectionInput.value = newSortDirection;
211
+ sortForm.appendChild(sortDirectionInput);
212
+ 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);
219
+ }
220
+ let allFilters = document.querySelectorAll('.filters-toggle-content .filter input');
221
+ 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);
228
+ }
229
+ }
230
+ document.body.appendChild(sortForm);
231
+ sortForm.submit();
232
+ });
233
+ }
234
+ }
235
+
179
236
  // list "select all" option:
180
237
  let listSelect = document.querySelector('.list-select');
181
238
  if(listSelect){
@@ -3,7 +3,11 @@
3
3
  <div class="field-actions-container">
4
4
  <form name="delete-selection-form" id="delete-selection-form" action="{{&deletePath}}" method="post">
5
5
  <input type="hidden" name="ids" class="hidden-ids-input"/>
6
- <button class="button button-danger list-delete-selection">Bulk Delete</button>
6
+ <button class="button button-danger list-delete-selection">
7
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
8
+ <path d="M10,18a1,1,0,0,0,1-1V11a1,1,0,0,0-2,0v6A1,1,0,0,0,10,18ZM20,6H16V5a3,3,0,0,0-3-3H11A3,3,0,0,0,8,5V6H4A1,1,0,0,0,4,8H5V19a3,3,0,0,0,3,3h8a3,3,0,0,0,3-3V8h1a1,1,0,0,0,0-2ZM10,5a1,1,0,0,1,1-1h2a1,1,0,0,1,1,1V6H10Zm7,14a1,1,0,0,1-1,1H8a1,1,0,0,1-1-1V8H17Zm-3-1a1,1,0,0,0,1-1V11a1,1,0,0,0-2,0v6A1,1,0,0,0,14,18Z" fill="#6563ff"/>
9
+ </svg>
10
+ </button>
7
11
  </form>
8
12
  <button class="button button-primary list-select" data-checked="1">
9
13
  <input type="checkbox" id="input-check-select-all"/>
@@ -11,16 +15,22 @@
11
15
  </div>
12
16
  </th>
13
17
  {{#fieldsHeaders}}
14
- <th class="field field-{{&name}}">
15
- <span>{{&value}}</span>
18
+ <th class="field field-{{&name}} sortable{{#isSorted}} sorted sorted-{{&sortDirection}}{{/isSorted}}" data-column="{{&name}}">
19
+ <div class="header-content">
20
+ <span class="header-label">{{&value}}</span>
21
+ <span class="sort-indicators">
22
+ <svg class="sort-icon sort-asc{{#isSortedAsc}} active{{/isSortedAsc}}" viewBox="0 0 10 6" xmlns="http://www.w3.org/2000/svg">
23
+ <path d="M0 6L5 0L10 6Z"/>
24
+ </svg>
25
+ <svg class="sort-icon sort-desc{{#isSortedDesc}} active{{/isSortedDesc}}" viewBox="0 0 10 6" xmlns="http://www.w3.org/2000/svg">
26
+ <path d="M0 0L5 6L10 0Z"/>
27
+ </svg>
28
+ </span>
29
+ </div>
16
30
  </th>
17
31
  {{/fieldsHeaders}}
18
- <th class="field field-edit">
19
- <span>Edit</span>
20
- </th>
21
- <th class="field field-delete">
22
- <span>Delete</span>
23
- </th>
32
+ <th class="field field-edit"></th>
33
+ <th class="field field-delete"></th>
24
34
  </tr>
25
35
  {{#rows}}
26
36
  <tr class="row">
@@ -32,9 +32,11 @@
32
32
  </div>
33
33
  </form>
34
34
  </div>
35
- <table class="list">
36
- {{&list}}
37
- </table>
35
+ <div class="table-wrapper">
36
+ <table class="list">
37
+ {{&list}}
38
+ </table>
39
+ </div>
38
40
  <div class="pagination">
39
41
  {{&pagination}}
40
42
  </div>
@@ -45,6 +45,49 @@ class AdminFiltersManager
45
45
  return true;
46
46
  }
47
47
 
48
+ getSortingFromSession(req, entityPath)
49
+ {
50
+ if(!req?.session){
51
+ return {sortBy: '', sortDirection: ''};
52
+ }
53
+ let sessionKey = 'adminFilters_'+entityPath;
54
+ let sessionData = req.session[sessionKey];
55
+ if(!sessionData || 'object' !== typeof sessionData){
56
+ return {sortBy: '', sortDirection: ''};
57
+ }
58
+ return {
59
+ sortBy: sessionData.sortBy || '',
60
+ sortDirection: sessionData.sortDirection || ''
61
+ };
62
+ }
63
+
64
+ saveSortingToSession(req, entityPath, sortBy, sortDirection)
65
+ {
66
+ if(!req?.session){
67
+ return false;
68
+ }
69
+ let sessionKey = 'adminFilters_'+entityPath;
70
+ if(!req.session[sessionKey]){
71
+ req.session[sessionKey] = {regular: {}, entityFilterTerm: ''};
72
+ }
73
+ req.session[sessionKey].sortBy = sortBy || '';
74
+ req.session[sessionKey].sortDirection = sortDirection || '';
75
+ return true;
76
+ }
77
+
78
+ clearSortingFromSession(req, entityPath)
79
+ {
80
+ if(!req?.session){
81
+ return false;
82
+ }
83
+ let sessionKey = 'adminFilters_'+entityPath;
84
+ if(req.session[sessionKey]){
85
+ delete req.session[sessionKey].sortBy;
86
+ delete req.session[sessionKey].sortDirection;
87
+ }
88
+ return true;
89
+ }
90
+
48
91
  prepareTextFilters(entityFilterTerm, driverResource)
49
92
  {
50
93
  if(!entityFilterTerm || '' === entityFilterTerm.trim()){
@@ -42,10 +42,14 @@ class RouterContents
42
42
  let pageSize = Number(req?.query?.pageSize || 25);
43
43
  let shouldClearFilters = 'true' === req?.query?.clearFilters;
44
44
  let sessionFilters = this.filtersManager.getFiltersFromSession(req, entityPath);
45
+ let sessionSorting = this.filtersManager.getSortingFromSession(req, entityPath);
45
46
  let filtersFromParams = shouldClearFilters ? {} : req?.body?.filters || req?.query?.filters || {};
46
47
  let entityFilterTerm = shouldClearFilters ? '' : sc.get(req?.body, 'entityFilterTerm', '');
48
+ let sortByFromParams = shouldClearFilters ? '' : sc.get(req?.body, 'sortBy', sc.get(req?.query, 'sortBy', ''));
49
+ let sortDirectionFromParams = shouldClearFilters ? '' : sc.get(req?.body, 'sortDirection', sc.get(req?.query, 'sortDirection', ''));
47
50
  if(shouldClearFilters){
48
51
  this.filtersManager.clearFiltersFromSession(req, entityPath);
52
+ this.filtersManager.clearSortingFromSession(req, entityPath);
49
53
  }
50
54
  let hasNewEntityFilterTerm = entityFilterTerm && '' !== entityFilterTerm;
51
55
  let hasNewToggleFilters = Object.keys(filtersFromParams).length > 0;
@@ -61,8 +65,15 @@ class RouterContents
61
65
  this.filtersManager.saveFiltersToSession(req, entityPath, filtersToSave);
62
66
  sessionFilters = filtersToSave;
63
67
  }
68
+ if(sortByFromParams && sortDirectionFromParams){
69
+ this.filtersManager.saveSortingToSession(req, entityPath, sortByFromParams, sortDirectionFromParams);
70
+ sessionSorting = {sortBy: sortByFromParams, sortDirection: sortDirectionFromParams};
71
+ }
64
72
  let mergedFilters = shouldClearFilters ? {} : Object.assign({}, sessionFilters.regular || {});
65
73
  let finalEntityFilterTerm = shouldClearFilters ? '' : sessionFilters.entityFilterTerm || '';
74
+ let idProperty = this.fetchEntityIdPropertyKey(driverResource);
75
+ let finalSortBy = sessionSorting.sortBy || idProperty;
76
+ let finalSortDirection = sessionSorting.sortDirection || 'ASC';
66
77
  let filters = this.filtersManager.prepareFilters(mergedFilters, driverResource);
67
78
  let textFilters = this.filtersManager.prepareTextFilters(finalEntityFilterTerm, driverResource);
68
79
  let combinedFilters = this.filtersManager.combineFilters(filters, textFilters);
@@ -74,6 +85,12 @@ class RouterContents
74
85
  if(finalEntityFilterTerm){
75
86
  paginationUrl += '&entityFilterTerm='+encodeURIComponent(finalEntityFilterTerm);
76
87
  }
88
+ if(finalSortBy){
89
+ paginationUrl += '&sortBy='+encodeURIComponent(finalSortBy);
90
+ }
91
+ if(finalSortDirection){
92
+ paginationUrl += '&sortDirection='+encodeURIComponent(finalSortDirection);
93
+ }
77
94
  for(let filterKey of Object.keys(mergedFilters)){
78
95
  if(mergedFilters[filterKey]){
79
96
  paginationUrl += '&filters['+filterKey+']='+encodeURIComponent(mergedFilters[filterKey]);
@@ -109,9 +126,15 @@ class RouterContents
109
126
  driverResource.options.properties[property]?.alias || '',
110
127
  driverResource.id()
111
128
  );
129
+ let isSorted = finalSortBy === property;
130
+ let sortDirection = isSorted ? finalSortDirection.toLowerCase() : '';
112
131
  return {
113
132
  name: property,
114
- value: '' !== alias ? alias + ' ('+propertyTitle+')' : propertyTitle
133
+ value: '' !== alias ? alias + ' ('+propertyTitle+')' : propertyTitle,
134
+ isSorted,
135
+ sortDirection,
136
+ isSortedAsc: isSorted && 'asc' === sortDirection,
137
+ isSortedDesc: isSorted && 'desc' === sortDirection
115
138
  };
116
139
  }),
117
140
  rows: await this.loadEntitiesForList(
@@ -119,7 +142,9 @@ class RouterContents
119
142
  pageSize,
120
143
  currentPage,
121
144
  req,
122
- combinedFilters
145
+ combinedFilters,
146
+ finalSortBy,
147
+ finalSortDirection
123
148
  )
124
149
  }),
125
150
  pagination: renderedPagination,
@@ -369,15 +394,15 @@ class RouterContents
369
394
  return await entityRepository.count(filters);
370
395
  }
371
396
 
372
- async loadEntitiesForList(driverResource, pageSize, currentPage, req, filters)
397
+ async loadEntitiesForList(driverResource, pageSize, currentPage, req, filters, sortBy, sortDirection)
373
398
  {
374
399
  let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
375
400
  entityRepository.limit = pageSize;
376
401
  if(1 < currentPage){
377
402
  entityRepository.offset = (currentPage - 1) * pageSize;
378
403
  }
379
- entityRepository.sortBy = req?.body?.sortBy || false;
380
- entityRepository.sortDirection = req?.body?.sortDirection || false;
404
+ entityRepository.sortBy = sortBy || false;
405
+ entityRepository.sortDirection = sortDirection || false;
381
406
  let loadedEntities = await entityRepository.loadWithRelations(filters, []);
382
407
  entityRepository.limit = 0;
383
408
  entityRepository.offset = 0;
@@ -50,6 +50,9 @@ CREATE TABLE IF NOT EXISTS `cms_pages` (
50
50
  `locale` VARCHAR(10) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
51
51
  `publish_date` TIMESTAMP NULL DEFAULT (NOW()),
52
52
  `expire_date` TIMESTAMP NULL DEFAULT NULL,
53
+ `author` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
54
+ `meta_color_scheme` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
55
+ `meta_og_type` VARCHAR(50) NULL DEFAULT 'website' COLLATE 'utf8mb4_unicode_ci',
53
56
  `created_at` TIMESTAMP NOT NULL DEFAULT (NOW()),
54
57
  `updated_at` TIMESTAMP NOT NULL DEFAULT (NOW()) ON UPDATE CURRENT_TIMESTAMP,
55
58
  PRIMARY KEY (`id`) USING BTREE,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/cms",
3
3
  "scope": "@reldens",
4
- "version": "0.55.0",
4
+ "version": "0.56.0",
5
5
  "description": "Reldens - CMS",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@reldens/server-utils": "^0.46.0",
38
- "@reldens/storage": "^0.88.0",
38
+ "@reldens/storage": "^0.89.0",
39
39
  "@reldens/utils": "^0.54.0",
40
40
  "dotenv": "17.2.3",
41
41
  "mustache": "4.2.0"