domma-cms 0.3.0 → 0.5.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 (145) hide show
  1. package/README.md +3 -3
  2. package/admin/css/admin.css +1 -1
  3. package/admin/dist/domma/domma-tools.css +2313 -0
  4. package/admin/dist/domma/domma-tools.min.js +10 -0
  5. package/admin/index.html +4 -0
  6. package/admin/js/api.js +1 -1
  7. package/admin/js/app.js +8 -4
  8. package/admin/js/config/sidebar-config.js +1 -1
  9. package/admin/js/lib/markdown-toolbar.js +18 -10
  10. package/admin/js/templates/action-editor.html +171 -0
  11. package/admin/js/templates/actions-list.html +19 -0
  12. package/admin/js/templates/api-reference.html +1411 -0
  13. package/admin/js/templates/block-editor.html +158 -0
  14. package/admin/js/templates/blocks.html +8 -0
  15. package/admin/js/templates/collection-editor.html +47 -0
  16. package/admin/js/templates/collection-entries.html +3 -0
  17. package/admin/js/templates/collections.html +51 -4
  18. package/admin/js/templates/documentation.html +258 -0
  19. package/{plugins/form-builder/admin → admin/js}/templates/form-editor.html +238 -199
  20. package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
  21. package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
  22. package/admin/js/templates/login.html +29 -4
  23. package/admin/js/templates/my-profile.html +17 -0
  24. package/admin/js/templates/page-editor.html +39 -0
  25. package/admin/js/templates/pages.html +6 -1
  26. package/admin/js/templates/pro-docs.html +259 -0
  27. package/admin/js/templates/role-editor.html +59 -0
  28. package/admin/js/templates/roles.html +10 -0
  29. package/admin/js/templates/settings.html +123 -21
  30. package/admin/js/templates/tutorials.html +81 -0
  31. package/admin/js/templates/user-editor.html +7 -0
  32. package/admin/js/templates/users.html +3 -26
  33. package/admin/js/templates/view-editor.html +201 -0
  34. package/admin/js/templates/view-preview.html +51 -0
  35. package/admin/js/templates/views-list.html +19 -0
  36. package/admin/js/views/action-editor.js +1 -0
  37. package/admin/js/views/actions-list.js +1 -0
  38. package/admin/js/views/api-reference.js +1 -0
  39. package/admin/js/views/block-editor.js +8 -0
  40. package/admin/js/views/blocks.js +4 -0
  41. package/admin/js/views/collection-editor.js +3 -3
  42. package/admin/js/views/collection-entries.js +1 -1
  43. package/admin/js/views/collections.js +1 -1
  44. package/admin/js/views/dashboard.js +1 -1
  45. package/admin/js/views/form-editor.js +8 -0
  46. package/admin/js/views/form-submissions.js +1 -0
  47. package/admin/js/views/forms.js +1 -0
  48. package/admin/js/views/index.js +1 -1
  49. package/admin/js/views/login.js +2 -2
  50. package/admin/js/views/media.js +1 -1
  51. package/admin/js/views/my-profile.js +1 -0
  52. package/admin/js/views/page-editor.js +34 -15
  53. package/admin/js/views/pages.js +5 -5
  54. package/admin/js/views/plugins.js +10 -10
  55. package/admin/js/views/pro-docs.js +1 -0
  56. package/admin/js/views/role-editor.js +1 -0
  57. package/admin/js/views/roles.js +4 -0
  58. package/admin/js/views/settings.js +3 -1
  59. package/admin/js/views/user-editor.js +1 -1
  60. package/admin/js/views/users.js +4 -7
  61. package/admin/js/views/view-editor.js +1 -0
  62. package/admin/js/views/view-preview.js +1 -0
  63. package/admin/js/views/views-list.js +1 -0
  64. package/bin/cli.js +1 -1
  65. package/config/auth.json +1 -0
  66. package/config/connections.json.bak +9 -0
  67. package/config/connections.json.example +9 -0
  68. package/config/plugins.json +19 -29
  69. package/config/server.json +6 -6
  70. package/config/site.json +12 -2
  71. package/package.json +24 -10
  72. package/plugins/example-analytics/stats.json +17 -12
  73. package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
  74. package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
  75. package/plugins/theme-roller/config.js +1 -0
  76. package/plugins/theme-roller/plugin.js +233 -0
  77. package/plugins/theme-roller/plugin.json +31 -0
  78. package/plugins/theme-roller/public/active-theme.css +0 -0
  79. package/plugins/theme-roller/public/inject-head-late.html +1 -0
  80. package/public/css/forms.css +1 -0
  81. package/public/css/site.css +1 -1
  82. package/public/js/forms.js +1 -0
  83. package/public/js/site.js +1 -1
  84. package/scripts/build.js +194 -129
  85. package/scripts/pro.js +254 -0
  86. package/scripts/reset.js +33 -8
  87. package/scripts/seed.js +343 -78
  88. package/scripts/setup.js +1 -0
  89. package/server/middleware/auth.js +136 -120
  90. package/server/routes/api/actions.js +200 -0
  91. package/server/routes/api/auth.js +292 -146
  92. package/server/routes/api/blocks.js +84 -0
  93. package/server/routes/api/collections.js +79 -27
  94. package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +483 -505
  95. package/server/routes/api/layouts.js +49 -39
  96. package/server/routes/api/media.js +118 -92
  97. package/server/routes/api/navigation.js +40 -36
  98. package/server/routes/api/pages.js +132 -118
  99. package/server/routes/api/plugins.js +6 -3
  100. package/server/routes/api/settings.js +104 -88
  101. package/server/routes/api/users.js +27 -19
  102. package/server/routes/api/views.js +148 -0
  103. package/server/routes/public.js +124 -108
  104. package/server/server.js +269 -181
  105. package/server/services/actions.js +387 -0
  106. package/server/services/adapterRegistry.js +98 -0
  107. package/server/services/adapters/FileAdapter.js +192 -0
  108. package/server/services/adapters/MongoAdapter.js +220 -0
  109. package/server/services/blocks.js +162 -0
  110. package/server/services/collections.js +74 -86
  111. package/server/services/connectionManager.js +102 -0
  112. package/server/services/content.js +312 -307
  113. package/server/services/email.js +126 -0
  114. package/server/services/forms.js +173 -0
  115. package/server/services/markdown.js +1378 -747
  116. package/server/services/permissionRegistry.js +173 -0
  117. package/server/services/presetCollections.js +251 -0
  118. package/server/services/renderer.js +75 -1
  119. package/server/services/roles.js +227 -0
  120. package/server/services/rowAccess.js +104 -0
  121. package/server/services/userProfiles.js +199 -0
  122. package/server/services/users.js +281 -212
  123. package/server/services/views.js +280 -0
  124. package/server/templates/page.html +119 -113
  125. package/plugins/form-builder/admin/templates/form-settings.html +0 -29
  126. package/plugins/form-builder/admin/views/form-editor.js +0 -1444
  127. package/plugins/form-builder/admin/views/form-settings.js +0 -38
  128. package/plugins/form-builder/admin/views/form-submissions.js +0 -295
  129. package/plugins/form-builder/admin/views/forms-list.js +0 -164
  130. package/plugins/form-builder/config.js +0 -9
  131. package/plugins/form-builder/data/forms/consent.json +0 -104
  132. package/plugins/form-builder/data/forms/contact-details.json +0 -99
  133. package/plugins/form-builder/data/forms/contacts.json +0 -66
  134. package/plugins/form-builder/data/forms/feedback.json +0 -130
  135. package/plugins/form-builder/data/submissions/consent.json +0 -13
  136. package/plugins/form-builder/data/submissions/contact-details.json +0 -1
  137. package/plugins/form-builder/data/submissions/contacts.json +0 -26
  138. package/plugins/form-builder/data/submissions/feedback.json +0 -1
  139. package/plugins/form-builder/plugin.json +0 -52
  140. package/plugins/form-builder/public/inject-body.html +0 -352
  141. package/plugins/form-builder/public/inject-head.html +0 -58
  142. package/plugins/form-builder/public/package.json +0 -1
  143. package/scripts/copy-domma.js +0 -48
  144. package/server/services/userTypes.js +0 -167
  145. /package/{plugins/form-builder/public → public/js}/form-logic-engine.js +0 -0
