@reldens/cms 0.16.0 → 0.19.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.
Files changed (38) hide show
  1. package/README.md +92 -9
  2. package/admin/reldens-admin-client.css +55 -0
  3. package/admin/reldens-admin-client.js +24 -0
  4. package/admin/templates/cache-clean-button.html +4 -0
  5. package/admin/templates/clear-all-cache-button.html +18 -0
  6. package/admin/templates/fields/view/textarea.html +1 -1
  7. package/bin/reldens-cms-generate-entities.js +85 -18
  8. package/bin/reldens-cms.js +6 -6
  9. package/lib/admin-manager/contents-builder.js +257 -0
  10. package/lib/admin-manager/router-contents.js +618 -0
  11. package/lib/admin-manager/router.js +208 -0
  12. package/lib/admin-manager-validator.js +2 -1
  13. package/lib/admin-manager.js +116 -990
  14. package/lib/admin-translations.js +9 -1
  15. package/lib/cache/add-cache-button-subscriber.js +149 -0
  16. package/lib/cache/cache-manager.js +168 -0
  17. package/lib/cache/cache-routes-handler.js +99 -0
  18. package/lib/cms-pages-route-manager.js +45 -21
  19. package/lib/frontend.js +288 -71
  20. package/lib/installer.js +5 -2
  21. package/lib/json-fields-parser.js +74 -0
  22. package/lib/manager.js +49 -4
  23. package/lib/pagination-handler.js +243 -0
  24. package/lib/search-renderer.js +116 -0
  25. package/lib/search.js +344 -0
  26. package/lib/template-engine/collections-single-transformer.js +53 -0
  27. package/lib/template-engine/collections-transformer-base.js +84 -0
  28. package/lib/template-engine/collections-transformer.js +353 -0
  29. package/lib/template-engine/entities-transformer.js +65 -0
  30. package/lib/template-engine/partials-transformer.js +171 -0
  31. package/lib/template-engine.js +53 -387
  32. package/lib/templates-list.js +2 -0
  33. package/migrations/default-homepage.sql +6 -6
  34. package/migrations/install.sql +21 -20
  35. package/package.json +4 -4
  36. package/templates/page.html +19 -2
  37. package/templates/partials/entriesListView.html +14 -0
  38. package/templates/partials/pagedCollection.html +33 -0
