domma-cms 0.76.0 → 0.77.1

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 (88) hide show
  1. package/CLAUDE.md +1 -1
  2. package/admin/js/api.js +1 -1
  3. package/admin/js/app.js +2 -2
  4. package/admin/js/lib/action-editor-arrange.js +1 -0
  5. package/admin/js/lib/api-endpoint-editor-arrange.js +2 -0
  6. package/admin/js/lib/block-editor-arrange.js +2 -0
  7. package/admin/js/lib/collection-editor-arrange.js +3 -0
  8. package/admin/js/lib/collection-entries-arrange.js +1 -0
  9. package/admin/js/lib/component-editor-arrange.js +27 -0
  10. package/admin/js/lib/context-menu-editor-arrange.js +1 -0
  11. package/admin/js/lib/dashboard-arrange.js +1 -0
  12. package/admin/js/lib/form-editor-arrange.js +1 -0
  13. package/admin/js/lib/form-submissions-arrange.js +2 -0
  14. package/admin/js/lib/menu-editor-arrange.js +2 -0
  15. package/admin/js/lib/page-editor-arrange.js +1 -0
  16. package/admin/js/lib/plugin-code-arrange.js +1 -0
  17. package/admin/js/lib/project-page-arrange.js +1 -0
  18. package/admin/js/lib/role-editor-arrange.js +3 -0
  19. package/admin/js/lib/user-editor-arrange.js +1 -0
  20. package/admin/js/lib/view-editor-arrange.js +14 -0
  21. package/admin/js/templates/action-editor.html +223 -147
  22. package/admin/js/templates/api-endpoint-editor.html +172 -117
  23. package/admin/js/templates/block-editor.html +194 -203
  24. package/admin/js/templates/collection-editor.html +191 -113
  25. package/admin/js/templates/collection-entries.html +99 -36
  26. package/admin/js/templates/component-editor.html +128 -126
  27. package/admin/js/templates/context-menu-editor.html +208 -148
  28. package/admin/js/templates/dashboard.html +184 -22
  29. package/admin/js/templates/form-editor.html +41 -26
  30. package/admin/js/templates/form-submissions.html +78 -26
  31. package/admin/js/templates/menu-editor.html +57 -17
  32. package/admin/js/templates/menu-locations.html +54 -11
  33. package/admin/js/templates/page-editor.html +47 -26
  34. package/admin/js/templates/plugin-code.html +91 -73
  35. package/admin/js/templates/project-detail.html +83 -145
  36. package/admin/js/templates/project-settings.html +88 -53
  37. package/admin/js/templates/role-editor.html +158 -83
  38. package/admin/js/templates/user-editor.html +150 -14
  39. package/admin/js/templates/view-editor.html +203 -180
  40. package/admin/js/templates/view-preview.html +45 -45
  41. package/admin/js/views/action-editor.js +7 -1
  42. package/admin/js/views/api-endpoint-editor.js +2 -3
  43. package/admin/js/views/block-editor.js +14 -8
  44. package/admin/js/views/collection-editor.js +64 -5
  45. package/admin/js/views/collection-entries.js +50 -1
  46. package/admin/js/views/component-editor.js +2 -28
  47. package/admin/js/views/context-menu-editor.js +54 -55
  48. package/admin/js/views/dashboard.js +7 -1
  49. package/admin/js/views/form-editor.js +7 -7
  50. package/admin/js/views/form-submissions.js +11 -1
  51. package/admin/js/views/index.js +1 -1
  52. package/admin/js/views/login.js +2 -2
  53. package/admin/js/views/menu-editor.js +21 -43
  54. package/admin/js/views/menu-locations.js +16 -1
  55. package/admin/js/views/page-editor.js +26 -26
  56. package/admin/js/views/plugin-code.js +23 -4
  57. package/admin/js/views/project-detail.js +9 -1
  58. package/admin/js/views/project-settings.js +1 -1
  59. package/admin/js/views/role-editor.js +11 -3
  60. package/admin/js/views/user-editor.js +1 -1
  61. package/admin/js/views/view-editor.js +1 -1
  62. package/admin/js/views/view-preview.js +1 -1
  63. package/package.json +1 -1
  64. package/server/routes/api/dashboard.js +32 -4
  65. package/server/routes/api/forms.js +104 -13
  66. package/server/routes/api/users.js +12 -2
  67. package/server/routes/api/views.js +55 -1
  68. package/server/services/components.js +9 -0
  69. package/server/services/users.js +2 -2
  70. package/server/services/views.js +14 -0
  71. package/admin/js/templates/dashboard/activity-feed.html +0 -3
  72. package/admin/js/templates/dashboard/cache.html +0 -32
  73. package/admin/js/templates/dashboard/health-detail.html +0 -2
  74. package/admin/js/templates/dashboard/journeys.html +0 -17
  75. package/admin/js/templates/dashboard/kpi-strip.html +0 -34
  76. package/admin/js/templates/dashboard/spike-feed.html +0 -3
  77. package/admin/js/templates/dashboard/top-pages.html +0 -3
  78. package/admin/js/templates/dashboard/traffic-chart.html +0 -3
  79. package/admin/js/views/block-editor-enhance.js +0 -1
  80. package/admin/js/views/dashboard/lib/escape.js +0 -1
  81. package/admin/js/views/dashboard/widgets/activity-feed.js +0 -1
  82. package/admin/js/views/dashboard/widgets/cache.js +0 -1
  83. package/admin/js/views/dashboard/widgets/health-detail.js +0 -1
  84. package/admin/js/views/dashboard/widgets/journeys.js +0 -1
  85. package/admin/js/views/dashboard/widgets/kpi-strip.js +0 -1
  86. package/admin/js/views/dashboard/widgets/spike-feed.js +0 -6
  87. package/admin/js/views/dashboard/widgets/top-pages.js +0 -1
  88. package/admin/js/views/dashboard/widgets/traffic-chart.js +0 -1
