@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.
- package/README.md +92 -9
- package/admin/reldens-admin-client.css +55 -0
- package/admin/reldens-admin-client.js +24 -0
- package/admin/templates/cache-clean-button.html +4 -0
- package/admin/templates/clear-all-cache-button.html +18 -0
- package/admin/templates/fields/view/textarea.html +1 -1
- package/bin/reldens-cms-generate-entities.js +85 -18
- package/bin/reldens-cms.js +6 -6
- package/lib/admin-manager/contents-builder.js +257 -0
- package/lib/admin-manager/router-contents.js +618 -0
- package/lib/admin-manager/router.js +208 -0
- package/lib/admin-manager-validator.js +2 -1
- package/lib/admin-manager.js +116 -990
- package/lib/admin-translations.js +9 -1
- package/lib/cache/add-cache-button-subscriber.js +149 -0
- package/lib/cache/cache-manager.js +168 -0
- package/lib/cache/cache-routes-handler.js +99 -0
- package/lib/cms-pages-route-manager.js +45 -21
- package/lib/frontend.js +288 -71
- package/lib/installer.js +5 -2
- package/lib/json-fields-parser.js +74 -0
- package/lib/manager.js +49 -4
- package/lib/pagination-handler.js +243 -0
- package/lib/search-renderer.js +116 -0
- package/lib/search.js +344 -0
- package/lib/template-engine/collections-single-transformer.js +53 -0
- package/lib/template-engine/collections-transformer-base.js +84 -0
- package/lib/template-engine/collections-transformer.js +353 -0
- package/lib/template-engine/entities-transformer.js +65 -0
- package/lib/template-engine/partials-transformer.js +171 -0
- package/lib/template-engine.js +53 -387
- package/lib/templates-list.js +2 -0
- package/migrations/default-homepage.sql +6 -6
- package/migrations/install.sql +21 -20
- package/package.json +4 -4
- package/templates/page.html +19 -2
- package/templates/partials/entriesListView.html +14 -0
- 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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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">{{¤tPage}}</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,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">{{
|
|
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.
|
|
22
|
-
this.
|
|
23
|
-
this.driver = this.extractArgument('--driver') || process.env.RELDENS_STORAGE_DRIVER || 'prisma';
|
|
21
|
+
this.config = {};
|
|
22
|
+
this.parseArguments();
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
parseArguments()
|
|
27
26
|
{
|
|
28
|
-
let
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
return false;
|
|
72
137
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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(
|
|
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: '+
|
|
149
|
+
Logger.error('PrismaClient not found in module: '+resolvedPath);
|
|
83
150
|
return false;
|
|
84
151
|
}
|
|
85
|
-
Logger.debug('Prisma client loaded from: '+
|
|
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 '+
|
|
155
|
+
Logger.error('Failed to load Prisma client from '+resolvedPath+': '+error.message);
|
|
89
156
|
return false;
|
|
90
157
|
}
|
|
91
158
|
}
|
package/bin/reldens-cms.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
*
|
|
5
|
-
* Reldens - CMS - CLI
|
|
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
|
+
});
|