package/README.md CHANGED
@@ -157,18 +157,18 @@ Templates support dynamic content blocks, entity rendering, and collections with
157
157
  ```html
158
158
  <!-- Loop through records with full template rendering -->
159
159
  <collection name="cmsBlocks" filters="{status: 'active'}">
160
- <div class="block">
161
- <h3>{{row.title}}</h3>
162
- <div class="content">{{row.content}}</div>
163
- </div>
160
+ <div class="block">
161
+ <h3>{{row.title}}</h3>
162
+ <div class="content">{{row.content}}</div>
163
+ </div>
164
164
  </collection>
165
165
 
166
166
  <collection name="articles" filters="{category: 'technology'}">
167
- <div class="article">
168
- <h4>{{row.title}}</h4>
169
- <p>{{row.summary}}</p>
170
- <img src="{{row.featured_image}}" alt="{{row.title}}">
171
- </div>
167
+ <div class="article">
168
+ <h4>{{row.title}}</h4>
169
+ <p>{{row.summary}}</p>
170
+ <img src="{{row.featured_image}}" alt="{{row.title}}">
171
+ </div>
172
172
  </collection>
173
173
 
174
174
  <!-- With pagination and sorting -->
@@ -178,8 +178,91 @@ Templates support dynamic content blocks, entity rendering, and collections with
178
178
  <p>{{row.summary}}</p>
179
179
  </div>
180
180
  </collection>
181
+
182
+ <!-- Paginated collections with navigation -->
183
+ <collection name="articles"
184
+ filters="{featured: true}"
185
+ data="{limit: 10, sortBy: 'created_at', sortDirection: 'desc'}"
186
+ pagination="articles-1"
187
+ container="pagedCollection"
188
+ prevPages="2"
189
+ nextPages="2">
190
+ <div class="article-card">
191
+ <h4>{{row.title}}</h4>
192
+ <p>{{row.summary}}</p>
193
+ <span class="date">{{row.created_at}}</span>
194
+ </div>
195
+ </collection>
196
+
197
+ <!-- Multiple paginated collections on the same page -->
198
+ <collection name="news"
199
+ filters="{category: 'technology'}"
200
+ data="{limit: 5, sortBy: 'published_at'}"
201
+ pagination="news-tech"
202
+ container="customPager">
203
+ <article>{{row.title}}</article>
204
+ </collection>
205
+
206
+ <collection name="events"
207
+ filters="{upcoming: true}"
208
+ data="{limit: 8}"
209
+ pagination="events-upcoming"
210
+ prevPages="3"
211
+ nextPages="1">
212
+ <div class="event">{{row.title}} - {{row.date}}</div>
213
+ </collection>
214
+ ```
215
+
216
+ **Pagination Attributes:**
217
+ - `pagination="collection-id"` - Enables pagination with unique identifier
218
+ - `container="templateName"` - Custom pagination template (defaults to "pagedCollection")
219
+ - `prevPages="2"` - Number of previous page links to show (default: 2)
220
+ - `nextPages="2"` - Number of next page links to show (default: 2)
221
+
222
+ **Pagination URL Parameters:**
223
+ Pagination state is managed via URL query parameters:
224
+ ```
225
+ /articles?articles-1-key={"page":2,"limit":10,"sortBy":"created_at","sortDirection":"desc"}
226
+ /news?news-tech-key={"page":3,"limit":5,"category":"technology"}
227
+ ```
228
+
229
+ **Custom Pagination Template:**
230
+ Create `templates/partials/pagedCollection.html`:
231
+ ```html
232
+ <div class="row paginated-contents">
233
+ <div class="collection-content col-lg-12 mt-2 mb-2">
234
+ {{&collectionContentForCurrentPage}}
235
+ </div>
236
+ <div class="pagination col-lg-12 mt-2 mb-2">
237
+ <ul class="pagination-list">
238
+ {{#prevPageUrl}}
239
+ <li><a href="{{prevPageUrl}}" class="page-link">{{&prevPageLabel}}</a></li>
240
+ {{/prevPageUrl}}
241
+ {{#prevPages}}
242
+ <li><a href="{{pageUrl}}" class="page-link">{{&pageLabel}}</a></li>
243
+ {{/prevPages}}
244
+ <li class="current">{{&currentPage}}</li>
245
+ {{#nextPages}}
246
+ <li><a href="{{pageUrl}}" class="page-link">{{&pageLabel}}</a></li>
247
+ {{/nextPages}}
248
+ {{#nextPageUrl}}
249
+ <li><a href="{{nextPageUrl}}" class="page-link">{{&nextPageLabel}}</a></li>
250
+ {{/nextPageUrl}}
251
+ </ul>
252
+ </div>
253
+ </div>
181
254
  ```
182
255
 
256
+ **Available Pagination Template Variables:**
257
+ - `{{&collectionContentForCurrentPage}}` - Rendered collection items for current page
258
+ - `{{currentPage}}` - Current page number
259
+ - `{{totalPages}}` - Total number of pages
260
+ - `{{totalRecords}}` - Total number of records
261
+ - `{{prevPageUrl}}` / `{{nextPageUrl}}` - Previous/next page URLs
262
+ - `{{&prevPageLabel}}` / `{{&nextPageLabel}}` - Previous/next link labels ("Previous"/"Next")
263
+ - `{{#prevPages}}` / `{{#nextPages}}` - Arrays of page objects with `pageUrl` and `pageLabel`
264
+ - `{{hasNextPage}}` / `{{hasPrevPage}}` - Boolean flags for navigation availability
265
+
183
266
  **Custom Partials with Variables:**
184
267
 
185
268
  *New HTML-style partial tags:*
@@ -817,6 +817,18 @@
817
817
  }
818
818
  }
819
819
 