@@ -1,213 +1,236 @@
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>
1
+ <!-- View editor. Bound with M.applyBindings - see admin/js/views/view-editor.js. No page heading: the Tool
2
+ banner ("Views › <title>") is the heading; Save, Results, Delete and All views are banner actions. -->
3
+ <div class="vex">
4
+ <div class="vex-unsaved" data-bind-hidden="!dirty.value">
5
+ <span class="vex-dot" aria-hidden="true"></span>
6
+ <strong>Unsaved changes</strong>
7
+ <small data-bind-text="errorSummary.value"></small>
8
+ <span class="vex-spacer"></span>
9
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="discard">Discard</button>
10
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="saving.value">
11
+ <span data-icon="save" data-icon-size="13"></span> Save</button>
10
12
  </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
13
 
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">Project</label>
49
- <select id="view-project" class="form-input">
50
- <option value="">- none -</option>
51
- </select>
52
- <small class="text-muted">Tag this view to a project for grouping.</small>
53
- </div>
54
- <div>
55
- <label class="form-check-label" title="Included in fresh installs via the seed script">
56
- <input id="view-bundled" type="checkbox" class="form-check"> Bundled
57
- </label>
58
- </div>
59
- <div>
60
- <label class="form-label">Connection</label>
61
- <select id="view-connection" class="form-input">
62
- <option value="default">default</option>
63
- </select>
64
- <small class="text-muted">MongoDB connection to use for execution.</small>
65
- </div>
66
- </div>
67
- </div>
14
+ <div class="tabs vex-tabs" id="view-editor-tabs">
15
+ <div class="tab-list">
16
+ <button type="button" class="tab-item active">Source <span class="vex-tabdot" data-bind-hidden="!errs.value.source.length" title="Something here needs fixing"></span></button>
17
+ <button type="button" class="tab-item">Pipeline <b class="vex-count" data-bind-text="stageCount.value"></b> <span class="vex-tabdot" data-bind-hidden="!errs.value.pipeline.length" title="Something here needs fixing"></span></button>
18
+ <button type="button" class="tab-item">Display <span class="vex-tabdot" data-bind-hidden="!errs.value.display.length" title="Something here needs fixing"></span></button>
19
+ <button type="button" class="tab-item">Access <span class="vex-tabdot" data-bind-hidden="!errs.value.access.length" title="Something here needs fixing"></span></button>
20
+ <button type="button" class="tab-item">Results</button>
68
21
  </div>
