@reldens/cms 0.83.0 → 0.85.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.
@@ -379,6 +379,33 @@ Admin UI builder that:
379
379
  - Form field generation
380
380
  - Branding and styling
381
381
 
382
+ ### Delete Relations Warning
383
+
384
+ **File:** `lib/admin-manager/delete-relations-warning.js`
385
+
386
+ Builds the sentences appended to the delete confirmation dialog, so the admin states what actually happens to the
387
+ records that reference the one being deleted. `ContentsBuilder` owns an instance as `this.relationsWarning` and
388
+ calls `buildReverseRelationsMap()` once per admin contents build, then `fetchWarning(entityId, reverseRelations)`
389
+ per entity.
390
+
391
+ `buildReverseRelationsMap()` walks every resource and, for each `type: 'reference'` property, files the child
392
+ entity label under the referenced entity, grouped by what the foreign key does on delete. The group comes from
393
+ `fetchRelationGroup(property.onDelete)`, where `onDelete` is generated by `@reldens/storage` from the live
394
+ `information_schema` delete rule:
395
+
396
+ - `cascade` goes to `deleted`, rendered with `messages.confirmDeleteRelations`.
397
+ - `setNull` and `setDefault` go to `unlinked`, rendered with `messages.confirmUnlinkRelations`.
398
+ - Everything else, including `restrict`, `noAction` and a missing value, goes to `blocking`, rendered with
399
+ `messages.confirmBlockingRelations`.
400
+
401
+ There is deliberately no default claim: `restrict` and `noAction` neither delete nor unlink, they make the delete
402
+ fail, and a missing `onDelete` means the consumer's entities predate the generated attribute. Reporting either of
403
+ those as "will be deleted" was the original bug this grouping replaced.
404
+
405
+ Only the three sentences are produced here. The cms itself never deletes related rows: `RouterContents`
406
+ `processDeleteEntities()` deletes the entity row alone and the database referential actions do the rest, which is
407
+ exactly why the wording has to follow the foreign keys.
408
+
382
409
  ### Admin Filters Manager
383
410
 
384
411
  **File:** `lib/admin-manager/admin-filters-manager.js`
@@ -4,6 +4,7 @@
4
4
  *
5
5
  */
6
6
 
7
+ const { DeleteRelationsWarning } = require('./delete-relations-warning');
7
8
  const { sc } = require('@reldens/utils');
8
9
 
9
10
  class ContentsBuilder
@@ -29,6 +30,10 @@ class ContentsBuilder
29
30
  this.fetchUploadProperties = props.fetchUploadProperties;
30
31
  this.fetchTranslation = props.fetchTranslation;
31
32
  this.fetchEntityIdPropertyKey = props.fetchEntityIdPropertyKey;
33
+ this.relationsWarning = new DeleteRelationsWarning({
34
+ translations: props.translations,
35
+ resources: props.resources
36
+ });
32
37
  this.adminContents = {};
33
38
  }
34
39
 
@@ -116,7 +121,7 @@ class ContentsBuilder
116
121
  async buildEntitiesContents()