820
+ .extra-actions {
821
+ display: flex;
822
+ width: 100%;
823
+ justify-content: end;
824
+ margin: 1rem 0;
825
+ }
826
+
827
+ .cache-clean-form {
828
+ display: block;
829
+ text-align: center;
830
+ }
831
+
820
832
  & .extra-content-container, .default-room-container {
821
833
  display: flex;
822
834
  flex-direction: column;
@@ -836,4 +848,47 @@
836
848
  overflow: auto;
837
849
  }
838
850
 
851
+ .cache-confirm-dialog {
852
+ border: none;
853
+ border-radius: 8px;
854
+ padding: 0;
855
+ max-width: 500px;
856
+ width: 90%;
857
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
858
+ }
859
+
860
+ .cache-confirm-dialog::backdrop {
861
+ background-color: rgba(0, 0, 0, 0.5);
862
+ }
863
+
864
+ .cache-dialog-content {
865
+ padding: 1rem;
866
+ }
867
+
868
+ .cache-dialog-content h5 {
869
+ margin: 0 0 1rem 0;
870
+ font-size: 18px;
871
+ color: var(--darkGrey);
872
+ }
873
+
874
+ .cache-dialog-content p {
875
+ margin: 0 0 1rem 0;
876
+ color: var(--darkGrey);
877
+ }
878
+
879
+ .cache-warning {
880
+ padding: 1rem;
881
+ background-color: #fff3cd;
882
+ border: 1px solid #ffeaa7;
883
+ border-radius: 4px;
884
+ color: #856404;
885
+ margin-bottom: 1rem;
886
+ }
887
+
888
+ .cache-dialog-actions {
889
+ display: flex;
890
+ justify-content: flex-end;
891
+ gap: 1rem;
892
+ }
893
+
839
894
  }
@@ -276,4 +276,28 @@ window.addEventListener('DOMContentLoaded', () => {
276
276
  }
277
277
  }
278
278
 
279
+ // cache clear all functionality:
280
+ let cacheClearAllButton = document.querySelector('.cache-clear-all-button');
281
+ let cacheConfirmDialog = document.querySelector('.cache-confirm-dialog');
282
+ let cacheDialogCancel = document.querySelector('.cache-dialog-cancel');
283
+ let cacheClearForm = document.querySelector('.cache-clear-form');
284
+ if(cacheClearAllButton && cacheConfirmDialog){
285
+ cacheClearAllButton.addEventListener('click', () => {
286
+ cacheConfirmDialog.showModal();
287
+ });
288
+ }
289
+ if(cacheDialogCancel && cacheConfirmDialog){
290
+ cacheDialogCancel.addEventListener('click', () => {
291
+ cacheConfirmDialog.close();
292
+ });
293
+ }
294
+ if(cacheClearForm){
295
+ cacheClearForm.addEventListener('submit', (event) => {
296
+ let submitButton = cacheClearForm.querySelector('button[type="submit"]');
297
+ if(submitButton){
298
+ submitButton.disabled = true;
299
+ }
300
+ });
301
+ }
302
+
279
303
  });
@@ -0,0 +1,4 @@
1
+ <form class="cache-clean-form" method="POST" action="{{&cacheCleanRoute}}">
2
+ <input type="hidden" name="routeId" value="{{routeId}}"/>
3
+ <button class="button button-warning cache-clean-btn" type="submit">{{&buttonText}}</button>
4
+ </form>
@@ -0,0 +1,18 @@
1
+ <button type="button" class="button button-warning cache-clear-all-button">
2
+ {{buttonText}}
3
+ </button>
4
+ <dialog class="cache-confirm-dialog">
5
+ <div class="cache-dialog-content">
6
+ <h5>{{confirmTitle}}</h5>
7
+ <p>{{confirmMessage}}</p>
8
+ <div class="alert cache-warning">
9
+ <strong>{{warningText}}</strong> {{warningMessage}}
10
+ </div>
11
+ <div class="cache-dialog-actions">
12
+ <button type="button" class="button button-secondary cache-dialog-cancel">{{cancelText}}</button>
13
+ <form method="post" action="{{clearAllCacheRoute}}" class="cache-clear-form">
14
+ <button type="submit" class="button button-danger">{{confirmText}}</button>
15
+ </form>
16
+ </div>
17
+ </div>
18
+ </dialog>
@@ -1 +1 @@
1
- <textarea name="{{&fieldName}}" id="{{&fieldName}}" disabled="disabled">{{&fieldValue}}</textarea>
1
+ <textarea name="{{&fieldName}}" id="{{&fieldName}}" disabled="disabled">{{fieldValue}}</textarea>
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  const { Manager } = require('../index');
10
- const { Logger } = require('@reldens/utils');
10
+ const { Logger, sc } = require('@reldens/utils');
11
11
  const { FileHandler } = require('@reldens/server-utils');