22
+ <div class="tab-content">
69
23
 
70
- <!-- Pipeline tab -->
71
- <div class="tab-panel">
72
- <div class="card mb-3">
73
- <div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
74
- <h2>Aggregation Stages</h2>
75
- <div style="display:flex;gap:.5rem;">
76
- <select id="add-stage-type" class="form-input form-input--sm">
77
- <option value="$match">$match - filter rows</option>
78
- <option value="$sort">$sort - order rows</option>
79
- <option value="$lookup">$lookup - join collection</option>
80
- <option value="$project">$project - reshape fields</option>
81
- <option value="$unwind">$unwind - flatten array</option>
82
- <option value="$addFields">$addFields - compute fields</option>
83
- <option value="$group">$group - aggregate</option>
84
- </select>
85
- <button id="add-stage-btn" class="btn btn-ghost btn-sm">
86
- <span data-icon="plus"></span> Add Stage
87
- </button>
88
- </div>
89
- </div>
90
- <div class="card-body">
91
- <div id="pipeline-stages-list">
92
- <p class="text-muted stage-empty-placeholder" style="text-align:center;padding:2rem 0;">
93
- No stages yet. Add a stage to filter, join, or transform your data.
94
- </p>
24
+ <!-- Source -->
25
+ <div class="tab-panel active">
26
+ <div class="vex-card">
27
+ <div class="vex-card-title">What it is, and what it reads</div>
28
+ <div class="vex-grid">
29
+ <div class="vex-wide">
30
+ <label class="form-label" for="view-title">Title <span class="vex-req">*</span></label>
31
+ <input id="view-title" type="text" class="form-input" maxlength="120" placeholder="e.g. Active premium users">
32
+ </div>
33
+ <div>
34
+ <label class="form-label" for="view-slug">Slug
35
+ <span data-help-title="Slug" data-help="The view's id - in [view slug=&quot;…&quot;] and its API addresses. Made from the title when left blank. It cannot change once the view exists; duplicate the view to get another."></span></label>
36
+ <input id="view-slug" type="text" class="form-input font-mono" placeholder="made from the title">
37
+ <small class="vex-hint" data-bind-text="slugHint.value"></small>
38
+ </div>
39
+ <div>
40
+ <label class="form-label" for="view-source">Source collection <span class="vex-req">*</span>
41
+ <span data-help-title="Source" data-help="The collection whose entries the pipeline starts from. Changing it refreshes the field lists in Pipeline and Display. A MongoDB collection runs as a native aggregation; any other as the built-in pipeline evaluator - same stages, same results."></span></label>
42
+ <select id="view-source" class="form-input"><option value="">- choose -</option></select>
43
+ </div>
44
+ <div class="vex-wide">
45
+ <label class="form-label" for="view-description">Description</label>
46
+ <textarea id="view-description" class="form-input" rows="2" placeholder="What this view shows, and for whom…"></textarea>
47
+ </div>
48
+ <div>
49
+ <label class="form-label" for="view-project">Project
50
+ <span data-help="Files the view under a project - for the sidebar and for users limited to that project. Left empty, it belongs to Core."></span></label>
51
+ <select id="view-project" class="form-input"><option value="">- Core -</option></select>
52
+ </div>
53
+ <div>
54
+ <label class="form-label" for="view-connection">Connection
55
+ <span data-help="The MongoDB connection a MongoDB-backed source runs on. Ignored for file collections."></span></label>
56
+ <select id="view-connection" class="form-input"><option value="default">default</option></select>
57
+ </div>
58
+ <div class="vex-setting vex-wide">
59
+ <label class="form-label" for="view-bundled">Bundled
60
+ <span data-help="Included in fresh installs by the seed script. Leave off for a site's own views."></span></label>
61
+ <input id="view-bundled" type="checkbox" class="form-switch">
62
+ </div>
95
63
  </div>
96
64
  </div>
97
65
  </div>