@@ -229,5 +229,86 @@ export const myPluginView = {
229
229
  </div>
230
230
  </div>
231
231
 
232
+ <!-- Tutorial: Form Follow-Up -->
233
+ <div class="card card-collapsible mb-4">
234
+ <div class="card-header">
235
+ <h2>Form Follow-Up: Notifications &amp; Actions</h2>
236
+ </div>
237
+ <div class="card-body">
238
+
239
+ <p>Every form in Domma CMS can trigger up to four things after a submission is stored:</p>
240
+ <ol>
241
+ <li>Send an <strong>email notification</strong> to one or more recipients</li>
242
+ <li>POST to a <strong>webhook URL</strong></li>
243
+ <li>Execute a <strong>CMS Action</strong> (Pro)</li>
244
+ <li>Redirect the visitor to a <strong>success page</strong> (or show an inline message)</li>
245
+ </ol>
246
+ <p>These are configured per-form in the admin. Open any form in <a href="#/forms">Forms</a> and go to the
247
+ <strong>Settings</strong> and <strong>Actions</strong> tabs.</p>
248
+
249
+ <hr>
250
+
251
+ <h3>1. Email notification</h3>
252
+ <p>Go to the <strong>Actions</strong> tab of your form and enable <em>Send email on submit</em>. Enter one or
253
+ more comma-separated recipient addresses. This uses the SMTP settings configured in
254
+ <a href="#/settings">Site Settings → Email / SMTP</a>.</p>
255
+
256
+ <pre class="code-block"><code>Recipients: admin@example.com, team@example.com
257
+ Subject Prefix: [Contact Form]</code></pre>
258
+
259
+ <h3>2. Webhook</h3>
260
+ <p>Enable <em>POST to webhook on submit</em> and enter a URL. Domma will POST the following JSON body:</p>
261
+ <pre class="code-block"><code>{
262
+ "form": "enquiries",
263
+ "data": {
264
+ "full_name": "Jane Smith",
265
+ "email": "jane@example.com",
266
+ "message": "Hello!"
267
+ }
268
+ }</code></pre>
269
+ <p>Use this to integrate with Zapier, Make, Slack, or any HTTP endpoint.</p>
270
+
271
+ <h3>3. CMS Action (Pro)</h3>
272
+ <p>Actions are reusable workflow steps defined in <a href="#/actions">Actions</a>. A single action can chain
273
+ multiple steps: update a field, move an entry to another collection, send an email, call a webhook, or
274
+ delete an entry.</p>
275
+ <p>To wire an Action to a form:</p>
276
+ <ol>
277
+ <li>Create an Action in <a href="#/actions">Actions</a> targeting the same collection as your form.</li>
278
+ <li>Open the form in <a href="#/forms">Forms</a> → <strong>Actions</strong> tab → <strong>CMS Action</strong>
279
+ card.</li>
280
+ <li>Select the Action from the dropdown and save.</li>
281
+ </ol>
282
+ <p>The Action runs server-side, after the entry is saved. If it fails (e.g. MongoDB is not configured),
283
+ the submission is still stored — the action failure is non-fatal and logged as a warning.</p>
284
+
285
+ <h3>4. Success message vs. redirect</h3>
286
+ <p>After a successful submission the visitor sees one of two things:</p>
287
+ <ul>
288
+ <li><strong>Inline success message</strong> — the form is replaced by the text set in
289
+ <em>Settings → Success Message</em>. Good for simple acknowledgements.</li>
290
+ <li><strong>Page redirect</strong> — the visitor is sent to the URL set in
291
+ <em>Settings → Success Redirect URL</em>. Good for registration flows, checkouts, or when you want
292
+ a full thank-you page with additional content. <strong>Takes priority</strong> if both are set.</li>
293
+ </ul>
294
+ <p>Example: set <em>Success Redirect URL</em> to <code>/thank-you</code> and create a <code>thank-you.md</code>
295
+ page in the CMS with any content you like.</p>
296
+
297
+ <h3>Execution order</h3>
298
+ <p>On every submission, the pipeline runs in this fixed order:</p>
299
+ <ol>
300
+ <li>Validate fields + honeypot + rate limit</li>
301
+ <li>Store entry to collection</li>
302
+ <li>Send email (if enabled)</li>
303
+ <li>Call webhook (if enabled)</li>
304
+ <li>Execute CMS Action (if set)</li>
305
+ <li>Return success response → client redirects or shows message</li>
306
+ </ol>
307
+ <p>Steps 3–5 are non-fatal: a failure in any of them is logged as a warning but does not prevent the
308
+ submission from being stored or the success response from being returned.</p>
309
+
310
+ </div>
311
+ </div>
312
+
232
313
  </div>
233
314
  </div>
@@ -10,3 +10,10 @@
10
10
  <div id="user-form-container"></div>
11
11
  </div>
12
12
  </div>
13
+
14
+ <div class="card mt-4" id="profile-card" style="display:none;">
15
+ <div class="card-header"><h2><span data-icon="user"></span> Profile</h2></div>
16
+ <div class="card-body">
17
+ <div id="profile-form-container"></div>
18
+ </div>
19
+ </div>
@@ -5,31 +5,8 @@
5
5
  </div>
6
6
  </div>
7
7
 
8
- <div class="tabs" id="users-tabs">
9
- <div class="tab-list">
10
- <button class="tab-item active" data-tab="users">Users</button>
11
- <button class="tab-item" data-tab="user-types">User Types</button>
12
- </div>
13
- <div class="tab-content">
14
- <div class="tab-panel active" id="panel-users">
15
- <div class="card">
16
- <div class="card-body">
17
- <div id="users-table"></div>
18
- </div>
19
- </div>
20
- </div>
21
- <div class="tab-panel" id="panel-user-types">
22
- <div class="card">
23
- <div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
24
- <strong>User Types</strong>
25
- <button class="btn btn-primary btn-sm" id="btn-add-user-type">
26
- <span data-icon="plus"></span> Add User Type
27
- </button>
28
- </div>
29
- <div class="card-body">
30
- <div id="user-types-table"></div>
31
- </div>
32
- </div>
33
- </div>
8
+ <div class="card">
9
+ <div class="card-body">
10
+ <div id="users-table"></div>
34
11
  </div>
35
12
  </div>
@@ -0,0 +1,201 @@
1
+ <div class="view-header">
2
+ <h1><span data-icon="eye"></span> <span id="view-editor-title">New View</span></h1>
3
+ <div style="display:flex;gap:.5rem;">
4
+ <a href="#/views" class="btn btn-ghost btn-sm">
5
+ <span data-icon="arrow-left"></span> All Views
6
+ </a>
7
+ <button id="save-view-btn" class="btn btn-primary">
8
+ <span data-icon="save"></span> Save View
9
+ </button>
10
+ </div>
11
+ </div>
12
+
13
+ <div class="tabs" id="view-editor-tabs">
14
+ <div class="tab-list">
15
+ <button class="tab-item active">Source</button>
16
+ <button class="tab-item">Pipeline</button>
17
+ <button class="tab-item">Display</button>
18
+ <button class="tab-item">Access</button>
19
+ </div>
20
+ <div class="tab-content">
21
+
22
+ <!-- Source tab -->
23
+ <div class="tab-panel active">
24
+ <div class="card">
25
+ <div class="card-body" style="display:flex;flex-direction:column;gap:1rem;max-width:600px;">
26
+ <div>
27
+ <label class="form-label">Title <span style="color:var(--dm-danger,#f87171);">*</span></label>
28
+ <input id="view-title" type="text" class="form-input" placeholder="e.g. Active Premium Users">
29
+ </div>
30
+ <div>
31
+ <label class="form-label">Slug</label>
32
+ <input id="view-slug" type="text" class="form-input" placeholder="Auto-generated from title">
33
+ <small class="text-muted">URL-safe identifier. Leave blank to auto-generate.</small>
34
+ </div>
35
+ <div>
36
+ <label class="form-label">Description</label>
37
+ <textarea id="view-description" class="form-input" rows="2" placeholder="Describe what this view shows…"></textarea>
38
+ </div>
39
+ <div>
40
+ <label class="form-label">Source Collection <span style="color:var(--dm-danger,#f87171);">*</span></label>
41
+ <select id="view-source" class="form-input">
42
+ <option value="">— select collection —</option>
43
+ </select>
44
+ <small class="text-muted">Changing the collection will refresh field lists in Pipeline and
45
+ Display tabs.</small>
46
+ </div>
47
+ <div>
48
+ <label class="form-label">Connection</label>
49
+ <select id="view-connection" class="form-input">
50
+ <option value="default">default</option>
51
+ </select>
52
+ <small class="text-muted">MongoDB connection to use for execution.</small>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+
58
+ <!-- Pipeline tab -->
59
+ <div class="tab-panel">
60
+ <div class="card mb-3">
61
+ <div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
62
+ <h2>Aggregation Stages</h2>
63
+ <div style="display:flex;gap:.5rem;">
64
+ <select id="add-stage-type" class="form-input form-input--sm">
65
+ <option value="$match">$match — filter rows</option>
66
+ <option value="$sort">$sort — order rows</option>
67
+ <option value="$lookup">$lookup — join collection</option>
68
+ <option value="$project">$project — reshape fields</option>
69
+ <option value="$unwind">$unwind — flatten array</option>
70
+ <option value="$addFields">$addFields — compute fields</option>
71
+ <option value="$group">$group — aggregate</option>
72
+ </select>
73
+ <button id="add-stage-btn" class="btn btn-ghost btn-sm">
74
+ <span data-icon="plus"></span> Add Stage
75
+ </button>
76
+ </div>
77
+ </div>
78
+ <div class="card-body">
79
+ <div id="pipeline-stages-list">
80
+ <p class="text-muted stage-empty-placeholder" style="text-align:center;padding:2rem 0;">
81
+ No stages yet. Add a stage to filter, join, or transform your data.
82
+ </p>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ <div class="card">
87
+ <div class="card-body" style="font-size:.8rem;color:var(--dm-text-muted,#888);">
88
+ <strong>Guided stages</strong> ($match, $sort) show a visual condition builder.
89
+ Other stages use a raw JSON textarea — enter only the <em>inner</em> config (e.g. for $lookup, enter
90
+ <code>{ from, localField, foreignField, as }</code> — do not wrap in <code>{ "$lookup": ... }</code>).
91
+ Field dropdowns are populated from the source collection — select a collection on the Source tab
92
+ first.
93
+ </div>
94
+ </div>
95
+ </div>
96
+
97
+ <!-- Display tab -->
98
+ <div class="tab-panel">
99
+ <div class="card">
100
+ <div class="card-body" style="display:flex;flex-direction:column;gap:1.25rem;max-width:640px;">
101
+ <div>
102
+ <label class="form-label">Display Mode</label>
103
+ <select id="view-display-mode" class="form-input">
104
+ <option value="table">Table</option>
105
+ <option value="list">List</option>
106
+ <option value="block">Block</option>
107
+ </select>
108
+ </div>
109
+ <div id="view-block-section" style="display:none;">
110
+ <label class="form-label">Block Template</label>
111
+ <select id="view-block-name" class="form-input">
112
+ <option value="">— select block —</option>
113
+ </select>
114
+ <small class="text-muted">
115
+ Choose a reusable HTML block template.
116
+ <a href="#/blocks" style="margin-left:.35rem;">Manage blocks</a>
117
+ </small>
118
+ </div>
119
+ <div>
120
+ <label class="form-label">Page Size</label>
121
+ <input id="view-page-size" type="number" class="form-input" value="25" min="1" max="200">
122
+ </div>
123
+ <div id="view-columns-section">
124
+ <label class="form-label">Columns</label>
125
+ <small class="text-muted" style="display:block;margin-bottom:.75rem;">
126
+ Choose which fields to display and their labels.
127
+ Field dropdowns are auto-populated from the source collection.
128
+ </small>
129
+ <div id="view-columns-builder" style="min-height:2rem;"></div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </div>
134
+
135
+ <!-- Access tab -->
136
+ <div class="tab-panel">
137
+ <div class="card mb-3">
138
+ <div class="card-body" style="max-width:500px;">
139
+ <div style="margin-bottom:1.25rem;">
140
+ <label class="form-label">Allowed Roles</label>
141
+ <small class="text-muted" style="display:block;margin-bottom:.75rem;">
142
+ Select which roles can access this view.
143
+ </small>
144
+ <div id="view-roles-checkboxes" style="display:flex;flex-direction:column;gap:.5rem;"></div>
145
+ </div>
146
+ <div>
147
+ <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
148
+ <input type="checkbox" id="view-public">
149
+ <span>Public (no authentication required)</span>
150
+ </label>
151
+ <small class="text-muted" style="display:block;margin-top:.4rem;">
152
+ When enabled, the <code>/views/:slug/public</code> endpoint requires no login.
153
+ </small>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ <div class="card">
158
+ <div class="card-header"><h2>Row-Level Access</h2></div>
159
+ <div class="card-body" style="max-width:500px;display:flex;flex-direction:column;gap:1rem;">
160
+ <div>
161
+ <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
162
+ <input type="checkbox" id="view-rowlevel-enabled">
163
+ <span>Filter results to current user's entries</span>
164
+ </label>
165
+ <small class="text-muted" style="display:block;margin-top:.4rem;">
166
+ When enabled, each user sees only their own matching entries.
167
+ Admins always see all results.
168
+ </small>
169
+ </div>
170
+ <div id="view-rowlevel-config" style="display:none;flex-direction:column;gap:1rem;">
171
+ <div>
172
+ <label class="form-label">Mode</label>
173
+ <select id="view-rowlevel-mode" class="form-input">
174
+ <option value="owner">Owner — entry was created by the current user</option>
175
+ <option value="field">Field Match — a field on the entry matches the user</option>
176
+ </select>
177
+ </div>
178
+ <div id="view-rowlevel-field-group" style="display:none;">
179
+ <label class="form-label">Field Name</label>
180
+ <input id="view-rowlevel-field" type="text" class="form-input"
181
+ placeholder="e.g. assigned_to">
182
+ <small class="text-muted">The entry field whose value must match the user property
183
+ below.</small>
184
+ </div>
185
+ <div>
186
+ <label class="form-label">User Property</label>
187
+ <select id="view-rowlevel-userkey" class="form-input">
188
+ <option value="id">id (UUID)</option>
189
+ <option value="email">email</option>
190
+ <option value="name">name</option>
191
+ <option value="role">role</option>
192
+ </select>
193
+ <small class="text-muted">Which property of the logged-in user to compare against.</small>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </div>
199
+
200
+ </div>
201
+ </div>
@@ -0,0 +1,51 @@
1
+ <div class="view-header">
2
+ <h1><span data-icon="eye"></span> <span id="preview-title">View Preview</span></h1>
3
+ <div style="display:flex;gap:.5rem;align-items:center;">
4
+ <a id="preview-edit-btn" href="#/views" class="btn btn-ghost btn-sm">
5
+ <span data-icon="edit-3"></span> Edit View
6
+ </a>
7
+ <a href="#/views" class="btn btn-ghost btn-sm">
8
+ <span data-icon="arrow-left"></span> All Views
9
+ </a>
10
+ <button id="preview-run-btn" class="btn btn-primary">
11
+ <span data-icon="play"></span> Run
12
+ </button>
13
+ </div>
14
+ </div>
15
+
16
+ <div class="card mb-3" id="preview-meta-card" style="display:none;">
17
+ <div class="card-body" style="display:flex;gap:2rem;flex-wrap:wrap;font-size:.875rem;color:var(--dm-text-muted,#888);">
18
+ <span>Source: <strong id="preview-source"></strong></span>
19
+ <span>Connection: <strong id="preview-connection"></strong></span>
20
+ <span>Mode: <strong id="preview-mode"></strong></span>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="card mb-3" id="preview-pagination" style="display:none;">
25
+ <div class="card-body" style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap;">
26
+ <span id="preview-count" class="text-muted" style="font-size:.875rem;"></span>
27
+ <div style="display:flex;gap:.4rem;margin-left:auto;">
28
+ <button id="preview-prev" class="btn btn-ghost btn-sm" disabled>
29
+ <span data-icon="chevron-left"></span> Prev
30
+ </button>
31
+ <span id="preview-page-label" style="padding:.25rem .5rem;font-size:.875rem;align-self:center;"></span>
32
+ <button id="preview-next" class="btn btn-ghost btn-sm" disabled>
33
+ Next <span data-icon="chevron-right"></span>
34
+ </button>
35
+ </div>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="card">
40
+ <div class="card-body">
41
+ <div id="preview-placeholder" style="text-align:center;padding:3rem;color:var(--dm-text-muted,#888);">
42
+ <span data-icon="play" data-icon-size="48" style="opacity:.3;display:block;margin-bottom:1rem;"></span>
43
+ Click <strong>Run</strong> to execute this view and see results.
44
+ </div>
45
+ <div id="preview-table" style="display:none;"></div>
46
+ <div id="preview-list" style="display:none;"></div>
47
+ <div id="preview-empty" style="display:none;text-align:center;padding:2rem;color:var(--dm-text-muted,#888);">
48
+ No results returned.
49
+ </div>
50
+ </div>
51
+ </div>
@@ -0,0 +1,19 @@
1
+ <div class="view-header">
2
+ <h1><span data-icon="eye"></span> Views</h1>
3
+ <button id="create-view-btn" class="btn btn-primary">
4
+ <span data-icon="plus"></span> New View
5
+ </button>
6
+ </div>
7
+
8
+ <div id="views-pro-notice" class="card mb-3" style="display:none;">
9
+ <div class="card-body" style="color:var(--dm-warning,#f59e0b);display:flex;align-items:center;gap:.6rem;">
10
+ <span data-icon="alert-triangle"></span>
11
+ <span>Views require a MongoDB connection (pro mode). Configure one under Collections → Options.</span>
12
+ </div>
13
+ </div>
14
+
15
+ <div class="card">
16
+ <div class="card-body">
17
+ <div id="views-table"></div>
18
+ </div>
19
+ </div>
@@ -0,0 +1 @@
1
+ import{api as S}from"../api.js";let _=null;const z={deleteEntry:[],moveToCollection:[{name:"targetCollection",label:"Target Collection Slug",placeholder:"e.g. archived-applications"}],webhook:[{name:"url",label:"URL",placeholder:"https://hooks.example.com/notify"},{name:"method",label:"Method",placeholder:"POST"},{name:"body",label:"Body (JSON)",placeholder:'{"email": "{{entry.data.email}}"}',multiline:!0}],email:[{name:"to",label:"To",placeholder:"{{entry.data.email}}"},{name:"subject",label:"Subject",placeholder:"Your application update"},{name:"template",label:"Body",placeholder:"Your application has been approved.",multiline:!0}]};export const actionEditorView={templateUrl:"/admin/js/templates/action-editor.html",async onMount(e){_=null;const o=window.location.hash.match(/\/actions\/edit\/([^/?#]+)/);o&&(_=o[1]),E.tabs(e.find("#action-editor-tabs").get(0)),await D(e),await J(e),_&&(e.find("#action-editor-title").text("Edit Action"),await P(e,_)),e.find("#add-step-btn").off("click").on("click",()=>{const n=e.find("#add-step-type").val()||"updateField";B(e,{type:n,config:{}})}),e.find("#save-action-btn").off("click").on("click",async()=>{await K(e)}),U(e),Domma.icons.scan()}};async function D(e){const t=e.find("#action-collection").get(0);if(t)try{(await S.collections.list()).forEach(n=>{const i=document.createElement("option");i.value=n.slug,i.textContent=`${n.title} (${n.slug})`,t.appendChild(i)})}catch{}}async function J(e){const t=e.find("#action-roles-checkboxes").get(0);if(!t)return;["admin","manager","editor","subscriber"].forEach(n=>{const i=document.createElement("label");i.style.cssText="display:flex;align-items:center;gap:.5rem;cursor:pointer;";const l=document.createElement("input");l.type="checkbox",l.value=n,l.dataset.role=n,l.className="action-role-cb",l.checked=n==="admin",i.appendChild(l),i.appendChild(document.createTextNode(n)),t.appendChild(i)})}async function P(e,t){try{const o=await S.actions.get(t);if(!o){E.toast("Action not found.",{type:"error"}),R.navigate("/actions");return}j(e,o)}catch(o){E.toast(o.message||"Failed to load action.",{type:"error"}),R.navigate("/actions")}}function U(e){const t=e.find("#action-rowlevel-enabled").get(0),o=e.find("#action-rowlevel-config").get(0),n=e.find("#action-rowlevel-mode").get(0),i=e.find("#action-rowlevel-field-group").get(0);t&&(t.addEventListener("change",()=>{o&&(o.style.display=t.checked?"flex":"none")}),n&&n.addEventListener("change",()=>{i&&(i.style.display=n.value==="field"?"":"none")}))}function j(e,t){e.find("#action-title").val(t.title||""),e.find("#action-slug").val(t.slug||""),e.find("#action-description").val(t.description||""),e.find("#action-collection").val(t.collection||""),e.find("#action-trigger-type").val(t.trigger?.type||"manual"),e.find("#action-trigger-label").val(t.trigger?.label||"Run"),e.find("#action-trigger-icon").val(t.trigger?.icon||"zap"),e.find("#action-trigger-confirm").val(t.trigger?.confirmMessage||"");const o=t.access?.roles||["admin"];e.find(".action-role-cb").each(function(){this.checked=o.includes(this.value)});const n=t.access?.rowLevel;n&&(e.find("#action-rowlevel-enabled").prop("checked",!0),e.find("#action-rowlevel-config").css("display","flex"),e.find("#action-rowlevel-mode").val(n.mode||"owner"),e.find("#action-rowlevel-userkey").val(n.userKey||"id"),n.mode==="field"&&(e.find("#action-rowlevel-field-group").css("display",""),e.find("#action-rowlevel-field").val(n.field||"")));const i=e.find("#action-steps-list").get(0);if(i){const l=i.querySelector(".steps-empty-placeholder");for(l&&l.remove();i.firstChild;)i.removeChild(i.firstChild);const a=document.createElement("p");a.className="text-muted steps-empty-placeholder",a.textContent="No steps yet. Add a step to define what this action does.",a.style.cssText="text-align:center;padding:2rem 0;",i.appendChild(a)}(t.steps||[]).forEach(l=>B(e,l))}function B(e,t){const o=e.find("#action-steps-list").get(0);if(!o)return;const n=o.querySelector(".steps-empty-placeholder");n&&n.remove();const i=z[t.type]||[],l=document.createElement("div");l.className="card mb-2 step-card",l.dataset.stepType=t.type;const a=document.createElement("div");a.className="card-header",a.style.cssText="display:flex;align-items:center;gap:.5rem;";const s=document.createElement("code");s.textContent=t.type,s.style.cssText="flex:1;font-size:.85rem;";const p=document.createElement("button");p.type="button",p.className="btn btn-sm btn-danger";const g=document.createElement("span");g.setAttribute("data-icon","trash-2"),p.appendChild(g),p.addEventListener("click",()=>{if(l.remove(),!o.querySelector(".step-card")){const c=document.createElement("p");c.className="text-muted steps-empty-placeholder",c.textContent="No steps yet. Add a step to define what this action does.",c.style.cssText="text-align:center;padding:2rem 0;",o.appendChild(c)}}),a.appendChild(s),a.appendChild(p);const u=document.createElement("div");if(u.className="card-body",i.length===0){const c=document.createElement("p");c.className="text-muted",c.textContent=t.type==="deleteEntry"?"This step deletes the entry. No configuration required.":"No additional configuration required.",c.style.margin="0",u.appendChild(c)}t.type==="updateField"?G(u,t,e):i.forEach(c=>{const h=document.createElement("div");h.style.cssText="margin-bottom:.75rem;";const b=document.createElement("label");b.className="form-label",b.textContent=c.label,h.appendChild(b);let m;c.multiline?(m=document.createElement("textarea"),m.rows=3,m.style.cssText="font-family:monospace;font-size:.8rem;resize:vertical;",m.value=typeof t.config?.[c.name]=="object"?JSON.stringify(t.config[c.name],null,2):t.config?.[c.name]??""):(m=document.createElement("input"),m.type="text",m.value=t.config?.[c.name]??""),m.className=`form-input step-field-${c.name}`,m.placeholder=c.placeholder||"",m.dataset.field=c.name,h.appendChild(m),u.appendChild(h)}),l.appendChild(a),l.appendChild(u),o.appendChild(l),Domma.icons.scan(l)}async function G(e,t,o){const n=o.find("#action-collection").val(),i=document.createElement("div");i.style.cssText="margin-bottom:.75rem;";const l=document.createElement("label");l.className="form-label",l.textContent="Field",i.appendChild(l);const a=document.createElement("select");a.className="form-input step-field-field",a.dataset.field="field";const s=document.createElement("input");s.type="text",s.className="form-input mt-2 step-field-field-custom",s.placeholder="Field name, e.g. status",s.style.display="none";const p=document.createElement("div");p.style.cssText="margin-bottom:.75rem;";const g=document.createElement("label");g.className="form-label",g.textContent="New Value",p.appendChild(g);const u=document.createElement("div");p.appendChild(u);const c=document.createElement("button");c.type="button",c.className="btn btn-ghost btn-sm mt-2",c.style.cssText="font-size:.75rem;padding:.2rem .5rem;",c.textContent="Template Variables";const h=document.createElement("div");h.style.cssText="display:none;background:var(--dm-surface-2,#1e1e2e);border-radius:.4rem;padding:.75rem;margin-top:.5rem;font-size:.8rem;line-height:1.7;";const b=document.createElement("strong");b.textContent="Available template variables:",h.appendChild(b);const m=document.createElement("table");m.style.cssText="width:100%;border-collapse:collapse;margin-top:.4rem;",[["{{now}}","Current ISO timestamp"],["{{user.id}}","Executing user's ID"],["{{user.name}}","Executing user's name"],["{{user.email}}","Executing user's email"],["{{entry.id}}","Entry's unique ID"],["{{entry.data.fieldName}}","Any field value from the entry"],["{{env.CMS_PUBLIC_*}}","Public environment variables"]].forEach(([r,f])=>{const x=document.createElement("tr"),w=document.createElement("td");w.style.cssText="padding:.2rem .5rem .2rem 0;opacity:.7;white-space:nowrap;";const T=document.createElement("code");T.textContent=r,w.appendChild(T);const d=document.createElement("td");d.textContent=f,x.appendChild(w),x.appendChild(d),m.appendChild(x)}),h.appendChild(m),c.addEventListener("click",()=>{const r=h.style.display!=="none";h.style.display=r?"none":"",c.textContent=r?"Template Variables":"Hide Variables"}),p.appendChild(c),p.appendChild(h),e.appendChild(i),e.appendChild(p);let k=[];if(n)try{k=(await S.collections.get(n)).fields||[]}catch{}const F=document.createElement("option");F.value="",F.textContent=k.length?"\u2014 select a field \u2014":"\u2014 no fields available \u2014",a.appendChild(F),k.forEach(r=>{const f=document.createElement("option");f.value=r.name,f.textContent=`${r.label} (${r.name})`,f.dataset.fieldType=r.type,f.dataset.fieldOptions=r.type==="select"?JSON.stringify(r.options||[]):"",a.appendChild(f)});const L=document.createElement("option");L.value="__custom__",L.textContent="\u2014 enter manually \u2014",a.appendChild(L);const C=t.config?.field||"",I=C&&[...a.options].find(r=>r.value===C);I?a.value=C:C&&(a.value="__custom__",s.value=C,s.style.display=""),i.appendChild(a),i.appendChild(s);function V(r,f){u.textContent="";const x=r?[...a.options].find(d=>d.value===r):null,w=x?.dataset.fieldType==="select",T=w?JSON.parse(x.dataset.fieldOptions||"[]"):[];if(w&&T.length){const d=document.createElement("select");d.className="form-input step-field-value",d.dataset.field="value";const O=document.createElement("option");O.value="",O.textContent="\u2014 select a value \u2014",d.appendChild(O),T.forEach(v=>{const N=typeof v=="string"?v:v.value??"",M=typeof v=="string"?v:v.label||v.value||N;if(!N||N==="undefined")return;const A=document.createElement("option");A.value=N,A.textContent=M,d.appendChild(A)});const q=document.createElement("option");q.value="__custom__",q.textContent="\u2014 enter manually \u2014",d.appendChild(q);const y=document.createElement("input");y.type="text",y.className="form-input mt-2",y.placeholder="e.g. approved or {{now}}",y.style.display="none",f&&[...d.options].find(v=>v.value===f&&v.value!=="__custom__")?d.value=f:f&&(d.value="__custom__",y.value=f,y.style.display=""),d.addEventListener("change",()=>{const v=d.value==="__custom__";y.style.display=v?"":"none",v||(y.value="")}),u.appendChild(d),u.appendChild(y)}else{const d=document.createElement("input");d.type="text",d.className="form-input step-field-value",d.dataset.field="value",d.placeholder="e.g. approved or {{now}}",d.value=f||"",u.appendChild(d)}}V(I?C:null,t.config?.value||""),a.addEventListener("change",()=>{const r=a.value==="__custom__";s.style.display=r?"":"none",r||(s.value=""),V(r?null:a.value,"")})}function H(e){const t=[];return e.find(".step-card").each(function(){const o=this.dataset.stepType,n={};if(o==="updateField"){let l=this.querySelector(".step-field-field")?.value?.trim()||"";l==="__custom__"&&(l=this.querySelector(".step-field-field-custom")?.value?.trim()||""),n.field=l;const a=this.querySelector(".step-field-value");let s=a?.value?.trim()||"";s==="__custom__"&&(s=a?.nextElementSibling?.value?.trim()||""),n.value=s}else(z[o]||[]).forEach(l=>{const a=this.querySelector(`.step-field-${l.name}`);if(!a)return;const s=a.value.trim();if(l.multiline&&s)try{n[l.name]=JSON.parse(s)}catch{n[l.name]=s}else n[l.name]=s});t.push({type:o,config:n})}),t}async function K(e){const t=e.find("#action-title").val().trim();if(!t){E.toast("Title is required.",{type:"warning"});return}const o=e.find("#action-collection").val();if(!o){E.toast("Target collection is required (General tab).",{type:"warning"});return}const n=[];e.find(".action-role-cb:checked").each(function(){n.push(this.value)});const i=e.find("#action-rowlevel-enabled").is(":checked");let l=null;if(i){const p=e.find("#action-rowlevel-mode").val()||"owner",g=e.find("#action-rowlevel-userkey").val()||"id";if(l={mode:p,userKey:g},p==="field"){const u=e.find("#action-rowlevel-field").val().trim();if(!u){E.toast("Field name is required for Field Match mode.",{type:"warning"});return}l.field=u}}const a={title:t,slug:e.find("#action-slug").val().trim()||void 0,description:e.find("#action-description").val().trim(),collection:o,trigger:{type:e.find("#action-trigger-type").val()||"manual",label:e.find("#action-trigger-label").val().trim()||"Run",icon:e.find("#action-trigger-icon").val().trim()||"zap",confirmMessage:e.find("#action-trigger-confirm").val().trim()||null},steps:H(e),access:{roles:n,rowLevel:l}},s=e.find("#save-action-btn").get(0);s&&(s.disabled=!0);try{if(_)await S.actions.update(_,a),E.toast("Action updated.",{type:"success"});else{const p=await S.actions.create(a);E.toast("Action created.",{type:"success"}),R.navigate(`/actions/edit/${p.slug}`)}}catch(p){E.toast(p.message||"Failed to save action.",{type:"error"})}finally{s&&(s.disabled=!1)}}
@@ -0,0 +1 @@
1
+ import{api as i}from"../api.js";function a(o){return String(o).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}export const actionsListView={templateUrl:"/admin/js/templates/actions-list.html",async onMount(o){await r(o),o.find("#create-action-btn").off("click").on("click",()=>{R.navigate("/actions/new")}),Domma.icons.scan()}};async function r(o){let s=[];try{s=await i.actions.list(),o.find("#actions-pro-notice").hide()}catch(e){e.message?.includes("MongoDB")||e.message?.includes("pro mode")||e.message?.includes("connection")?o.find("#actions-pro-notice").show():E.toast("Could not load actions.",{type:"error"})}T.create("#actions-table",{data:s,columns:[{key:"title",title:"Title",render:(e,t)=>{const n=document.createElement("a");return n.href=`#/actions/edit/${a(t.slug)}`,n.textContent=e,n.style.fontWeight="600",n.outerHTML}},{key:"slug",title:"Slug",render:e=>`<code>${a(e)}</code>`},{key:"collection",title:"Collection",render:e=>`<code>${a(e||"\u2014")}</code>`},{key:"trigger",title:"Trigger",render:e=>a(e?.label||e?.type||"manual")},{key:"steps",title:"Steps",render:e=>String(Array.isArray(e)?e.length:0)},{key:"access",title:"Roles",render:e=>(e?.roles||[]).map(t=>`<span class="badge badge-secondary">${a(t)}</span>`).join(" ")},{key:"slug",title:"Actions",render:e=>{const t=document.createElement("div");t.style.cssText="display:flex;gap:.4rem;justify-content:flex-end;";const n=document.createElement("a");n.href=`#/actions/edit/${a(e)}`,n.className="btn btn-sm btn-primary",n.textContent="Edit";const c=document.createElement("button");return c.className="btn btn-sm btn-danger js-delete-action",c.dataset.slug=e,c.textContent="Delete",t.appendChild(n),t.appendChild(c),t.outerHTML}}],emptyMessage:'No actions yet. Click "New Action" to create your first action.'}),document.querySelectorAll(".js-delete-action").forEach(e=>{e.addEventListener("click",async()=>{const t=e.dataset.slug;if(await E.confirm(`Delete action "${t}"? This cannot be undone.`))try{await i.actions.delete(t),E.toast("Action deleted.",{type:"success"}),await r(o)}catch{E.toast("Failed to delete action.",{type:"error"})}})}),Domma.icons.scan()}
@@ -0,0 +1 @@
1
+ export const apiReferenceView={templateUrl:"/admin/js/templates/api-reference.html",async onMount(e){Domma.icons.scan(),Domma.syntax.scan()}};
@@ -0,0 +1,8 @@
1
+ import{api as p}from"../api.js";let d=null;export const blockEditorView={templateUrl:"/admin/js/templates/block-editor.html",async onMount(e){d=null;const s=window.location.hash.match(/\/blocks\/edit\/([^/?#]+)/);s&&(d=decodeURIComponent(s[1]));const i=e.find("#block-name").get(0),n=e.find("#block-content").get(0);if(d){e.find("#block-editor-title").text("Edit Block"),i&&(i.value=d,i.disabled=!0);try{const c=await p.blocks.get(d);n&&(n.value=c.content||"")}catch(c){E.toast(c.message||"Block not found.",{type:"error"}),R.navigate("/blocks");return}}n&&(v(n,e),w(n,e)),e.find("#save-block-btn").off("click").on("click",async()=>{await L(e)}),Domma.icons.scan()}};function v(e,t){const s=t.find("#block-line-numbers").get(0),i=t.find("#block-cursor-pos").get(0);function n(){const l=e.value.split(`
2
+ `).length;s.textContent=Array.from({length:l},(o,r)=>r+1).join(`
3
+ `),s.scrollTop=e.scrollTop}n(),e.addEventListener("input",n),e.addEventListener("scroll",()=>{s.scrollTop=e.scrollTop});function c(){if(!i)return;const o=e.value.slice(0,e.selectionStart).split(`
4
+ `);i.textContent=`Ln ${o.length}, Col ${o[o.length-1].length+1}`}e.addEventListener("keyup",c),e.addEventListener("click",c),e.addEventListener("keydown",l=>{if(l.key==="Tab"){l.preventDefault();const o=e.selectionStart,r=e.selectionEnd;if(!l.shiftKey)e.value=e.value.slice(0,o)+" "+e.value.slice(r),e.selectionStart=e.selectionEnd=o+2;else{const a=e.value.lastIndexOf(`
5
+ `,o-1)+1,m=e.value.slice(a,a+2),f=m===" "?2:m[0]===" "?1:0;f>0&&(e.value=e.value.slice(0,a)+e.value.slice(a+f),e.selectionStart=e.selectionEnd=Math.max(a,o-f))}e.dispatchEvent(new Event("input"))}if(l.key==="Enter"){l.preventDefault();const o=e.selectionStart,r=e.value.lastIndexOf(`
6
+ `,o-1)+1,a=e.value.slice(r,o).match(/^(\s*)/)[1];e.value=e.value.slice(0,o)+`
7
+ `+a+e.value.slice(e.selectionEnd),e.selectionStart=e.selectionEnd=o+1+a.length,e.dispatchEvent(new Event("input"))}});const u=t.find("#block-editor-toolbar").get(0);u&&u.addEventListener("click",l=>{const o=l.target.closest("[data-action]");o&&(b(o.dataset.action,e),e.focus())})}function b(e,t){switch(e){case"undo":document.execCommand("undo");break;case"redo":document.execCommand("redo");break;case"cut":t.selectionStart!==t.selectionEnd&&(navigator.clipboard.writeText(t.value.slice(t.selectionStart,t.selectionEnd)).catch(()=>{}),document.execCommand("cut"));break;case"copy":t.selectionStart!==t.selectionEnd&&navigator.clipboard.writeText(t.value.slice(t.selectionStart,t.selectionEnd)).catch(()=>{});break;case"paste":navigator.clipboard.readText().then(s=>{const i=t.selectionStart;t.value=t.value.slice(0,i)+s+t.value.slice(t.selectionEnd),t.selectionStart=t.selectionEnd=i+s.length,t.dispatchEvent(new Event("input"))}).catch(()=>E.toast("Use Ctrl+V to paste.",{type:"info"}));break;case"select-all":t.select();break;case"format":g(t);break}}function g(e){const t=e.value.match(/(<[^>]+>|[^<]+)/g)||[],s=[];let i=0;const n=" ";for(const l of t){const o=l.trim();if(o)if(o.startsWith("</"))i=Math.max(0,i-1),s.push(n.repeat(i)+o);else if(o.startsWith("<")&&!o.startsWith("<!")&&!o.endsWith("/>")){s.push(n.repeat(i)+o);const r=(o.match(/^<(\w+)/)||[])[1]||"";k.has(r.toLowerCase())||i++}else s.push(n.repeat(i)+o)}const c=s.join(`
8
+ `),u=e.selectionStart;e.value=c,e.selectionStart=e.selectionEnd=Math.min(u,c.length),e.dispatchEvent(new Event("input"))}const k=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]);function w(e,t){const s=t.find("#block-editor-body").get(0),i=t.find("#block-sample-data").get(0),n=t.find("#block-preview-output");t.find("[data-mode]").each(function(){this.addEventListener("click",()=>{t.find("[data-mode]").each(function(){this.classList.remove("active")}),this.classList.add("active"),s.className=`editor-body editor-mode-${this.dataset.mode}`,this.dataset.mode!=="write"&&c()})}),e.addEventListener("input",()=>{h(i,e),c()}),e.value.trim()&&h(i,e);function c(){if(!!s.classList.contains("editor-mode-write"))return;const l=e.value.replace(/\{\{([\w_]+)\}\}/g,(o,r)=>{const a=i.querySelector(`[data-placeholder="${CSS.escape(r)}"]`);return C(a?.value??`[${r}]`)});n.html(l,{safe:!1}),Domma.icons.scan(n.get(0))}i._renderPreview=c}function h(e,t){const s=y(t.value),i=new Set([...e.querySelectorAll("[data-placeholder]")].map(n=>n.dataset.placeholder));if(s.size>0&&e.querySelector("p.text-muted")?.remove(),s.forEach(n=>{i.has(n)||S(e,n)}),e.querySelectorAll("[data-placeholder]").forEach(n=>{s.has(n.dataset.placeholder)||n.closest(".block-sample-row")?.remove()}),s.size===0&&!e.querySelector("p.text-muted")){const n=document.createElement("p");n.className="text-muted",n.style.cssText="font-size:.8rem;margin:0;",n.textContent="No {{placeholders}} detected in template.",e.appendChild(n)}}function S(e,t){const s=document.createElement("div");s.className="block-sample-row",s.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.35rem;";const i=document.createElement("label");i.style.cssText="font-size:.75rem;color:var(--dm-text-muted,#888);white-space:nowrap;min-width:90px;text-align:right;flex-shrink:0;",i.textContent=`{{${t}}}`;const n=document.createElement("input");n.type="text",n.className="form-input form-input--sm",n.style.flex="1",n.dataset.placeholder=t,n.placeholder=`Sample ${t}`,n.value=x(t),n.addEventListener("input",()=>{e._renderPreview&&e._renderPreview()}),s.appendChild(i),s.appendChild(n),e.appendChild(s)}function x(e){const t=e.toLowerCase();return t==="_id"?"a1b2c3d4-e5f6-7890-abcd-ef1234567890":t==="_createdat"?new Date().toISOString():t==="_updatedat"?new Date().toISOString():t.includes("email")?"user@example.com":t.includes("phone")?"+44 7700 900000":t.includes("name")?"Jane Smith":t.includes("title")?"Sample Title":t.includes("message")||t.includes("content")||t.includes("description")?"This is a sample value for preview purposes.":t.includes("rating")?"excellent":t.includes("status")?"active":t.includes("priority")?"high":t.includes("date")?new Date().toLocaleDateString():t.includes("tag")?"tag1, tag2":t.includes("subject")?"Sample Subject":t.includes("category")?"general":`Sample ${e}`}function y(e){const t=new Set;for(const[,s]of e.matchAll(/\{\{([\w_]+)\}\}/g))t.add(s);return t}function C(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function L(e){const t=e.find("#block-name").get(0),s=e.find("#block-content").get(0),i=(t?.value||"").trim(),n=s?.value??"";if(!i){E.toast("Block name is required.",{type:"warning"});return}if(!/^[a-z0-9][a-z0-9-]*$/.test(i)){E.toast("Name must start with a letter or digit and contain only lowercase letters, digits, and hyphens.",{type:"warning"});return}const c=e.find("#save-block-btn").get(0);c&&(c.disabled=!0);try{await p.blocks.put(i,{content:n}),E.toast(d?"Block updated.":"Block created.",{type:"success"}),d||(d=i,R.navigate(`/blocks/edit/${encodeURIComponent(i)}`))}catch(u){E.toast(u.message||"Failed to save block.",{type:"error"})}finally{c&&(c.disabled=!1)}}
@@ -0,0 +1,4 @@
1
+ import{api as o}from"../api.js";function l(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}export const blocksView={templateUrl:"/admin/js/templates/blocks.html",async onMount(t){await s(t),Domma.icons.scan()}};async function s(t){const a=t.find("#blocks-table-container").get(0);if(!a)return;let c=[];try{c=await o.blocks.list()}catch(e){a.innerHTML="";const n=document.createElement("p");n.className="text-muted",n.textContent=`Failed to load blocks: ${e.message}`,a.appendChild(n);return}T.create(a,{data:c,emptyMessage:'No blocks yet. Click "New Block" to create your first template.',columns:[{key:"name",title:"Name",render:e=>`<a href="#/blocks/edit/${l(e)}">${l(e)}.html</a>`},{key:"size",title:"Size",render:e=>`${e} B`},{key:"updatedAt",title:"Updated",render:e=>e?new Date(e).toLocaleString():"\u2014"},{key:"name",title:"Actions",render:e=>{const n=l(e);return`<div style="display:flex;gap:.4rem;justify-content:flex-end;">
2
+ <a href="#/blocks/edit/${n}" class="btn btn-sm btn-primary">Edit</a>
3
+ <button class="btn btn-sm btn-danger js-delete-block" data-name="${n}">Delete</button>
4
+ </div>`}}]}),a.querySelectorAll(".js-delete-block").forEach(e=>{e.addEventListener("click",async()=>{await r(e.dataset.name,t)})}),Domma.icons.scan(a)}async function r(t,a){if(await E.confirm(`Delete block "${t}"? This cannot be undone.`))try{await o.blocks.delete(t),E.toast("Block deleted.",{type:"success"}),await s(a)}catch(e){E.toast(e.message||"Failed to delete block.",{type:"error"})}}
@@ -1,3 +1,3 @@
1
- import{api as O}from"../api.js";const j=[{value:"string",label:"Text (single line)"},{value:"email",label:"Email"},{value:"tel",label:"Phone"},{value:"number",label:"Number"},{value:"textarea",label:"Textarea (multi-line)"},{value:"select",label:"Dropdown (select)"},{value:"radio",label:"Radio buttons"},{value:"checkbox",label:"Single checkbox"},{value:"checkbox-group",label:"Checkbox group"},{value:"date",label:"Date"},{value:"time",label:"Time"},{value:"url",label:"URL"},{value:"hidden",label:"Hidden field"}],U=new Set(["select","radio","checkbox-group"]),G=["public","subscriber","editor","manager","admin"],H=["create","read","update","delete"];let m=[],N=null,w=!0;function M(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function V(e){return j.find(t=>t.value===e)?.label||e}function J(e){const t={...m[e]},c=document.getElementById(`fb-label-${e}`),s=document.getElementById(`fb-name-${e}`),p=document.getElementById(`fb-type-${e}`),i=document.getElementById(`fb-required-${e}`),l=document.getElementById(`fb-placeholder-${e}`),n=document.getElementById(`fb-helper-${e}`);if(c&&(t.label=c.value.trim()||t.label),s&&(t.name=s.value.trim()||t.name),p&&(t.type=p.value||t.type),i&&(t.required=i.checked),l&&(t.placeholder=l.value.trim()),n&&(t.helper=n.value.trim()),U.has(t.type)){const o=document.getElementById(`fb-options-${e}`);o&&(t.options=o.value.split(`
2
- `).filter(a=>a.trim()).map(a=>{const[r,...u]=a.split(":");return{value:r.trim(),label:u.join(":").trim()||r.trim()}}))}return t}function Y(){return m.map((e,t)=>J(t))}function K(e,t){const c=document.createElement("div");c.className="fb-field-card",c.dataset.index=t,c.style.cssText="border:1px solid var(--border-color,#333);border-radius:8px;margin-bottom:.75rem;overflow:hidden;";const s=document.createElement("div");s.className="fb-field-header",s.style.cssText="display:flex;align-items:center;gap:.5rem;padding:.6rem .75rem;background:var(--card-header-bg,rgba(255,255,255,.03));cursor:pointer;user-select:none;";const p=document.createElement("span");p.textContent="\u283F",p.style.cssText="cursor:grab;opacity:.4;font-size:1.1rem;";const i=document.createElement("span");i.className="fb-field-summary",i.style.cssText="flex:1;font-weight:500;font-size:.9rem;",i.textContent=e.label||"(Untitled field)";const l=document.createElement("span");l.style.cssText="font-size:.75rem;opacity:.5;",l.textContent=V(e.type);const n=document.createElement("span");n.className="fb-field-chevron",n.textContent="\u25BE",n.style.cssText="opacity:.5;transition:transform .2s;";const o=document.createElement("button");o.type="button",o.textContent="\xD7",o.className="btn btn-sm",o.style.cssText="padding:.15rem .45rem;line-height:1;font-size:1rem;opacity:.6;",o.title="Remove field",o.addEventListener("click",d=>{d.stopPropagation(),m.splice(t,1),_(document.getElementById("fields-list"))}),s.appendChild(p),s.appendChild(i),s.appendChild(l),s.appendChild(n),s.appendChild(o);const a=document.createElement("div");a.className="fb-field-body",a.style.cssText="padding:.75rem;display:none;";const r=document.createElement("div");r.style.cssText="display:grid;grid-template-columns:1fr 1fr 1fr;gap:.6rem;margin-bottom:.6rem;";const u=document.createElement("div"),f=document.createElement("label");f.className="form-label",f.textContent="Label";const b=document.createElement("input");b.id=`fb-label-${t}`,b.type="text",b.className="form-input",b.value=e.label||"",b.addEventListener("input",()=>{i.textContent=b.value.trim()||"(Untitled field)";const d=document.getElementById(`fb-name-${t}`);d&&!d.dataset.manual&&(d.value=M(b.value).replace(/-/g,"_"))}),u.appendChild(f),u.appendChild(b);const S=document.createElement("div"),q=document.createElement("label");q.className="form-label",q.textContent="Name (key)";const h=document.createElement("input");h.id=`fb-name-${t}`,h.type="text",h.className="form-input",h.value=e.name||"",h.addEventListener("input",()=>{h.dataset.manual="1"}),S.appendChild(q),S.appendChild(h);const A=document.createElement("div"),B=document.createElement("label");B.className="form-label",B.textContent="Type";const g=document.createElement("select");g.id=`fb-type-${t}`,g.className="form-input",j.forEach(d=>{const I=document.createElement("option");I.value=d.value,I.textContent=d.label,d.value===e.type&&(I.selected=!0),g.appendChild(I)}),g.addEventListener("change",()=>{l.textContent=V(g.value);const d=a.querySelector(".fb-options-wrap");d&&(d.style.display=U.has(g.value)?"":"none")}),A.appendChild(B),A.appendChild(g),r.appendChild(u),r.appendChild(S),r.appendChild(A);const y=document.createElement("div");y.style.cssText="display:grid;grid-template-columns:1fr 1fr auto;gap:.6rem;align-items:end;margin-bottom:.6rem;";const F=document.createElement("div"),$=document.createElement("label");$.className="form-label",$.textContent="Placeholder";const v=document.createElement("input");v.id=`fb-placeholder-${t}`,v.type="text",v.className="form-input",v.value=e.placeholder||"",F.appendChild($),F.appendChild(v);const W=document.createElement("div"),z=document.createElement("label");z.className="form-label",z.textContent="Helper text";const C=document.createElement("input");C.id=`fb-helper-${t}`,C.type="text",C.className="form-input",C.value=e.helper||"",W.appendChild(z),W.appendChild(C);const P=document.createElement("div");P.style.cssText="padding-bottom:.35rem;";const L=document.createElement("label");L.style.cssText="display:flex;align-items:center;gap:.4rem;cursor:pointer;white-space:nowrap;";const k=document.createElement("input");k.id=`fb-required-${t}`,k.type="checkbox",k.checked=!!e.required,L.appendChild(k),L.appendChild(document.createTextNode("Required")),P.appendChild(L),y.appendChild(F),y.appendChild(W),y.appendChild(P);const x=document.createElement("div");x.className="fb-options-wrap",x.style.display=U.has(e.type)?"":"none";const D=document.createElement("label");D.className="form-label",D.textContent="Options (one per line: value: Label)";const T=document.createElement("textarea");return T.id=`fb-options-${t}`,T.className="form-input",T.rows=4,T.value=(e.options||[]).map(d=>`${d.value}: ${d.label}`).join(`
3
- `),x.appendChild(D),x.appendChild(T),a.appendChild(r),a.appendChild(y),a.appendChild(x),s.addEventListener("click",()=>{const d=a.style.display!=="none";a.style.display=d?"none":"",n.style.transform=d?"":"rotate(180deg)"}),c.appendChild(s),c.appendChild(a),c}function _(e){if(e){if(e.textContent="",m.length===0){const t=document.createElement("p");t.className="text-muted",t.id="fields-empty-msg",t.style.cssText="text-align:center;padding:2rem 0;",t.textContent='No fields yet. Click "Add Field" to get started.',e.appendChild(t);return}m.forEach((t,c)=>{e.appendChild(K(t,c))})}}function Q(e,t){t.textContent="",H.forEach(c=>{const s=e?.[c]||{enabled:!1,access:"admin"},p=document.createElement("div");p.style.cssText="display:grid;grid-template-columns:140px 1fr 160px;gap:.75rem;align-items:center;padding:.6rem 0;border-bottom:1px solid var(--border-color,#333);";const i=document.createElement("strong");i.textContent=c.charAt(0).toUpperCase()+c.slice(1),i.style.cssText="font-size:.9rem;";const l=document.createElement("label");l.style.cssText="display:flex;align-items:center;gap:.45rem;cursor:pointer;font-size:.875rem;";const n=document.createElement("input");n.type="checkbox",n.id=`api-${c}-enabled`,n.checked=!!s.enabled,l.appendChild(n),l.appendChild(document.createTextNode("Enable public access"));const o=document.createElement("select");o.id=`api-${c}-access`,o.className="form-input",G.forEach(a=>{const r=document.createElement("option");r.value=a,r.textContent=a.charAt(0).toUpperCase()+a.slice(1),a===s.access&&(r.selected=!0),o.appendChild(r)}),p.appendChild(i),p.appendChild(l),p.appendChild(o),t.appendChild(p)})}function X(){const e={};return H.forEach(t=>{const c=document.getElementById(`api-${t}-enabled`)?.checked??!1,s=document.getElementById(`api-${t}-access`)?.value||"admin";e[t]={enabled:c,access:s}}),e}export const collectionEditorView={templateUrl:"/admin/js/templates/collection-editor.html",async onMount(e){m=[],N=null,w=!0;const c=window.location.hash.match(/\/collections\/edit\/([^/?#]+)/);c&&(N=c[1],w=!1),E.tabs(e.find("#collection-tabs").get(0));const s=e.find("#fields-list").get(0),p=e.find("#api-access-rows").get(0);let i={create:{enabled:!1,access:"admin"},read:{enabled:!0,access:"public"},update:{enabled:!1,access:"admin"},delete:{enabled:!1,access:"admin"}};if(w){const l=e.find("#field-title").get(0),n=e.find("#field-slug").get(0);l&&n&&(l.addEventListener("input",()=>{n.dataset.manual||(n.value=M(l.value))}),n.addEventListener("input",()=>{n.dataset.manual="1"}))}else try{const l=await O.collections.get(N);if(!l){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const n=e.find("#editor-title-text").get(0);n&&(n.textContent=l.title),e.find("#field-title").val(l.title||""),e.find("#field-slug").val(l.slug||""),e.find("#field-slug").prop("readonly",!0),e.find("#slug-hint").get(0).textContent="Slug cannot be changed after creation.",e.find("#field-description").val(l.description||""),m=l.fields||[],i=l.api||i}catch{E.toast("Failed to load collection.",{type:"error"}),R.navigate("/collections");return}_(s),Q(i,p),e.find("#add-field-btn").off("click").on("click",()=>{m=Y(),m.push({id:`field-${Date.now()}`,name:"",label:"",type:"string",required:!1,placeholder:"",helper:"",options:[],validation:[],logic:null}),_(s);const l=s.querySelectorAll(".fb-field-card");if(l.length){const n=l[l.length-1],o=n.querySelector(".fb-field-body"),a=n.querySelector(".fb-field-chevron");o&&(o.style.display=""),a&&(a.style.transform="rotate(180deg)"),n.querySelector(`#fb-label-${m.length-1}`)?.focus()}}),e.find("#save-collection-btn").off("click").on("click",async()=>{const l=e.find("#field-title").val().trim(),n=e.find("#field-slug").val().trim(),o=e.find("#field-description").val().trim();if(!l){E.toast("Title is required.",{type:"warning"});return}const a=Y(),r=X(),u=e.find("#save-collection-btn");u.prop("disabled",!0);try{if(w){const f=await O.collections.create({title:l,slug:n,description:o,fields:a,api:r});N=f.slug,w=!1,E.toast("Collection created.",{type:"success"}),R.navigate(`/collections/edit/${f.slug}`)}else await O.collections.update(N,{title:l,description:o,fields:a,api:r}),E.toast("Collection saved.",{type:"success"})}catch(f){E.toast(f.message||"Failed to save.",{type:"error"})}finally{u.prop("disabled",!1)}}),Domma.icons.scan()}};
1
+ import{api as D}from"../api.js";const Q=[{value:"string",label:"Text (single line)"},{value:"email",label:"Email"},{value:"tel",label:"Phone"},{value:"number",label:"Number"},{value:"textarea",label:"Textarea (multi-line)"},{value:"select",label:"Dropdown (select)"},{value:"radio",label:"Radio buttons"},{value:"checkbox",label:"Single checkbox"},{value:"checkbox-group",label:"Checkbox group"},{value:"date",label:"Date"},{value:"time",label:"Time"},{value:"url",label:"URL"},{value:"hidden",label:"Hidden field"}],J=new Set(["select","radio","checkbox-group"]),le=["public","subscriber","editor","manager","admin"],X=["create","read","update","delete"];let f=[],x=null,T=!0;function Z(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function $(e){return Q.find(t=>t.value===e)?.label||e}function ne(e){const t={...f[e]},o=document.getElementById(`fb-label-${e}`),n=document.getElementById(`fb-name-${e}`),i=document.getElementById(`fb-type-${e}`),p=document.getElementById(`fb-required-${e}`),r=document.getElementById(`fb-placeholder-${e}`),l=document.getElementById(`fb-helper-${e}`);if(o&&(t.label=o.value.trim()||t.label),n&&(t.name=n.value.trim()||t.name),i&&(t.type=i.value||t.type),p&&(t.required=p.checked),r&&(t.placeholder=r.value.trim()),l&&(t.helper=l.value.trim()),J.has(t.type)){const c=document.getElementById(`fb-options-${e}`);c&&(t.options=c.value.split(`
2
+ `).filter(u=>u.trim()).map(u=>{const[g,...m]=u.split(":");return{value:g.trim(),label:m.join(":").trim()||g.trim()}}))}const a=document.getElementById(`fb-span-${e}`);if(document.getElementById(`fb-fullwidth-${e}`)?.checked)t.fullWidth=!0,delete t.span;else{delete t.fullWidth;const c=parseInt(a?.value,10);c>1?t.span=c:delete t.span}return t}function ee(){return f.map((e,t)=>ne(t))}function ae(e,t){const o=document.createElement("div");o.className="fb-field-card",o.dataset.index=t,o.style.cssText="border:1px solid var(--border-color,#333);border-radius:8px;margin-bottom:.75rem;overflow:hidden;";const n=document.createElement("div");n.className="fb-field-header",n.style.cssText="display:flex;align-items:center;gap:.5rem;padding:.6rem .75rem;background:var(--card-header-bg,rgba(255,255,255,.03));cursor:pointer;user-select:none;";const i=document.createElement("span");i.textContent="\u283F",i.style.cssText="cursor:grab;opacity:.4;font-size:1.1rem;";const p=document.createElement("span");p.className="fb-field-summary",p.style.cssText="flex:1;font-weight:500;font-size:.9rem;",p.textContent=e.label||"(Untitled field)";const r=document.createElement("span");r.style.cssText="font-size:.75rem;opacity:.5;",r.textContent=$(e.type);const l=document.createElement("span");l.className="fb-field-chevron",l.textContent="\u25BE",l.style.cssText="opacity:.5;transition:transform .2s;";const a=document.createElement("button");a.type="button",a.textContent="\xD7",a.className="btn btn-sm",a.style.cssText="padding:.15rem .45rem;line-height:1;font-size:1rem;opacity:.6;",a.title="Remove field",a.addEventListener("click",d=>{d.stopPropagation(),f.splice(t,1),K(document.getElementById("fields-list"))}),n.appendChild(i),n.appendChild(p),n.appendChild(r),n.appendChild(l),n.appendChild(a);const s=document.createElement("div");s.className="fb-field-body",s.style.cssText="padding:.75rem;display:none;";const c=document.createElement("div");c.style.cssText="display:grid;grid-template-columns:1fr 1fr 1fr;gap:.6rem;margin-bottom:.6rem;";const u=document.createElement("div"),g=document.createElement("label");g.className="form-label",g.textContent="Label";const m=document.createElement("input");m.id=`fb-label-${t}`,m.type="text",m.className="form-input",m.value=e.label||"",m.addEventListener("input",()=>{p.textContent=m.value.trim()||"(Untitled field)";const d=document.getElementById(`fb-name-${t}`);d&&!d.dataset.manual&&(d.value=Z(m.value).replace(/-/g,"_"))}),u.appendChild(g),u.appendChild(m);const w=document.createElement("div"),h=document.createElement("label");h.className="form-label",h.textContent="Name (key)";const y=document.createElement("input");y.id=`fb-name-${t}`,y.type="text",y.className="form-input",y.value=e.name||"",y.addEventListener("input",()=>{y.dataset.manual="1"}),w.appendChild(h),w.appendChild(y);const z=document.createElement("div"),P=document.createElement("label");P.className="form-label",P.textContent="Type";const v=document.createElement("select");v.id=`fb-type-${t}`,v.className="form-input",Q.forEach(d=>{const F=document.createElement("option");F.value=d.value,F.textContent=d.label,d.value===e.type&&(F.selected=!0),v.appendChild(F)}),v.addEventListener("change",()=>{r.textContent=$(v.value);const d=s.querySelector(".fb-options-wrap");d&&(d.style.display=J.has(v.value)?"":"none")}),z.appendChild(P),z.appendChild(v),c.appendChild(u),c.appendChild(w),c.appendChild(z);const N=document.createElement("div");N.style.cssText="display:grid;grid-template-columns:1fr 1fr auto;gap:.6rem;align-items:end;margin-bottom:.6rem;";const O=document.createElement("div"),U=document.createElement("label");U.className="form-label",U.textContent="Placeholder";const I=document.createElement("input");I.id=`fb-placeholder-${t}`,I.type="text",I.className="form-input",I.value=e.placeholder||"",O.appendChild(U),O.appendChild(I);const M=document.createElement("div"),_=document.createElement("label");_.className="form-label",_.textContent="Helper text";const k=document.createElement("input");k.id=`fb-helper-${t}`,k.type="text",k.className="form-input",k.value=e.helper||"",M.appendChild(_),M.appendChild(k);const j=document.createElement("div");j.style.cssText="padding-bottom:.35rem;";const B=document.createElement("label");B.style.cssText="display:flex;align-items:center;gap:.4rem;cursor:pointer;white-space:nowrap;";const q=document.createElement("input");q.id=`fb-required-${t}`,q.type="checkbox",q.checked=!!e.required,B.appendChild(q),B.appendChild(document.createTextNode("Required")),j.appendChild(B),N.appendChild(O),N.appendChild(M),N.appendChild(j);const L=document.createElement("div");L.className="fb-options-wrap",L.style.display=J.has(e.type)?"":"none";const V=document.createElement("label");V.className="form-label",V.textContent="Options (one per line: value: Label)";const S=document.createElement("textarea");S.id=`fb-options-${t}`,S.className="form-input",S.rows=4,S.value=(e.options||[]).map(d=>typeof d=="string"?`${d}: ${d}`:`${d.value??""}: ${d.label??d.value??""}`).join(`
3
+ `),L.appendChild(V),L.appendChild(S);const b=document.createElement("div");b.className="fb-grid-row",b.style.gridTemplateColumns="1fr auto",b.style.gap=".6rem",b.style.alignItems="end",b.style.marginBottom=".6rem",b.style.display=document.getElementById("collection-layout")?.value==="grid"?"grid":"none";const H=document.createElement("div"),Y=document.createElement("label");Y.className="form-label",Y.textContent="Column Span";const C=document.createElement("input");C.id=`fb-span-${t}`,C.type="number",C.className="form-input",C.min="1",C.max="6",C.value=e.span>1?String(e.span):"1",H.appendChild(Y),H.appendChild(C);const G=document.createElement("div");G.style.cssText="padding-bottom:.35rem;";const W=document.createElement("label");W.style.cssText="display:flex;align-items:center;gap:.4rem;cursor:pointer;white-space:nowrap;";const A=document.createElement("input");return A.id=`fb-fullwidth-${t}`,A.type="checkbox",A.checked=!!e.fullWidth,W.appendChild(A),W.appendChild(document.createTextNode("Full Width")),G.appendChild(W),b.appendChild(H),b.appendChild(G),s.appendChild(c),s.appendChild(N),s.appendChild(L),s.appendChild(b),n.addEventListener("click",()=>{const d=s.style.display!=="none";s.style.display=d?"none":"",l.style.transform=d?"":"rotate(180deg)"}),o.appendChild(n),o.appendChild(s),o}function K(e){if(e){if(e.textContent="",f.length===0){const t=document.createElement("p");t.className="text-muted",t.id="fields-empty-msg",t.style.cssText="text-align:center;padding:2rem 0;",t.textContent='No fields yet. Click "Add Field" to get started.',e.appendChild(t);return}f.forEach((t,o)=>{e.appendChild(ae(t,o))})}}function oe(e,t){t.textContent="",X.forEach(o=>{const n=e?.[o]||{enabled:!1,access:"admin"},i=document.createElement("div");i.style.cssText="display:grid;grid-template-columns:140px 1fr 160px;gap:.75rem;align-items:center;padding:.6rem 0;border-bottom:1px solid var(--border-color,#333);";const p=document.createElement("strong");p.textContent=o.charAt(0).toUpperCase()+o.slice(1),p.style.cssText="font-size:.9rem;";const r=document.createElement("label");r.style.cssText="display:flex;align-items:center;gap:.45rem;cursor:pointer;font-size:.875rem;";const l=document.createElement("input");l.type="checkbox",l.id=`api-${o}-enabled`,l.checked=!!n.enabled,r.appendChild(l),r.appendChild(document.createTextNode("Enable public access"));const a=document.createElement("select");a.id=`api-${o}-access`,a.className="form-input",le.forEach(s=>{const c=document.createElement("option");c.value=s,c.textContent=s.charAt(0).toUpperCase()+s.slice(1),s===n.access&&(c.selected=!0),a.appendChild(c)}),i.appendChild(p),i.appendChild(r),i.appendChild(a),t.appendChild(i)})}function se(e,t){E.dropdown("#storage-adapter-trigger",{items:[{label:"File (default)",value:"file"},{label:"MongoDB",value:"mongodb"}],onSelect:({item:n})=>{e.find("#storage-adapter").val(n.value),e.find("#storage-adapter-label").text(n.label);const i=n.value==="mongodb";e.find("#storage-connection-group").toggle(i),e.find("#storage-migration-warning").toggle(i&&!T)}});const o=t.map(n=>({label:n,value:n}));E.dropdown("#storage-connection-trigger",{items:o.length?o:[{label:"default",value:"default"}],onSelect:({item:n})=>{e.find("#storage-connection").val(n.value),e.find("#storage-connection-label").text(n.label)}})}function te(){return(document.getElementById("storage-adapter")?.value||"file")==="mongodb"?{adapter:"mongodb",connection:document.getElementById("storage-connection")?.value||"default"}:{adapter:"file"}}function ce(){const e={};return X.forEach(t=>{const o=document.getElementById(`api-${t}-enabled`)?.checked??!1,n=document.getElementById(`api-${t}-access`)?.value||"admin";e[t]={enabled:o,access:n}}),e}export const collectionEditorView={templateUrl:"/admin/js/templates/collection-editor.html",async onMount(e){f=[],x=null,T=!0;const o=window.location.hash.match(/\/collections\/edit\/([^/?#]+)/);o&&(x=o[1],T=!1),E.tabs(e.find("#collection-tabs").get(0)),e.find("#collection-layout").get(0)?.addEventListener("change",function(){const l=this.value==="grid";e.find("#collection-columns-group").get(0).style.display=l?"":"none",document.querySelectorAll(".fb-grid-row").forEach(a=>{a.style.display=l?"grid":"none"})});const n=e.find("#fields-list").get(0),i=e.find("#api-access-rows").get(0),p=await D.collections.proStatus();p?.pro&&x!=="roles"&&(e.find("#storage-tab-btn").show(),se(e,p.connections));let r={create:{enabled:!1,access:"admin"},read:{enabled:!0,access:"public"},update:{enabled:!1,access:"admin"},delete:{enabled:!1,access:"admin"}};if(T){const l=e.find("#field-title").get(0),a=e.find("#field-slug").get(0);l&&a&&(l.addEventListener("input",()=>{a.dataset.manual||(a.value=Z(l.value))}),a.addEventListener("input",()=>{a.dataset.manual="1"}))}else try{const l=await D.collections.get(x);if(!l){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const a=e.find("#editor-title-text").get(0);a&&(a.textContent=l.title),e.find("#field-title").val(l.title||""),e.find("#field-slug").val(l.slug||""),e.find("#field-slug").prop("readonly",!0),e.find("#slug-hint").get(0).textContent="Slug cannot be changed after creation.",e.find("#field-description").val(l.description||""),e.find("#collection-layout").val(l.layout||"stacked"),e.find("#collection-columns").val(l.columns||2),e.find("#collection-columns-group").get(0).style.display=l.layout==="grid"?"":"none",f=l.fields||[],r=l.api||r,l.storage&&(e.find("#storage-adapter").val(l.storage.adapter||"file"),e.find("#storage-adapter-label").text(l.storage.adapter==="mongodb"?"MongoDB":"File (default)"),l.storage.adapter==="mongodb"&&(e.find("#storage-connection-group").show(),e.find("#storage-connection").val(l.storage.connection||"default"),e.find("#storage-connection-label").text(l.storage.connection||"default"))),x==="roles"&&e.find("#storage-tab-btn").hide()}catch{E.toast("Failed to load collection.",{type:"error"}),R.navigate("/collections");return}K(n),oe(r,i),e.find("#add-field-btn").off("click").on("click",()=>{f=ee(),f.push({id:`field-${Date.now()}`,name:"",label:"",type:"string",required:!1,placeholder:"",helper:"",options:[],validation:[],logic:null}),K(n);const l=n.querySelectorAll(".fb-field-card");if(l.length){const a=l[l.length-1],s=a.querySelector(".fb-field-body"),c=a.querySelector(".fb-field-chevron");s&&(s.style.display=""),c&&(c.style.transform="rotate(180deg)"),a.querySelector(`#fb-label-${f.length-1}`)?.focus()}}),e.find("#save-collection-btn").off("click").on("click",async()=>{const l=e.find("#field-title").val().trim(),a=e.find("#field-slug").val().trim(),s=e.find("#field-description").val().trim();if(!l){E.toast("Title is required.",{type:"warning"});return}const c=ee(),u=ce(),g=e.find("#collection-layout").val()||"stacked",m=parseInt(e.find("#collection-columns").val(),10)||2,w=e.find("#save-collection-btn");w.prop("disabled",!0);try{if(T){const h=await D.collections.create({title:l,slug:a,description:s,layout:g,columns:m,fields:c,api:u,storage:te()});x=h.slug,T=!1,E.toast("Collection created.",{type:"success"}),R.navigate(`/collections/edit/${h.slug}`)}else await D.collections.update(x,{title:l,description:s,layout:g,columns:m,fields:c,api:u,storage:te()}),E.toast("Collection saved.",{type:"success"})}catch(h){E.toast(h.message||"Failed to save.",{type:"error"})}finally{w.prop("disabled",!1)}}),Domma.icons.scan()}};