12
12
  const readline = require('readline');
13
13
 
@@ -18,22 +18,84 @@ class CmsEntitiesGenerator
18
18
  {
19
19
  this.args = process.argv.slice(2);
20
20
  this.projectRoot = process.cwd();
21
- this.isOverride = this.args.includes('--override');
22
- this.prismaClientPath = this.extractArgument('--prisma-client');
23
- this.driver = this.extractArgument('--driver') || process.env.RELDENS_STORAGE_DRIVER || 'prisma';
21
+ this.config = {};
22
+ this.parseArguments();
24
23
  }
25
24
 
26
- extractArgument(argumentName)
25
+ parseArguments()
27
26
  {
28
- let argIndex = this.args.indexOf(argumentName);
29
- if(-1 === argIndex || argIndex + 1 >= this.args.length){
30
- return null;
27
+ for(let i = 0; i < this.args.length; i++){
28
+ let arg = this.args[i];
29
+ if(!arg.startsWith('--')){
30
+ continue;
31
+ }
32
+ let equalIndex = arg.indexOf('=');
33
+ if(-1 === equalIndex){
34
+ let flag = arg.substring(2);
35
+ if('override' === flag || 'dry-prisma' === flag || 'help' === flag || 'h' === flag){
36
+ this.config[flag] = true;
37
+ }
38
+ continue;
39
+ }
40
+ let key = arg.substring(2, equalIndex);
41
+ let value = arg.substring(equalIndex + 1);
42
+ this.config[key] = value;
31
43
  }
32
- return this.args[argIndex + 1];
44
+ }
45
+
46
+ shouldShowHelp()
47
+ {
48
+ return 0 === this.args.length || sc.get(this.config, 'help', false) || sc.get(this.config, 'h', false);
49
+ }
50
+
51
+ showHelp()
52
+ {
53
+ Logger.info('');
54
+ Logger.info('Reldens CMS Entities Generator');
55
+ Logger.info('==============================');
56
+ Logger.info('');
57
+ Logger.info('Usage: npx reldens-cms-generate-entities [options]');
58
+ Logger.info('');
59
+ Logger.info('Options:');
60
+ Logger.info(' --prisma-client=[path] Path to Prisma client (e.g., ./prisma/client)');
61
+ Logger.info(' --driver=[driver] Storage driver (default: prisma)');
62
+ Logger.info(' --override Force regeneration and overwrite existing files');
63
+ Logger.info(' --dry-prisma Skip Prisma schema generation');
64
+ Logger.info(' --help, -h Show this help message');
65
+ Logger.info('');
66
+ Logger.info('Examples:');
67
+ Logger.info(' npx reldens-cms-generate-entities --prisma-client=./prisma/client --driver=prisma');
68
+ Logger.info(' npx reldens-cms-generate-entities --override --dry-prisma');
69
+ Logger.info(' npx reldens-cms-generate-entities --help');
70
+ Logger.info('');
71
+ }
72
+
73
+ get isOverride()
74
+ {
75
+ return sc.get(this.config, 'override', false);
76
+ }
77
+
78
+ get isDryPrisma()
79
+ {
80
+ return sc.get(this.config, 'dry-prisma', false);
81
+ }
82
+
83
+ get prismaClientPath()
84
+ {
85
+ return sc.get(this.config, 'prisma-client', null);
86
+ }
87
+
88
+ get driver()
89
+ {
90
+ return sc.get(this.config, 'driver', process.env.RELDENS_STORAGE_DRIVER || 'prisma');
33
91
  }
34
92
 
35
93
  async run()
36
94
  {
95
+ if(this.shouldShowHelp()){
96
+ this.showHelp();
97
+ return true;
98
+ }
37
99
  if(this.isOverride){
38
100
  let confirmed = await this.confirmOverride();
39
101
  if(!confirmed){
@@ -41,6 +103,9 @@ class CmsEntitiesGenerator
41
103
  return false;
42
104
  }
43
105
  }
106
+ if(this.isDryPrisma){
107
+ Logger.info('Running in dry-prisma mode - skipping Prisma schema generation.');
108
+ }
44
109
  let managerConfig = {projectRoot: this.projectRoot};
45
110
  if('prisma' === this.driver){
46
111
  let prismaClient = await this.loadPrismaClient();
@@ -55,7 +120,7 @@ class CmsEntitiesGenerator
55
120
  }
56
121
  Logger.debug('Reldens CMS Manager instance created for entities generation.');
57
122
  await manager.initializeDataServer();
58
- let success = await manager.installer.generateEntities(manager.dataServer, this.isOverride, false);
123
+ let success = await manager.installer.generateEntities(manager.dataServer, this.isOverride, false, this.isDryPrisma);
59
124
  if(!success){
60
125
  Logger.error('Entities generation failed.');
61
126
  return false;
@@ -68,24 +133,26 @@ class CmsEntitiesGenerator
68
133
  {
69
134
  let clientPath = this.prismaClientPath;
70
135
  if(!clientPath){
71
- clientPath = FileHandler.joinPaths(process.cwd(), 'generated-entities', 'prisma');
136
+ return false;
72
137
  }
73
- if(!FileHandler.exists(clientPath)){
74
- Logger.error('Prisma client not found at: '+clientPath);
75
- Logger.error('Please ensure the client exists or specify a custom path with --prisma-client');
138
+ let resolvedPath = clientPath.startsWith('./')
139
+ ? FileHandler.joinPaths(process.cwd(), clientPath.substring(2))
140
+ : clientPath;
141
+ if(!FileHandler.exists(resolvedPath)){
142
+ Logger.error('Prisma client not found at: '+resolvedPath);
76
143
  return false;
77
144
  }
78
145
  try {
79
- let PrismaClientModule = require(clientPath);
146
+ let PrismaClientModule = require(resolvedPath);
80
147
  let PrismaClient = PrismaClientModule.PrismaClient || PrismaClientModule.default?.PrismaClient;
81
148
  if(!PrismaClient){
82
- Logger.error('PrismaClient not found in module: '+clientPath);
149
+ Logger.error('PrismaClient not found in module: '+resolvedPath);
83
150
  return false;
84
151
  }
85
- Logger.debug('Prisma client loaded from: '+clientPath);
152
+ Logger.debug('Prisma client loaded from: '+resolvedPath);
86
153
  return new PrismaClient();
87
154
  } catch (error) {
88
- Logger.error('Failed to load Prisma client from '+clientPath+': '+error.message);
155
+ Logger.error('Failed to load Prisma client from '+resolvedPath+': '+error.message);
89
156
  return false;
90
157
  }
91
158
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  *
5
- * Reldens - CMS - CLI Installer
5
+ * Reldens - CMS - CLI
6
6
  *
7
7
  */
8
8
 
@@ -21,10 +21,10 @@ if(FileHandler.exists(indexPath)){
21
21
 
22
22
  let managerConfig = {projectRoot};
23
23
  let entitiesPath = FileHandler.joinPaths(
24
- projectRoot,
25
- 'generated-entities',
26
- 'models',
27
- 'prisma',
24
+ projectRoot,
25
+ 'generated-entities',
26
+ 'models',
27
+ 'prisma',
28
28
  'registered-models-prisma.js'
29
29
  );
30
30
 
@@ -48,4 +48,4 @@ manager.start().then((result) => {
48
48
  }).catch((error) => {
49
49
  Logger.critical('Failed to start CMS:', error);
50
50
  process.exit();
51
- });
51
+ });