98
- <div class="card">
99
- <div class="card-body" style="font-size:.8rem;color:var(--dm-text-muted,#888);">
100
- <strong>Guided stages</strong> ($match, $sort) show a visual condition builder.
101
- Other stages use a raw JSON textarea - enter only the <em>inner</em> config (e.g. for $lookup, enter
102
- <code>{ from, localField, foreignField, as }</code> - do not wrap in <code>{ "$lookup": ... }</code>).
103
- Field dropdowns are populated from the source collection - select a collection on the Source tab
104
- first.
66
+
67
+ <!-- Pipeline -->
68
+ <div class="tab-panel">
69
+ <div class="vex-card">
70
+ <div class="vex-card-title">Stages <small class="vex-muted">run top to bottom</small>
71
+ <span data-help-title="Pipeline" data-help="Each stage takes the rows from the one above and passes on its result: filter them ($match), order them ($sort), join another collection ($lookup), reshape or compute fields, group and total, keep or skip some, or count them. $match and $sort have a builder; the rest take their settings as JSON - only the inside, e.g. { &quot;from&quot;: …, &quot;as&quot;: … } for $lookup. Right-click a stage (or ⋮, or Shift+F10) to move, duplicate or delete it, or to see the results up to that stage."></span>
72
+ <span class="vex-spacer"></span>
73
+ <select id="add-stage-type" class="form-input vex-add-type" aria-label="Stage to add"></select>
74
+ <button type="button" id="add-stage-btn" class="btn btn-sm btn-ghost"><span data-icon="plus" data-icon-size="13"></span> Add stage</button>
75
+ </div>
76
+ <div id="pipeline-stages-list" class="vex-stages"></div>
77
+ <p class="vex-note" data-bind-hidden="stageCount.value">No stages yet - every entry of the source is shown as it is. Add one to filter, join or reshape.</p>
105
78
  </div>
106
79
  </div>
107
- </div>
108
80
 
109
- <!-- Display tab -->
110
- <div class="tab-panel">
111
- <div class="card">
112
- <div class="card-body" style="display:flex;flex-direction:column;gap:1.25rem;max-width:640px;">
113
- <div>
114
- <label class="form-label">Display Mode</label>
115
- <select id="view-display-mode" class="form-input">
116
- <option value="table">Table</option>
117
- <option value="list">List</option>
118
- <option value="block">Block</option>
119
- </select>
120
- </div>
121
- <div id="view-block-section" style="display:none;">
122
- <label class="form-label">Block Template</label>
123
- <select id="view-block-name" class="form-input">
124
- <option value="">- select block -</option>
125
- </select>
126
- <small class="text-muted">
127
- Choose a reusable HTML block template.
128
- <a href="#/blocks" style="margin-left:.35rem;">Manage blocks</a>
129
- </small>
130
- </div>
131
- <div>
132
- <label class="form-label">Page Size</label>
133
- <input id="view-page-size" type="number" class="form-input" value="25" min="1" max="200">
134
- </div>
135
- <div id="view-columns-section">
136
- <label class="form-label">Columns</label>
137
- <small class="text-muted" style="display:block;margin-bottom:.75rem;">
138
- Choose which fields to display and their labels.
139
- Field dropdowns are auto-populated from the source collection.
140
- </small>
141
- <div id="view-columns-builder" style="min-height:2rem;"></div>
81
+ <!-- Display -->
82
+ <div class="tab-panel">
83
+ <div class="vex-card">
84
+ <div class="vex-card-title">How it is shown</div>
85
+ <div class="vex-grid">
86
+ <div>
87
+ <label class="form-label" for="view-display-mode">Display mode
88
+ <span data-help="Table: a row per result with the columns below. List: each result's first fields, one under another. Block: each result rendered through a block template of yours."></span></label>
89
+ <select id="view-display-mode" class="form-input">
90
+ <option value="table">Table</option>
91
+ <option value="list">List</option>
92
+ <option value="block">Block</option>
93
+ </select>
94
+ </div>
95
+ <div>
96
+ <label class="form-label" for="view-page-size">Page size
97
+ <span data-help="Results per page, 1 to 200."></span></label>
98
+ <input id="view-page-size" type="number" class="form-input" value="25" min="1" max="200">
99
+ </div>
100
+ <div id="view-block-section" class="vex-wide" hidden>
101
+ <label class="form-label" for="view-block-name">Block template</label>
102
+ <select id="view-block-name" class="form-input"><option value="">- choose a block -</option></select>
103
+ <small class="vex-hint"><a href="#/blocks">Manage blocks</a></small>
104
+ </div>
142
105
  </div>
