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.
Files changed (41) hide show
  1. zrb/builtin/llm/llm_chat.py +68 -9
  2. zrb/builtin/llm/tool/api.py +4 -2
  3. zrb/builtin/llm/tool/file.py +39 -0
  4. zrb/builtin/llm/tool/rag.py +37 -22
  5. zrb/builtin/llm/tool/web.py +46 -20
  6. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_util.py +28 -6
  7. 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
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/schema/my_entity.py +3 -1
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_db_repository.py +18 -1
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_repository.py +4 -0
  11. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/role_service.py +20 -11
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +17 -2
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository.py +4 -0
  14. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +19 -11
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/permission.html +209 -180
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/role.html +362 -0
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/user.html +377 -0
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/common/util.js +68 -13
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/util.js +50 -29
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +3 -1
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/role.py +6 -5
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +9 -3
  23. zrb/config.py +3 -1
  24. zrb/content_transformer/content_transformer.py +7 -1
  25. zrb/context/context.py +8 -2
  26. zrb/input/any_input.py +5 -0
  27. zrb/input/base_input.py +6 -0
  28. zrb/input/bool_input.py +2 -0
  29. zrb/input/float_input.py +2 -0
  30. zrb/input/int_input.py +2 -0
  31. zrb/input/option_input.py +2 -0
  32. zrb/input/password_input.py +2 -0
  33. zrb/input/text_input.py +11 -5
  34. zrb/runner/cli.py +1 -1
  35. zrb/runner/common_util.py +3 -3
  36. zrb/runner/web_route/task_input_api_route.py +1 -1
  37. zrb/task/llm_task.py +103 -16
  38. {zrb-1.2.1.dist-info → zrb-1.3.0.dist-info}/METADATA +85 -18
  39. {zrb-1.2.1.dist-info → zrb-1.3.0.dist-info}/RECORD +41 -40
  40. {zrb-1.2.1.dist-info → zrb-1.3.0.dist-info}/WHEEL +0 -0
  41. {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
- // For checkboxes or radio buttons, update each matching element within the form
123
- if (element.type === 'checkbox' || element.type === 'radio') {
124
- const elements = form.querySelectorAll(`[name="${key}"]`);
125
- elements.forEach(el => {
126
- el.checked = (el.value === data[key]);
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
- // For other types of inputs, selects, and textareas, simply set the value
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.selectedIndex = 0; // Select the first option by default
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
- // Convert FormData to a plain object
154
- formData.forEach((value, key) => {
155
- data[key] = value;
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
  },
@@ -1,51 +1,72 @@
1
1
  const CRUD_UTIL = {
2
2
 
3
- renderPagination(paginationComponent, crudState, total, fetchFunction = "fetchRows") {
4
- const totalPages = Math.ceil(total / crudState.pageSize);
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 (if not already handled by PicoCSS or external CSS)
7
+ // Ensure left alignment
7
8
  paginationComponent.style.textAlign = "left";
8
- let paginationHTML = "";
9
- // Only show "First" and "Previous" if we're not on page 1
10
- if (crudState.currentPage > 1) {
11
- paginationHTML += `<button class="secondary" onclick="${fetchFunction}(1)">&laquo;</button>`;
12
- paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${crudState.currentPage - 1})">&lt;</button>`;
9
+ let buttons = [];
10
+ // "First" and "Previous" buttons if not on the first page
11
+ if (currentPage > 1) {
12
+ buttons.push({ label: "&laquo;", page: 1 });
13
+ buttons.push({ label: "&lt;", page: currentPage - 1 });
13
14
  }
14
15
  if (totalPages <= 5) {
15
- // If total pages are few, simply list them all
16
+ // List all pages
16
17
  for (let i = 1; i <= totalPages; i++) {
17
- paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${i})" ${i === crudState.currentPage ? "disabled" : ""}>${i}</button>`;
18
+ buttons.push({ label: i, page: i });
18
19
  }
19
20
  } else {
20
- // Always show first page
21
- paginationHTML += `<button class="secondary" onclick="${fetchFunction}(1)" ${crudState.currentPage === 1 ? "disabled" : ""}>1</button>`;
22
- // Determine start and end for the page range around current page
23
- const start = Math.max(2, crudState.currentPage - 1);
24
- const end = Math.min(totalPages - 1, crudState.currentPage + 1);
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
- paginationHTML += `<span style="padding: 0 5px;">...</span>`;
27
+ buttons.push({ label: "...", isSpan: true });
28
28
  }
29
- // Render the range around the current page
29
+ // Pages around current page
30
30
  for (let i = start; i <= end; i++) {
31
- paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${i})" ${i === crudState.currentPage ? "disabled" : ""}>${i}</button>`;
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
- paginationHTML += `<span style="padding: 0 5px;">...</span>`;
34
+ buttons.push({ label: "...", isSpan: true });
36
35
  }
37
- // Always show last page
38
- paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${totalPages})" ${crudState.currentPage === totalPages ? "disabled" : ""}>${totalPages}</button>`;
36
+ // Always show the last page
37
+ buttons.push({ label: totalPages, page: totalPages, disabled: currentPage === totalPages });
39
38
  }
40
- // Only show "Next" and "Last" if we're not on the last page
41
- if (crudState.currentPage < totalPages) {
42
- paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${crudState.currentPage + 1})">&gt;</button>`;
43
- paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${totalPages})">&raquo;</button>`;
39
+ // "Next" and "Last" buttons if not on the last page
40
+ if (currentPage < totalPages) {
41
+ buttons.push({ label: "&gt;", page: currentPage + 1 });
42
+ buttons.push({ label: "&raquo;", page: totalPages });
44
43
  }
45
- paginationComponent.innerHTML = paginationHTML;
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(**self.model_dump(), updated_by=updated_by)
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(**self.model_dump(), updated_by=updated_by)
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)