zrb 1.2.1__py3-none-any.whl → 1.3.0__py3-none-any.whl
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.
- zrb/builtin/llm/llm_chat.py +68 -9
- zrb/builtin/llm/tool/api.py +4 -2
- zrb/builtin/llm/tool/file.py +39 -0
- zrb/builtin/llm/tool/rag.py +37 -22
- zrb/builtin/llm/tool/web.py +46 -20
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_util.py +28 -6
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/gateway/view/content/my-module/my-entity.html +206 -178
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/schema/my_entity.py +3 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_db_repository.py +18 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_repository.py +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/role_service.py +20 -11
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +17 -2
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository.py +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +19 -11
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/permission.html +209 -180
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/role.html +362 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/user.html +377 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/common/util.js +68 -13
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/util.js +50 -29
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +3 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/role.py +6 -5
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +9 -3
- zrb/config.py +3 -1
- zrb/content_transformer/content_transformer.py +7 -1
- zrb/context/context.py +8 -2
- zrb/input/any_input.py +5 -0
- zrb/input/base_input.py +6 -0
- zrb/input/bool_input.py +2 -0
- zrb/input/float_input.py +2 -0
- zrb/input/int_input.py +2 -0
- zrb/input/option_input.py +2 -0
- zrb/input/password_input.py +2 -0
- zrb/input/text_input.py +11 -5
- zrb/runner/cli.py +1 -1
- zrb/runner/common_util.py +3 -3
- zrb/runner/web_route/task_input_api_route.py +1 -1
- zrb/task/llm_task.py +103 -16
- {zrb-1.2.1.dist-info → zrb-1.3.0.dist-info}/METADATA +85 -18
- {zrb-1.2.1.dist-info → zrb-1.3.0.dist-info}/RECORD +41 -40
- {zrb-1.2.1.dist-info → zrb-1.3.0.dist-info}/WHEEL +0 -0
- {zrb-1.2.1.dist-info → zrb-1.3.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,377 @@
|
|
1
|
+
<link rel="stylesheet" href="/static/crud/style.css">
|
2
|
+
|
3
|
+
<main id="crud-app"
|
4
|
+
class="container"
|
5
|
+
data-page-size='{{ page_size | tojson }}'
|
6
|
+
data-page='{{ page | tojson }}'
|
7
|
+
data-sort='{{ sort | tojson }}'
|
8
|
+
data-filter='{{ filter | tojson }}'
|
9
|
+
data-allow-create='{{ allow_create | tojson }}'
|
10
|
+
data-allow-update='{{ allow_update | tojson }}'
|
11
|
+
data-allow-delete='{{ allow_delete | tojson }}'>
|
12
|
+
<article>
|
13
|
+
<h1>User</h1>
|
14
|
+
|
15
|
+
<fieldset id="crud-table-fieldset" user="group" class="grid">
|
16
|
+
<input id="crud-filter-input" placeholder="🔍 Filter" aria-label="Search" />
|
17
|
+
<button id="crud-search-button">🔍 Search</button>
|
18
|
+
{% if allow_create %}
|
19
|
+
<button id="crud-show-create-button" class="contrast">➕ Add</button>
|
20
|
+
{% endif %}
|
21
|
+
</fieldset>
|
22
|
+
|
23
|
+
<div id="crud-table-container">
|
24
|
+
<table id="crud-table" class="striped">
|
25
|
+
<thead>
|
26
|
+
<tr>
|
27
|
+
<th scope="col">ID</th>
|
28
|
+
<th scope="col">Username</th>
|
29
|
+
<th scope="col">Status</th>
|
30
|
+
<th scope="col">Roles/Permissions</th>
|
31
|
+
{% if allow_update or allow_delete %}
|
32
|
+
<th scope="col">Actions</th>
|
33
|
+
{% endif %}
|
34
|
+
</tr>
|
35
|
+
</thead>
|
36
|
+
<tbody></tbody>
|
37
|
+
</table>
|
38
|
+
</div>
|
39
|
+
<div id="crud-pagination"></div>
|
40
|
+
|
41
|
+
{% if allow_create %}
|
42
|
+
<dialog id="crud-create-form-dialog">
|
43
|
+
<article>
|
44
|
+
<h2>New User</h2>
|
45
|
+
<form id="crud-create-form">
|
46
|
+
<label>
|
47
|
+
Username:
|
48
|
+
<input type="text" name="username" required>
|
49
|
+
</label>
|
50
|
+
<label>
|
51
|
+
Password:
|
52
|
+
<input type="password" name="password" required>
|
53
|
+
</label>
|
54
|
+
<label>
|
55
|
+
Active:
|
56
|
+
<input type="checkbox" name="active" value="true">
|
57
|
+
</label>
|
58
|
+
<label>
|
59
|
+
Role Names:
|
60
|
+
<textarea name="role_names" required>[]</textarea>
|
61
|
+
</label>
|
62
|
+
<footer>
|
63
|
+
<button id="crud-create-button">➕ Save</button>
|
64
|
+
<button id="crud-cancel-create-button" class="secondary">❌ Cancel</button>
|
65
|
+
</footer>
|
66
|
+
</form>
|
67
|
+
</article>
|
68
|
+
</dialog>
|
69
|
+
{% endif %}
|
70
|
+
|
71
|
+
{% if allow_update %}
|
72
|
+
<dialog id="crud-update-form-dialog">
|
73
|
+
<article>
|
74
|
+
<h2>Update User</h2>
|
75
|
+
<form id="crud-update-form">
|
76
|
+
<label>
|
77
|
+
Username:
|
78
|
+
<input type="text" name="username" required>
|
79
|
+
</label>
|
80
|
+
<label>
|
81
|
+
Password:
|
82
|
+
<input type="password" name="password">
|
83
|
+
</label>
|
84
|
+
<label>
|
85
|
+
Active:
|
86
|
+
<input type="checkbox" name="active" value="true">
|
87
|
+
</label>
|
88
|
+
<label>
|
89
|
+
Role Names:
|
90
|
+
<textarea name="role_names" required></textarea>
|
91
|
+
</label>
|
92
|
+
<footer>
|
93
|
+
<button id="crud-update-button">✏️ Save</button>
|
94
|
+
<button id="crud-cancel-update-button" class="secondary">❌ Cancel</button>
|
95
|
+
</footer>
|
96
|
+
</form>
|
97
|
+
</article>
|
98
|
+
</dialog>
|
99
|
+
{% endif %}
|
100
|
+
|
101
|
+
{% if allow_delete %}
|
102
|
+
<dialog id="crud-delete-form-dialog">
|
103
|
+
<article>
|
104
|
+
<h2>Delete User</h2>
|
105
|
+
<form id="crud-delete-form">
|
106
|
+
<label>
|
107
|
+
Username:
|
108
|
+
<input type="text" name="username" readonly>
|
109
|
+
</label>
|
110
|
+
<label>
|
111
|
+
Active:
|
112
|
+
<input type="checkbox" name="active" value="true" readonly>
|
113
|
+
</label>
|
114
|
+
<label>
|
115
|
+
Role Names:
|
116
|
+
<textarea name="role_names" readonly></textarea>
|
117
|
+
</label>
|
118
|
+
<footer>
|
119
|
+
<button id="crud-cancel-delete-button" class="secondary">❌ Cancel</button>
|
120
|
+
<button id="crud-delete-button">🗑️ Delete</button>
|
121
|
+
</footer>
|
122
|
+
</form>
|
123
|
+
</article>
|
124
|
+
</dialog>
|
125
|
+
{% endif %}
|
126
|
+
|
127
|
+
<dialog id="crud-alert-dialog">
|
128
|
+
<article>
|
129
|
+
<h2 id="crud-alert-title">Error</h2>
|
130
|
+
<pre id="crud-alert-message"></pre>
|
131
|
+
<footer>
|
132
|
+
<button id="crud-alert-close-button">Close</button>
|
133
|
+
</footer>
|
134
|
+
</article>
|
135
|
+
</dialog>
|
136
|
+
</article>
|
137
|
+
</main>
|
138
|
+
|
139
|
+
<script src="/static/crud/util.js"></script>
|
140
|
+
<script>
|
141
|
+
class CrudApp {
|
142
|
+
constructor(apiUrl, initialState) {
|
143
|
+
this.apiUrl = apiUrl;
|
144
|
+
this.state = { ...initialState };
|
145
|
+
this.init();
|
146
|
+
}
|
147
|
+
|
148
|
+
init() {
|
149
|
+
// Cache common elements
|
150
|
+
this.filterInput = document.getElementById("crud-filter-input");
|
151
|
+
this.searchButton = document.getElementById("crud-search-button");
|
152
|
+
this.filterInput.value = this.state.filter;
|
153
|
+
|
154
|
+
this.filterInput.addEventListener("change", (e) => this.applySearch(e));
|
155
|
+
this.searchButton.addEventListener("click", (e) => this.applySearch(e));
|
156
|
+
|
157
|
+
// Attach optional events if elements exist
|
158
|
+
this.attachEvent("crud-show-create-button", this.showCreateForm.bind(this));
|
159
|
+
this.attachEvent("crud-create-button", this.createRow.bind(this));
|
160
|
+
this.attachEvent("crud-cancel-create-button", this.hideCreateForm.bind(this));
|
161
|
+
this.attachEvent("crud-update-button", this.updateRow.bind(this));
|
162
|
+
this.attachEvent("crud-cancel-update-button", this.hideUpdateForm.bind(this));
|
163
|
+
this.attachEvent("crud-delete-button", this.deleteRow.bind(this));
|
164
|
+
this.attachEvent("crud-cancel-delete-button", this.hideDeleteForm.bind(this));
|
165
|
+
this.attachEvent("crud-alert-close-button", this.hideAlert.bind(this));
|
166
|
+
|
167
|
+
// Initial data fetch
|
168
|
+
this.fetchRows(this.state.currentPage);
|
169
|
+
}
|
170
|
+
|
171
|
+
attachEvent(elementId, handler) {
|
172
|
+
const el = document.getElementById(elementId);
|
173
|
+
if (el) el.addEventListener("click", handler);
|
174
|
+
}
|
175
|
+
|
176
|
+
async applySearch(event) {
|
177
|
+
if (event) event.preventDefault();
|
178
|
+
this.state.filter = this.filterInput.value;
|
179
|
+
await this.fetchRows(this.state.currentPage);
|
180
|
+
}
|
181
|
+
|
182
|
+
async fetchRows(page = null) {
|
183
|
+
try {
|
184
|
+
if (page !== null) {
|
185
|
+
this.state.currentPage = page;
|
186
|
+
}
|
187
|
+
const defaultSearchColumn = "username";
|
188
|
+
// Update address bar
|
189
|
+
const searchParam = CRUD_UTIL.getSearchParam(this.state, defaultSearchColumn, false);
|
190
|
+
const newUrl = `${window.location.pathname}?${searchParam}`;
|
191
|
+
window.history.pushState({ path: newUrl }, "", newUrl);
|
192
|
+
|
193
|
+
// Fetch table data
|
194
|
+
const apiSearchParam = CRUD_UTIL.getSearchParam(this.state, defaultSearchColumn, true);
|
195
|
+
const result = await UTIL.fetchAPI(`${this.apiUrl}?${apiSearchParam}`, { method: "GET" });
|
196
|
+
this.renderRows(result.data);
|
197
|
+
const crudPagination = document.getElementById("crud-pagination");
|
198
|
+
CRUD_UTIL.renderPagination(crudPagination, this, result.count);
|
199
|
+
} catch (error) {
|
200
|
+
console.error("Error fetching items:", error);
|
201
|
+
}
|
202
|
+
}
|
203
|
+
|
204
|
+
renderRows(rows) {
|
205
|
+
const tableBody = document.querySelector("#crud-table tbody");
|
206
|
+
let tableBodyHTML = "";
|
207
|
+
rows.forEach(row => {
|
208
|
+
const rowComponents = this.getRowComponents(row);
|
209
|
+
let actionColumn = "";
|
210
|
+
if (this.state.allowUpdate) {
|
211
|
+
actionColumn += `<button class="contrast" data-id="${row.id}" data-action="edit">✏️ Edit</button>`;
|
212
|
+
}
|
213
|
+
if (this.state.allowDelete) {
|
214
|
+
actionColumn += `<button class="secondary" data-id="${row.id}" data-action="delete">🗑️ Delete</button>`;
|
215
|
+
}
|
216
|
+
if (this.state.allowUpdate || this.state.allowDelete) {
|
217
|
+
actionColumn = `<td><fieldset class="grid" user="group">${actionColumn}</fieldset></td>`;
|
218
|
+
}
|
219
|
+
tableBodyHTML += `<tr>${rowComponents.join('')}${actionColumn}</tr>`;
|
220
|
+
});
|
221
|
+
tableBody.innerHTML = tableBodyHTML;
|
222
|
+
this.attachRowActionListeners();
|
223
|
+
}
|
224
|
+
|
225
|
+
attachRowActionListeners() {
|
226
|
+
document.querySelectorAll('button[data-action="edit"]').forEach(button => {
|
227
|
+
button.addEventListener("click", () => {
|
228
|
+
this.showUpdateForm(button.getAttribute("data-id"));
|
229
|
+
});
|
230
|
+
});
|
231
|
+
document.querySelectorAll('button[data-action="delete"]').forEach(button => {
|
232
|
+
button.addEventListener("click", () => {
|
233
|
+
this.showDeleteForm(button.getAttribute("data-id"));
|
234
|
+
});
|
235
|
+
});
|
236
|
+
}
|
237
|
+
|
238
|
+
getRowComponents(row) {
|
239
|
+
const rowComponents = [
|
240
|
+
`<td>${row.id}</td>`,
|
241
|
+
`<td>${row.username}</td>`,
|
242
|
+
`<td>${row.active ? "Active" : "Inactive"}</td>`,
|
243
|
+
`<td>
|
244
|
+
Roles: ${row.role_names.join(", ")}
|
245
|
+
<br />
|
246
|
+
Permissions: ${row.permission_names.join(", ")}
|
247
|
+
</td>`
|
248
|
+
];
|
249
|
+
return rowComponents;
|
250
|
+
}
|
251
|
+
|
252
|
+
// Create methods
|
253
|
+
showCreateForm(event = null) {
|
254
|
+
if (event) event.preventDefault();
|
255
|
+
const createDialog = document.getElementById("crud-create-form-dialog");
|
256
|
+
const createForm = document.getElementById("crud-create-form");
|
257
|
+
UTIL.clearFormData(createForm);
|
258
|
+
createDialog.showModal();
|
259
|
+
}
|
260
|
+
|
261
|
+
async createRow(event = null) {
|
262
|
+
if (event) event.preventDefault();
|
263
|
+
try {
|
264
|
+
const createForm = document.getElementById("crud-create-form");
|
265
|
+
const formData = UTIL.getFormData(createForm);
|
266
|
+
formData.role_names = JSON.parse(formData.role_names);
|
267
|
+
await UTIL.fetchAPI(this.apiUrl, { method: "POST", body: JSON.stringify(formData) });
|
268
|
+
await this.fetchRows();
|
269
|
+
this.hideCreateForm();
|
270
|
+
} catch (error) {
|
271
|
+
console.error(error);
|
272
|
+
this.showAlert("Create User Error", error);
|
273
|
+
}
|
274
|
+
}
|
275
|
+
|
276
|
+
hideCreateForm(event = null) {
|
277
|
+
if (event) event.preventDefault();
|
278
|
+
document.getElementById("crud-create-form-dialog").close();
|
279
|
+
}
|
280
|
+
|
281
|
+
// Update methods
|
282
|
+
async showUpdateForm(id) {
|
283
|
+
this.state.updatedRowId = id;
|
284
|
+
const updateDialog = document.getElementById("crud-update-form-dialog");
|
285
|
+
const updateForm = document.getElementById("crud-update-form");
|
286
|
+
const rawFormData = await UTIL.fetchAPI(`${this.apiUrl}/${id}`, { method: "GET" });
|
287
|
+
const { role_names, ...formData } = rawFormData;
|
288
|
+
UTIL.setFormData(updateForm, formData);
|
289
|
+
updateForm.querySelector('[name="role_names"]').value = JSON.stringify(role_names);
|
290
|
+
updateDialog.showModal();
|
291
|
+
}
|
292
|
+
|
293
|
+
async updateRow(event = null) {
|
294
|
+
if (event) event.preventDefault();
|
295
|
+
try {
|
296
|
+
const updateForm = document.getElementById("crud-update-form");
|
297
|
+
const formData = UTIL.getFormData(updateForm);
|
298
|
+
if (!formData.password) {
|
299
|
+
delete formData.password;
|
300
|
+
}
|
301
|
+
formData.role_names = JSON.parse(formData.role_names);
|
302
|
+
await UTIL.fetchAPI(`${this.apiUrl}/${this.state.updatedRowId}`, {
|
303
|
+
method: "PUT",
|
304
|
+
body: JSON.stringify(formData)
|
305
|
+
});
|
306
|
+
await this.fetchRows();
|
307
|
+
this.hideUpdateForm();
|
308
|
+
} catch (error) {
|
309
|
+
console.error(error);
|
310
|
+
this.showAlert("Update User Error", error);
|
311
|
+
}
|
312
|
+
}
|
313
|
+
|
314
|
+
hideUpdateForm(event = null) {
|
315
|
+
if (event) event.preventDefault();
|
316
|
+
document.getElementById("crud-update-form-dialog").close();
|
317
|
+
}
|
318
|
+
|
319
|
+
// Delete methods
|
320
|
+
async showDeleteForm(id) {
|
321
|
+
this.state.deletedRowId = id;
|
322
|
+
const deleteDialog = document.getElementById("crud-delete-form-dialog");
|
323
|
+
const deleteForm = document.getElementById("crud-delete-form");
|
324
|
+
const rawFormData = await UTIL.fetchAPI(`${this.apiUrl}/${id}`, { method: "GET" });
|
325
|
+
const { role_names, ...formData } = rawFormData;
|
326
|
+
UTIL.setFormData(deleteForm, formData);
|
327
|
+
deleteForm.querySelector('[name="role_names"]').value = JSON.stringify(role_names);
|
328
|
+
deleteDialog.showModal();
|
329
|
+
}
|
330
|
+
|
331
|
+
async deleteRow(event = null) {
|
332
|
+
if (event) event.preventDefault();
|
333
|
+
try {
|
334
|
+
await UTIL.fetchAPI(`${this.apiUrl}/${this.state.deletedRowId}`, { method: "DELETE" });
|
335
|
+
await this.fetchRows();
|
336
|
+
this.hideDeleteForm();
|
337
|
+
} catch (error) {
|
338
|
+
console.error(error);
|
339
|
+
this.showAlert("Delete User Error", error);
|
340
|
+
}
|
341
|
+
}
|
342
|
+
|
343
|
+
hideDeleteForm(event = null) {
|
344
|
+
if (event) event.preventDefault();
|
345
|
+
document.getElementById("crud-delete-form-dialog").close();
|
346
|
+
}
|
347
|
+
|
348
|
+
// Alert methods
|
349
|
+
showAlert(title, error) {
|
350
|
+
const alertDialog = document.getElementById("crud-alert-dialog");
|
351
|
+
document.getElementById("crud-alert-title").textContent = title;
|
352
|
+
document.getElementById("crud-alert-message").textContent = error.message || String(error);
|
353
|
+
alertDialog.showModal();
|
354
|
+
}
|
355
|
+
|
356
|
+
hideAlert(event = null) {
|
357
|
+
if (event) event.preventDefault();
|
358
|
+
document.getElementById("crud-alert-dialog").close();
|
359
|
+
}
|
360
|
+
}
|
361
|
+
|
362
|
+
// Initialize the CrudApp on DOM ready
|
363
|
+
document.addEventListener("DOMContentLoaded", () => {
|
364
|
+
const app = document.getElementById("crud-app");
|
365
|
+
new CrudApp("/api/v1/users", {
|
366
|
+
pageSize: JSON.parse(app.dataset.pageSize),
|
367
|
+
currentPage: JSON.parse(app.dataset.page),
|
368
|
+
sort: JSON.parse(app.dataset.sort),
|
369
|
+
filter: JSON.parse(app.dataset.filter),
|
370
|
+
allowCreate: JSON.parse(app.dataset.allowCreate),
|
371
|
+
allowUpdate: JSON.parse(app.dataset.allowUpdate),
|
372
|
+
allowDelete: JSON.parse(app.dataset.allowDelete),
|
373
|
+
updatedRowId: null,
|
374
|
+
deletedRowId: null,
|
375
|
+
});
|
376
|
+
});
|
377
|
+
</script>
|
@@ -116,20 +116,39 @@ const UTIL = {
|
|
116
116
|
|
117
117
|
setFormData(form, data) {
|
118
118
|
for (const key in data) {
|
119
|
-
// Only search within this form for an element with the matching name
|
120
119
|
const element = form.querySelector(`[name="${key}"]`);
|
121
|
-
if (element) {
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
120
|
+
if (!element) {
|
121
|
+
continue;
|
122
|
+
}
|
123
|
+
const value = data[key];
|
124
|
+
// Handle checkboxes and radio buttons
|
125
|
+
if (element.type === 'checkbox' || element.type === 'radio') {
|
126
|
+
const elements = form.querySelectorAll(`[name="${key}"]`);
|
127
|
+
elements.forEach(el => {
|
128
|
+
// If value is an array, check if this option is included;
|
129
|
+
// otherwise compare to a single value.
|
130
|
+
if (Array.isArray(value)) {
|
131
|
+
el.checked = value.includes(el.value);
|
132
|
+
} else {
|
133
|
+
el.checked = (value === true) || (el.value == value);
|
134
|
+
}
|
135
|
+
});
|
136
|
+
}
|
137
|
+
// Handle select elements
|
138
|
+
else if (element.tagName === "SELECT") {
|
139
|
+
if (element.multiple && Array.isArray(value)) {
|
140
|
+
// For multi-select, mark options as selected if their value is in the array.
|
141
|
+
Array.from(element.options).forEach(option => {
|
142
|
+
option.selected = value.includes(option.value);
|
127
143
|
});
|
128
144
|
} else {
|
129
|
-
|
130
|
-
element.value = data[key];
|
145
|
+
element.value = value;
|
131
146
|
}
|
132
147
|
}
|
148
|
+
// Handle all other inputs (including textarea)
|
149
|
+
else {
|
150
|
+
element.value = value;
|
151
|
+
}
|
133
152
|
}
|
134
153
|
},
|
135
154
|
|
@@ -139,7 +158,13 @@ const UTIL = {
|
|
139
158
|
if (element.type === "checkbox" || element.type === "radio") {
|
140
159
|
element.checked = element.defaultChecked; // Restore default checked state
|
141
160
|
} else if (element.tagName === "SELECT") {
|
142
|
-
element.
|
161
|
+
if (element.multiple) {
|
162
|
+
// Deselect all options for multi-select.
|
163
|
+
Array.from(element.options).forEach(option => option.selected = false);
|
164
|
+
} else {
|
165
|
+
// Select the first option by default
|
166
|
+
element.selectedIndex = 0;
|
167
|
+
}
|
143
168
|
} else {
|
144
169
|
element.value = element.defaultValue || ""; // Reset to default value or empty
|
145
170
|
}
|
@@ -150,9 +175,39 @@ const UTIL = {
|
|
150
175
|
getFormData(form) {
|
151
176
|
const formData = new FormData(form);
|
152
177
|
const data = {};
|
153
|
-
//
|
154
|
-
formData.
|
155
|
-
|
178
|
+
// Populate data from FormData (this covers inputs, textareas, selects, and checked radio buttons)
|
179
|
+
for (const [key, value] of formData.entries()) {
|
180
|
+
// If key already exists, it’s part of a multi-value field.
|
181
|
+
if (key in data) {
|
182
|
+
if (!Array.isArray(data[key])) {
|
183
|
+
data[key] = [data[key]];
|
184
|
+
}
|
185
|
+
data[key].push(value);
|
186
|
+
} else {
|
187
|
+
data[key] = value;
|
188
|
+
}
|
189
|
+
}
|
190
|
+
// Process all checkbox inputs.
|
191
|
+
const checkboxes = form.querySelectorAll("input[type='checkbox']");
|
192
|
+
// Use a Set to iterate over unique checkbox names.
|
193
|
+
const checkboxNames = new Set();
|
194
|
+
checkboxes.forEach(el => checkboxNames.add(el.name));
|
195
|
+
checkboxNames.forEach(name => {
|
196
|
+
const elems = form.querySelectorAll(`input[type='checkbox'][name="${name}"]`);
|
197
|
+
if (elems.length === 1) {
|
198
|
+
// Unique checkbox: convert its presence in formData to a boolean.
|
199
|
+
// If it wasn’t included by FormData (because it was unchecked), default to false.
|
200
|
+
data[name] = elems[0].checked;
|
201
|
+
} else {
|
202
|
+
// Multiple checkboxes: ensure the value is an array.
|
203
|
+
// If none of the checkboxes were checked, ensure an empty array is returned.
|
204
|
+
if (!(name in data)) {
|
205
|
+
data[name] = [];
|
206
|
+
} else if (!Array.isArray(data[name])) {
|
207
|
+
// This case can happen if only one checkbox in the group was checked.
|
208
|
+
data[name] = [data[name]];
|
209
|
+
}
|
210
|
+
}
|
156
211
|
});
|
157
212
|
return data;
|
158
213
|
},
|
zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/util.js
CHANGED
@@ -1,51 +1,72 @@
|
|
1
1
|
const CRUD_UTIL = {
|
2
2
|
|
3
|
-
renderPagination(paginationComponent,
|
4
|
-
const
|
3
|
+
renderPagination(paginationComponent, crudApp, total) {
|
4
|
+
const { pageSize, currentPage } = crudApp.state;
|
5
|
+
const totalPages = Math.ceil(total / pageSize);
|
5
6
|
paginationComponent.innerHTML = "";
|
6
|
-
// Ensure left alignment
|
7
|
+
// Ensure left alignment
|
7
8
|
paginationComponent.style.textAlign = "left";
|
8
|
-
let
|
9
|
-
//
|
10
|
-
if (
|
11
|
-
|
12
|
-
|
9
|
+
let buttons = [];
|
10
|
+
// "First" and "Previous" buttons if not on the first page
|
11
|
+
if (currentPage > 1) {
|
12
|
+
buttons.push({ label: "«", page: 1 });
|
13
|
+
buttons.push({ label: "<", page: currentPage - 1 });
|
13
14
|
}
|
14
15
|
if (totalPages <= 5) {
|
15
|
-
//
|
16
|
+
// List all pages
|
16
17
|
for (let i = 1; i <= totalPages; i++) {
|
17
|
-
|
18
|
+
buttons.push({ label: i, page: i });
|
18
19
|
}
|
19
20
|
} else {
|
20
|
-
// Always show first page
|
21
|
-
|
22
|
-
|
23
|
-
const
|
24
|
-
|
25
|
-
// Add ellipsis if there's a gap between first page and the start of the range
|
21
|
+
// Always show the first page
|
22
|
+
buttons.push({ label: "1", page: 1, disabled: currentPage === 1 });
|
23
|
+
const start = Math.max(2, currentPage - 1);
|
24
|
+
const end = Math.min(totalPages - 1, currentPage + 1);
|
25
|
+
// Add ellipsis if there's a gap
|
26
26
|
if (start > 2) {
|
27
|
-
|
27
|
+
buttons.push({ label: "...", isSpan: true });
|
28
28
|
}
|
29
|
-
//
|
29
|
+
// Pages around current page
|
30
30
|
for (let i = start; i <= end; i++) {
|
31
|
-
|
31
|
+
buttons.push({ label: i, page: i });
|
32
32
|
}
|
33
|
-
// Add ellipsis if there's a gap between the end of the range and the last page
|
34
33
|
if (end < totalPages - 1) {
|
35
|
-
|
34
|
+
buttons.push({ label: "...", isSpan: true });
|
36
35
|
}
|
37
|
-
// Always show last page
|
38
|
-
|
36
|
+
// Always show the last page
|
37
|
+
buttons.push({ label: totalPages, page: totalPages, disabled: currentPage === totalPages });
|
39
38
|
}
|
40
|
-
//
|
41
|
-
if (
|
42
|
-
|
43
|
-
|
39
|
+
// "Next" and "Last" buttons if not on the last page
|
40
|
+
if (currentPage < totalPages) {
|
41
|
+
buttons.push({ label: ">", page: currentPage + 1 });
|
42
|
+
buttons.push({ label: "»", page: totalPages });
|
44
43
|
}
|
45
|
-
|
44
|
+
// Render buttons and spans
|
45
|
+
buttons.forEach(btn => {
|
46
|
+
if (btn.isSpan) {
|
47
|
+
paginationComponent.insertAdjacentHTML("beforeend", `<span style="padding: 0 5px;">${btn.label}</span>`);
|
48
|
+
} else {
|
49
|
+
const buttonEl = document.createElement("button");
|
50
|
+
buttonEl.className = "secondary";
|
51
|
+
buttonEl.innerHTML = btn.label;
|
52
|
+
if (btn.disabled) {
|
53
|
+
buttonEl.disabled = true;
|
54
|
+
} else {
|
55
|
+
buttonEl.dataset.page = btn.page;
|
56
|
+
}
|
57
|
+
paginationComponent.appendChild(buttonEl);
|
58
|
+
}
|
59
|
+
});
|
60
|
+
// Attach event listeners to pagination buttons
|
61
|
+
paginationComponent.querySelectorAll("button[data-page]").forEach(button => {
|
62
|
+
button.addEventListener("click", () => {
|
63
|
+
const page = parseInt(button.dataset.page);
|
64
|
+
crudApp.fetchRows(page);
|
65
|
+
});
|
66
|
+
});
|
46
67
|
},
|
47
68
|
|
48
|
-
splitUnescaped(query, delimiter=",") {
|
69
|
+
splitUnescaped(query, delimiter = ",") {
|
49
70
|
const parts = [];
|
50
71
|
let current = "";
|
51
72
|
let escaped = false;
|
@@ -25,7 +25,9 @@ class PermissionUpdate(SQLModel):
|
|
25
25
|
description: str | None = None
|
26
26
|
|
27
27
|
def with_audit(self, updated_by: str) -> "PermissionUpdateWithAudit":
|
28
|
-
return PermissionUpdateWithAudit(
|
28
|
+
return PermissionUpdateWithAudit(
|
29
|
+
**self.model_dump(exclude_none=True), updated_by=updated_by
|
30
|
+
)
|
29
31
|
|
30
32
|
|
31
33
|
class PermissionUpdateWithAudit(PermissionUpdate):
|
@@ -7,11 +7,10 @@ from sqlmodel import Field, SQLModel
|
|
7
7
|
|
8
8
|
class RoleBase(SQLModel):
|
9
9
|
name: str
|
10
|
+
description: str = ""
|
10
11
|
|
11
12
|
|
12
13
|
class RoleCreate(RoleBase):
|
13
|
-
description: str
|
14
|
-
|
15
14
|
def with_audit(self, created_by: str) -> "RoleCreateWithAudit":
|
16
15
|
return RoleCreateWithAudit(**self.model_dump(), created_by=created_by)
|
17
16
|
|
@@ -51,7 +50,9 @@ class RoleUpdate(SQLModel):
|
|
51
50
|
description: str | None = None
|
52
51
|
|
53
52
|
def with_audit(self, updated_by: str) -> "RoleUpdateWithAudit":
|
54
|
-
return RoleUpdateWithAudit(
|
53
|
+
return RoleUpdateWithAudit(
|
54
|
+
**self.model_dump(exclude_none=True), updated_by=updated_by
|
55
|
+
)
|
55
56
|
|
56
57
|
|
57
58
|
class RoleUpdateWithAudit(RoleUpdate):
|
@@ -63,7 +64,7 @@ class RoleUpdateWithPermissions(RoleUpdate):
|
|
63
64
|
|
64
65
|
def with_audit(self, updated_by: str) -> "RoleUpdateWithPermissionsAndAudit":
|
65
66
|
return RoleUpdateWithPermissionsAndAudit(
|
66
|
-
**self.model_dump(), updated_by=updated_by
|
67
|
+
**self.model_dump(exclude_none=True), updated_by=updated_by
|
67
68
|
)
|
68
69
|
|
69
70
|
|
@@ -73,7 +74,7 @@ class RoleUpdateWithPermissionsAndAudit(RoleUpdateWithPermissions):
|
|
73
74
|
def get_role_update_with_audit(self) -> RoleUpdateWithAudit:
|
74
75
|
data = {
|
75
76
|
key: val
|
76
|
-
for key, val in self.model_dump().items()
|
77
|
+
for key, val in self.model_dump(exclude_none=True).items()
|
77
78
|
if key != "permission_names"
|
78
79
|
}
|
79
80
|
return RoleUpdateWithAudit(**data)
|