143
106
  </div>
107
+ <div class="vex-card" id="view-columns-section">
108
+ <div class="vex-card-title">Columns
109
+ <span data-help-title="Columns" data-help="Which fields the table shows, in order, and what each heading says. The field lists come from the source collection. With none, the first six fields of the first result are shown. Right-click a column (or ⋮) to move or remove it."></span></div>
110
+ <div id="view-columns-builder" class="vex-cols"></div>
111
+ </div>
144
112
  </div>
145
- </div>
146
113
 
147
- <!-- Access tab -->
148
- <div class="tab-panel">
149
- <div class="card mb-3">
150
- <div class="card-body" style="max-width:500px;">
151
- <div style="margin-bottom:1.25rem;">
152
- <label class="form-label">Allowed Roles</label>
153
- <small class="text-muted" style="display:block;margin-bottom:.75rem;">
154
- Select which roles can access this view.
155
- </small>
156
- <div id="view-roles-checkboxes" style="display:flex;flex-direction:column;gap:.5rem;"></div>
157
- </div>
158
- <div>
159
- <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
160
- <input type="checkbox" id="view-public">
161
- <span>Public (no authentication required)</span>
162
- </label>
163
- <small class="text-muted" style="display:block;margin-top:.4rem;">
164
- When enabled, the <code>/views/:slug/public</code> endpoint requires no login.
165
- </small>
114
+ <!-- Access -->
115
+ <div class="tab-panel">
116
+ <div class="vex-card">
117
+ <div class="vex-card-title">Who can see it
118
+ <span data-help-title="Access" data-help="Signed-in users holding one of these roles can see the view's results - on pages ([view]) and at /api/views/&lt;slug&gt;/public. Public lets anyone, signed in or not. Admins always can."></span></div>
119
+ <div id="view-roles-checkboxes" class="vex-roles"></div>
120
+ <div class="vex-setting">
121
+ <label class="form-label" for="view-public">Public - no sign-in needed</label>
122
+ <input type="checkbox" id="view-public" class="form-switch">
166
123
  </div>
167
124
  </div>
168
- </div>
169
- <div class="card">
170
- <div class="card-header"><h2>Row-Level Access</h2></div>
171
- <div class="card-body" style="max-width:500px;display:flex;flex-direction:column;gap:1rem;">
172
- <div>
173
- <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
174
- <input type="checkbox" id="view-rowlevel-enabled">
175
- <span>Filter results to current user's entries</span>
176
- </label>
177
- <small class="text-muted" style="display:block;margin-top:.4rem;">
178
- When enabled, each user sees only their own matching entries.
179
- Admins always see all results.
180
- </small>
181
- </div>
182
- <div id="view-rowlevel-config" style="display:none;flex-direction:column;gap:1rem;">
125
+ <div class="vex-card">
126
+ <div class="vex-card-title">Row-level access
127
+ <span data-help-title="Row-level access" data-help="Each user sees only the results that are theirs. Owner: entries they created. Field Match: entries whose chosen field equals something about them (their id, email, name or role). Admins always see every result."></span></div>
128
+ <div class="vex-setting">
129
+ <label class="form-label" for="view-rowlevel-enabled">Show each user only their own results</label>
130
+ <input type="checkbox" id="view-rowlevel-enabled" class="form-switch">
131
+ </div>
132
+ <div id="view-rowlevel-config" class="vex-grid" hidden>
183
133
  <div>
184
- <label class="form-label">Mode</label>
134
+ <label class="form-label" for="view-rowlevel-mode">Mode</label>
185
135
  <select id="view-rowlevel-mode" class="form-input">
186
- <option value="owner">Owner - entry was created by the current user</option>
187
- <option value="field">Field Match - a field on the entry matches the user</option>
136
+ <option value="owner">Owner - they created the entry</option>
137
+ <option value="field">Field Match - a field equals them</option>
188
138
  </select>