117
122
  {
118
123
  let entitiesContents = {};
119
- let reverseRelations = this.buildReverseRelationsMap();
124
+ let reverseRelations = this.relationsWarning.buildReverseRelationsMap();
120
125
  for(let driverResource of this.resources()){
121
126
  let templateTitle = this.translations.labels[driverResource.id()];
122
127
  let entityName = (driverResource.id().replace(/_/g, '-'));
@@ -130,7 +135,7 @@ class ContentsBuilder
130
135
  let editProperties = Object.keys(driverResource.options.properties);
131
136
  editProperties.splice(editProperties.indexOf(idProperty), 1);
132
137
  let entityId = driverResource.id();
133
- let deleteRelationsWarning = this.fetchDeleteRelationsWarning(entityId, reverseRelations);
138
+ let deleteRelationsWarning = this.relationsWarning.fetchWarning(entityId, reverseRelations);
134
139
  let filters = driverResource.options.filterProperties.map((property) => {
135
140
  return {
136
141
  propertyKey: property,
@@ -228,48 +233,6 @@ class ContentsBuilder
228
233
  return editFields;
229
234
  }
230
235
 
231
- buildReverseRelationsMap()
232
- {
233
- let reverseRelations = {};
234
- for(let resource of this.resources()){
235
- let childLabel = sc.get(this.translations.labels, resource.id(), resource.id());
236
- this.addResourceReverseRelations(reverseRelations, resource.options.properties, childLabel);
237
- }
238
- return reverseRelations;
239
- }
240
-
241
- addResourceReverseRelations(reverseRelations, properties, childLabel)
242
- {
243
- for(let propertyKey of Object.keys(properties)){
244
- let property = properties[propertyKey];
245
- if('reference' !== property.type){
246
- continue;
247
- }
248
- if(!reverseRelations[property.reference]){
249
- reverseRelations[property.reference] = {deleted: [], unlinked: []};
250
- }
251
- let relationGroup = true === property.unlinkOnDelete ? 'unlinked' : 'deleted';
252
- if(-1 === reverseRelations[property.reference][relationGroup].indexOf(childLabel)){
253
- reverseRelations[property.reference][relationGroup].push(childLabel);
254
- }
255
- }
256
- }
257
-
258
- fetchDeleteRelationsWarning(entityId, reverseRelations)
259
- {
260
- return this.buildRelationsSentence(entityId, reverseRelations, 'deleted', 'confirmDeleteRelations')
261
- +this.buildRelationsSentence(entityId, reverseRelations, 'unlinked', 'confirmUnlinkRelations');
262
- }
263
-
264
- buildRelationsSentence(entityId, reverseRelations, relationGroup, messageKey)
265
- {
266
- let childLabels = sc.get(sc.get(reverseRelations, entityId, {}), relationGroup, []);
267
- if(0 === childLabels.length){
268
- return '';
269
- }
270
- return ' '+this.translations.messages[messageKey]+' '+[...childLabels].sort().join(', ')+'.';
271
- }
272
-
273
236
  async render(content, params)
274
237
  {
275
238
  return await this.renderCallback(content, params);
@@ -19,7 +19,8 @@ module.exports.DefaultTranslations = {
19
19
  confirmClearCacheMessage: 'Are you sure you want to clear all cache? This action cannot be undone.',
20
20
  clearCacheWarning: 'This will clear all cached routes and may impact site performance temporarily.',
21
21
  confirmDeleteRelations: 'The following related records will also be deleted:',
22
- confirmUnlinkRelations: 'The following related records will be kept but unlinked from it:'
22
+ confirmUnlinkRelations: 'The following related records will be kept but unlinked from it:',
23
+ confirmBlockingRelations: 'The following related records reference it and may block the delete:'
23
24
  },
24
25
  labels: {
25
26
  navigation: 'Reldens - CMS',
@@ -0,0 +1,79 @@
1
+ /**
2
+ *
3
+ * Reldens - DeleteRelationsWarning
4
+ *
5
+ * Builds the delete confirmation warning for an entity by grouping every entity that references it, using the
6
+ * relation onDelete rule: "cascade" means the related records are deleted with it, "setNull" and "setDefault"
7
+ * mean they are kept and only unlinked, and anything else (restrict, no action, or an unknown rule) is reported
8
+ * as possibly blocking the delete instead of claiming an outcome that will not happen.
9
+ *
10
+ */
11
+
12
+ const { sc } = require('@reldens/utils');
13
+
14
+ class DeleteRelationsWarning
15
+ {
16
+
17
+ constructor(props)
18
+ {
19
+ this.translations = props.translations;
20
+ this.resources = props.resources;
21
+ }
22
+
23
+ buildReverseRelationsMap()
24
+ {
25
+ let reverseRelations = {};
26
+ for(let resource of this.resources()){
27
+ let childLabel = sc.get(this.translations.labels, resource.id(), resource.id());
28
+ this.addResourceReverseRelations(reverseRelations, resource.options.properties, childLabel);
29
+ }
30
+ return reverseRelations;
31
+ }
32
+
33
+ addResourceReverseRelations(reverseRelations, properties, childLabel)
34
+ {
35
+ for(let propertyKey of Object.keys(properties)){
36
+ let property = properties[propertyKey];
37
+ if('reference' !== property.type){
38
+ continue;
39
+ }
40
+ if(!reverseRelations[property.reference]){
41
+ reverseRelations[property.reference] = {deleted: [], unlinked: [], blocking: []};
42
+ }
43
+ let relationGroup = this.fetchRelationGroup(property.onDelete);
44
+ if(-1 === reverseRelations[property.reference][relationGroup].indexOf(childLabel)){
45
+ reverseRelations[property.reference][relationGroup].push(childLabel);
46
+ }
47
+ }
48
+ }
49
+
50
+ fetchRelationGroup(onDelete)
51
+ {
52
+ if('cascade' === onDelete){
53
+ return 'deleted';
54
+ }
55
+ if('setNull' === onDelete || 'setDefault' === onDelete){
56
+ return 'unlinked';
57
+ }
58
+ return 'blocking';
59
+ }
60
+
61
+ fetchWarning(entityId, reverseRelations)
62
+ {
63
+ return this.buildRelationsSentence(entityId, reverseRelations, 'deleted', 'confirmDeleteRelations')
64
+ +this.buildRelationsSentence(entityId, reverseRelations, 'unlinked', 'confirmUnlinkRelations')
65
+ +this.buildRelationsSentence(entityId, reverseRelations, 'blocking', 'confirmBlockingRelations');
66
+ }
67
+
68
+ buildRelationsSentence(entityId, reverseRelations, relationGroup, messageKey)
69
+ {
70
+ let childLabels = sc.get(sc.get(reverseRelations, entityId, {}), relationGroup, []);
71
+ if(0 === childLabels.length){
72
+ return '';
73
+ }
74
+ return ' '+this.translations.messages[messageKey]+' '+[...childLabels].sort().join(', ')+'.';
75
+ }
76
+
77
+ }
78
+
79
+ module.exports.DeleteRelationsWarning = DeleteRelationsWarning;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/cms",
3
3
  "scope": "@reldens",
4
- "version": "0.83.0",
4
+ "version": "0.85.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.56.0",
38
- "@reldens/storage": "^0.110.0",
38
+ "@reldens/storage": "^0.112.0",
39
39
  "@reldens/utils": "^0.57.0",
40
40
  "dotenv": "17.4.2",
41
41
  "mustache": "4.2.0"