189
139
  </div>
190
- <div id="view-rowlevel-field-group" style="display:none;">
191
- <label class="form-label">Field Name</label>
192
- <input id="view-rowlevel-field" type="text" class="form-input"
193
- placeholder="e.g. assigned_to">
194
- <small class="text-muted">The entry field whose value must match the user property
195
- below.</small>
196
- </div>
197
140
  <div>
198
- <label class="form-label">User Property</label>
141
+ <label class="form-label" for="view-rowlevel-userkey">Compared with their</label>
199
142
  <select id="view-rowlevel-userkey" class="form-input">
200
- <option value="id">id (UUID)</option>
143
+ <option value="id">id</option>
201
144
  <option value="email">email</option>
202
145
  <option value="name">name</option>
203
146
  <option value="role">role</option>
204
147
  </select>
205
- <small class="text-muted">Which property of the logged-in user to compare against.</small>
148
+ </div>
149
+ <div id="view-rowlevel-field-group" class="vex-wide" hidden>
150
+ <label class="form-label" for="view-rowlevel-field">Entry field</label>
151
+ <input id="view-rowlevel-field" type="text" class="form-input font-mono" placeholder="e.g. assigned_to">
206
152
  </div>
207
153
  </div>
208
154
  </div>
209
155
  </div>
210
- </div>
211
156
 
157
+ <!-- Results -->
158
+ <div class="tab-panel">
159
+ <div class="vex-card">
160
+ <div class="vex-card-title">Results <small class="vex-muted" data-bind-text="previewMeta.value"></small>
161
+ <span data-help-title="Results" data-help="The pipeline as it stands now - saved or not - run against the source, first rows only. It follows your edits. Row-level access is not applied here: this is the admin's view."></span>
162
+ <span class="vex-spacer"></span>
163
+ <button type="button" class="btn btn-sm btn-ghost" data-bind-hidden="!upTo.value" data-on-click="allStages">Run every stage</button>
164
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="runPreview"><span data-icon="refresh-cw" data-icon-size="13"></span> Run</button>
165
+ </div>
166
+ <p class="vex-note" data-bind-hidden="!previewNote.value" data-bind-text="previewNote.value"></p>
167
+ <p class="vex-note is-bad" data-bind-hidden="!previewError.value" data-bind-text="previewError.value"></p>
168
+ <div class="vex-results"></div>
169
+ </div>
170
+ </div>
171
+ </div>
212
172
  </div>
213
173
  </div>
174
+
175
+ <style>
176
+ .vex { display: flex; flex-direction: column; gap: .75rem; }
177
+ .vex [hidden] { display: none !important; }
178
+ .vex-unsaved { position: sticky; top: 0; z-index: 5; display: flex; align-items: center; gap: .5rem; padding: .5rem .75rem;
179
+ border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); background: var(--dm-surface-raised, var(--dm-surface));
180
+ box-shadow: 0 2px 8px color-mix(in srgb, var(--dm-text) 10%, transparent); }
181
+ .vex-unsaved small { color: var(--dm-danger); }
182
+ .vex-dot { width: .55rem; height: .55rem; border-radius: 50%; background: var(--dm-warning); flex-shrink: 0; }
183
+ .vex-spacer { flex: 1; }
184
+ .vex-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .7rem; margin-top: .75rem; }
185
+ .vex-card-title { display: flex; align-items: center; gap: .4rem; font-weight: 600; flex-wrap: wrap; }
186
+ .vex-card-title .btn { display: inline-flex; align-items: center; gap: .3rem; }
187
+ .vex-muted, .vex-hint { color: var(--dm-text-muted); font-weight: 400; font-size: var(--dm-text-sm); }
188
+ .vex-hint { display: block; font-size: var(--dm-text-xs); margin-top: .25rem; }
189
+ .vex-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
190
+ .vex-note.is-bad { color: var(--dm-danger); }
191
+ .vex-req { color: var(--dm-danger); }
192
+ .vex-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .85rem; }
193
+ .vex-wide { grid-column: 1 / -1; }
194
+ .vex-grid .form-label, .vex-card .form-label { display: flex; align-items: center; gap: .3rem; }
195
+ .vex-setting { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
196
+ .vex-setting .form-label { margin: 0; }
197
+ .vex-count { font-weight: 500; opacity: .65; font-size: var(--dm-text-xs); margin-left: .15rem; }
198
+ .vex-tabdot { display: inline-block; width: .45rem; height: .45rem; border-radius: 50%; background: var(--dm-danger); margin-left: .25rem; vertical-align: middle; }
199
+ .vex-add-type { width: auto; min-width: 12rem; }
200
+ .vex-stages { display: flex; flex-direction: column; gap: .5rem; }
201
+ .vex-stage { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); }
202
+ .vex-stage.has-error { border-color: var(--dm-danger); }
203
+ .vex-stage-head { display: flex; align-items: center; gap: .5rem; padding: .45rem .6rem; border-bottom: 1px solid var(--dm-border);
204
+ background: color-mix(in srgb, var(--dm-text) 3%, transparent); border-radius: var(--dm-radius-md) var(--dm-radius-md) 0 0; }
205
+ .vex-stage-head:focus-visible { outline: 2px solid var(--dm-primary); outline-offset: -2px; }
206
+ .vex-stage-no { font-variant-numeric: tabular-nums; color: var(--dm-text-muted); font-size: var(--dm-text-xs); min-width: 1.2rem; }
207
+ .vex-stage-type { font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-sm); color: var(--dm-primary); }
208
+ .vex-stage-sum { flex: 1; min-width: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
209
+ .vex-stage-body { padding: .6rem; display: flex; flex-direction: column; gap: .4rem; }
210
+ .vex-stage-body textarea { font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-sm); resize: vertical; }
211
+ .vex-stage-err { color: var(--dm-danger); font-size: var(--dm-text-xs); }
212
+ .vex-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; line-height: 1; }
213
+ .vex-kebab:hover { color: var(--dm-primary); }
214
+ .vex-cond, .vex-sort-row, .vex-col { display: flex; gap: .4rem; align-items: center; flex-wrap: wrap; }
215
+ /* One line per condition: the admin's .form-input is width:100%, which stacked them. */
216
+ .vex-cond > select, .vex-sort-row > select { width: auto; flex: 0 1 13rem; min-width: 9rem; }
217
+ .vex-cond .vex-val { flex: 1 1 10rem; min-width: 8rem; display: flex; }
218
+ .vex-cond .vex-val > * { flex: 1; }
219
+ .vex-logic { display: flex; align-items: center; gap: .5rem; }
220
+ .vex-logic select { width: auto; }
221
+ .vex-x { border: 0; background: none; cursor: pointer; color: var(--dm-text-muted); padding: 0 .3rem; font-size: var(--dm-text-lg); line-height: 1; }
222
+ .vex-x:hover { color: var(--dm-danger); }
223
+ .vex-cols { display: flex; flex-direction: column; gap: .4rem; }
224
+ .vex-col { padding: .2rem .25rem; border-radius: var(--dm-radius-sm); }
225
+ .vex-col:focus-within, .vex-col:hover { background: color-mix(in srgb, var(--dm-text) 4%, transparent); }
226
+ .vex-col .col-key { width: auto; flex: 0 1 16rem; min-width: 12rem; }
227
+ .vex-col .col-label { width: auto; flex: 1; }
228
+ .vex-roles { display: flex; flex-wrap: wrap; gap: .4rem .9rem; }
229
+ .vex-roles label { display: inline-flex; align-items: center; gap: .35rem; cursor: pointer; }
230
+ .vex-results { overflow-x: auto; }
231
+ .vex-results table { width: 100%; border-collapse: collapse; font-size: var(--dm-text-sm); }
232
+ .vex-results th, .vex-results td { text-align: left; padding: .35rem .5rem; border-bottom: 1px solid var(--dm-border); white-space: nowrap; max-width: 22rem; overflow: hidden; text-overflow: ellipsis; }
233
+ .vex-results th { color: var(--dm-text-muted); font-weight: 600; }
234
+ .vex-results tr[tabindex]:hover, .vex-results tr[tabindex]:focus-visible { background: color-mix(in srgb, var(--dm-text) 5%, transparent); outline: none; }
235
+ @media (max-width: 640px) { .vex-grid { grid-template-columns: 1fr; } }
236
+ </style>
@@ -1,51 +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>
1
+ <!-- View preview. Bound with M.applyBindings - see admin/js/views/view-preview.js. No page heading: the Tool
2
+ banner ("Views › <title>") is the heading; Run, Edit and All views are banner actions. -->
3
+ <div class="vpx">
4
+ <div class="vpx-card">
5
+ <div class="vpx-card-title">What runs
6
+ <span data-help-title="Preview" data-help="The saved view, run as you (an admin) would see it: every row the pipeline gives, a page at a time. Row-level access narrows the results for other users, not here. To change what it shows, Edit it - the editor's Results tab runs unsaved changes too."></span></div>
7
+ <div class="vpx-chips" data-each="chips.value key=text"></div>
8
+ <p class="vpx-note" data-bind-hidden="!description.value" data-bind-text="description.value"></p>
13
9
  </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
10
 
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>
11
+ <div class="vpx-card">
12
+ <div class="vpx-bar">
13
+ <strong data-bind-text="countText.value"></strong>
14
+ <span class="vpx-spacer"></span>
15
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="prev" data-bind-disabled="!canPrev.value" aria-label="Previous page">‹ Prev</button>
16
+ <span class="vpx-muted" data-bind-text="pageText.value"></span>
17
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="next" data-bind-disabled="!canNext.value" aria-label="Next page">Next ›</button>
35
18
  </div>
19
+ <p class="vpx-note" data-bind-hidden="!loading.value">Running…</p>
20
+ <p class="vpx-note is-bad" data-bind-hidden="!error.value" data-bind-text="error.value"></p>
21
+ <p class="vpx-note" data-bind-hidden="loading.value || error.value || total.value">No rows come out of this view.</p>
22
+ <div class="vpx-results"></div>
36
23
  </div>
37
24
  </div>
38
25
 
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>
26
+ <style>
27
+ .vpx { display: flex; flex-direction: column; gap: .75rem; }
28
+ .vpx [hidden] { display: none !important; }
29
+ .vpx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
30
+ .vpx-card-title { display: flex; align-items: center; gap: .4rem; font-weight: 600; }
31
+ .vpx-chips { display: flex; flex-wrap: wrap; gap: .35rem; }
32
+ .vpx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .55rem; border-radius: 999px; white-space: nowrap;
33
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
34
+ .vpx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
35
+ .vpx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
36
+ .vpx-chip.is-danger { background: color-mix(in srgb, var(--dm-danger) 16%, transparent); color: var(--dm-danger); }
37
+ .vpx-bar { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
38
+ .vpx-spacer { flex: 1; }
39
+ .vpx-muted { color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
40
+ .vpx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
41
+ .vpx-note.is-bad { color: var(--dm-danger); }
42
+ .vpx-results { overflow-x: auto; }
43
+ .vpx-results table { width: 100%; border-collapse: collapse; font-size: var(--dm-text-sm); }
44
+ .vpx-results th, .vpx-results td { text-align: left; padding: .4rem .55rem; border-bottom: 1px solid var(--dm-border); white-space: nowrap; max-width: 22rem; overflow: hidden; text-overflow: ellipsis; }
45
+ .vpx-results th { color: var(--dm-text-muted); font-weight: 600; }
46
+ .vpx-results tr[tabindex] { cursor: default; }
47
+ .vpx-results tr[tabindex]:hover, .vpx-results tr[tabindex]:focus-visible { background: color-mix(in srgb, var(--dm-text) 5%, transparent); outline: none; }
48
+ .vpx-list-item { padding: .55rem .25rem; border-bottom: 1px solid var(--dm-border); display: grid; grid-template-columns: minmax(8rem, max-content) 1fr; gap: .15rem .75rem; font-size: var(--dm-text-sm); }
49
+ .vpx-list-item dt { color: var(--dm-text-muted); }
50
+ .vpx-list-item dd { margin: 0; overflow: hidden; text-overflow: ellipsis; }
51
+ </style>