Skip to content

Commit 09840a7

Browse files
committed
feat: general improvements
1 parent 9e80a2d commit 09840a7

18 files changed

Lines changed: 567 additions & 151 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* A modular form.
3+
* Triggers the following events:
4+
* - webexpress.webui.Event.MODAL_SHOW_EVENT
5+
* - webexpress.webui.Event.MODAL_HIDE_EVENT
6+
*/
7+
webexpress.webapp.ModalFormCtrl = class extends webexpress.webui.ModalFormCtrl {
8+
9+
/**
10+
* Constructor
11+
* @param {HTMLElement} element - The DOM element associated with the modal control.
12+
*/
13+
constructor(form) {
14+
super(document.createElement("div"));
15+
16+
document.body.appendChild(this._element);
17+
18+
this._selector = form;
19+
}
20+
};

src/WebExpress.WebApp/Assets/js/webexpress.webapp.table.js

Lines changed: 154 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,110 +5,173 @@
55
* - webexpress.webui.Event.COLUMN_REORDER_EVENT
66
*/
77
webexpress.webapp.TableCtrl = class extends webexpress.webui.TableCtrl {
8+
// Helper to create DOM elements with class list
9+
_createElement(tag, classList = []) {
10+
const el = document.createElement(tag);
11+
classList.forEach(cls => el.classList.add(cls));
12+
return el;
13+
}
14+
15+
// Helper to create the progress bar
16+
_createProgressDiv() {
17+
const div = this._createElement("div", ["progress"]);
18+
div.setAttribute("role", "status");
19+
div.style.height = "0.5em";
20+
const bar = this._createElement("div", [
21+
"progress-bar",
22+
"progress-bar-striped",
23+
"progress-bar-animated",
24+
]);
25+
bar.style.width = "100%";
26+
div.appendChild(bar);
27+
return div;
28+
}
29+
30+
// Fields
831
_restUri = "";
9-
_titleDiv = $("<h3>").addClass("me-auto");
10-
_progressDiv = $("<div role='status' style='height: 0.5em'>")
11-
.addClass("progress")
12-
.append($("<div class='progress-bar progress-bar-striped progress-bar-animated' style='width: 100%'>"));
13-
_filterDiv = $("<div class='col-3'>");
32+
_titleDiv = this._createElement("h3", ["me-auto"]);
33+
_progressDiv = this._createProgressDiv();
34+
_filterDiv = this._createElement("div", ["col-3"]);
1435
_filterCtrl = null;
15-
_statusDiv = $("<span>");
16-
_paginationDiv = $("<div class='justify-content-end'>");
36+
_statusDiv = this._createElement("span");
37+
_paginationDiv = this._createElement("div", ["justify-content-end"]);
1738
_paginationCtrl = null;
1839
_filter = null;
1940
_page = 0;
20-
_previewColumns = [
21-
{ label: "<span class='placeholder col-6 placeholder-lg'></span>" },
22-
{ label: "<span class='placeholder col-6 placeholder-lg'></span>" },
23-
{ label: "<span class='placeholder col-6 placeholder-lg'></span>" }];
41+
_hasOptions = false;
42+
_columns = [];
43+
_rows = [];
44+
_options = [];
45+
46+
// Placeholder columns and rows for loading state
47+
_previewColumns = [
48+
{ label: "<span class='placeholder col-6 placeholder-lg'></span>" },
49+
{ label: "<span class='placeholder col-6 placeholder-lg'></span>" },
50+
{ label: "<span class='placeholder col-6 placeholder-lg'></span>" }
51+
];
2452
_previewBody = [
25-
{ cells: [ { text: "<span class='placeholder col-7'></span>" }, { text: "<span class='placeholder col-5'></span>" }, { text: "<span class='placeholder col-6'></span>" } ] },
26-
{ cells: [ { text: "<span class='placeholder col-6'></span>" }, { text: "<span class='placeholder col-7'></span>" }, { text: "<span class='placeholder col-5'></span>" } ] },
27-
{ cells: [ { text: "<span class='placeholder col-6'></span>" }, { text: "<span class='placeholder col-6'></span>" }, { text: "<span class='placeholder col-7'></span>" } ] }];
53+
{ cells: [{ text: "<span class='placeholder col-7'></span>" }, { text: "<span class='placeholder col-5'></span>" }, { text: "<span class='placeholder col-6'></span>" }] },
54+
{ cells: [{ text: "<span class='placeholder col-6'></span>" }, { text: "<span class='placeholder col-7'></span>" }, { text: "<span class='placeholder col-5'></span>" }] },
55+
{ cells: [{ text: "<span class='placeholder col-6'></span>" }, { text: "<span class='placeholder col-6'></span>" }, { text: "<span class='placeholder col-7'></span>" }] }
56+
];
2857

2958
/**
30-
* Constructor
31-
* @param {HTMLElement} element - The DOM element associated with the modal control.
59+
* Constructor for the TableCtrl class.
60+
* @param {HTMLElement} element - The DOM element associated with the control.
3261
*/
3362
constructor(element) {
3463
super(element);
3564

36-
this._restUri = $(element).data("uri") ?? ""; // Retrieve the URI for loading content
37-
38-
// Cleanup the DOM element
39-
$(this._element)
40-
.removeAttr("data-uri");
65+
// Get REST URI from data attribute or fallback to empty string
66+
this._restUri = element.dataset.uri || "";
67+
68+
// Remove REST URI attribute from DOM for security/cleanliness
69+
element.removeAttribute("data-uri");
70+
71+
// Build the toolbar (title and filter)
72+
const toolbar = this._createElement("div", ["wx-table-toolbar"]);
73+
toolbar.appendChild(this._titleDiv);
74+
toolbar.appendChild(this._filterDiv);
75+
76+
// Insert toolbar and progress bar at the top of the element
77+
element.prepend(toolbar, this._progressDiv);
78+
79+
// Build the status bar (status and pagination)
80+
const statusbar = this._createElement("div", ["wx-table-statusbar"]);
81+
statusbar.appendChild(this._statusDiv);
82+
statusbar.appendChild(this._paginationDiv);
4183

42-
// Show placeholder while loading content
43-
$(this._element).prepend($("<div>").addClass("wx-table-toolbar").append(this._titleDiv, this._filterDiv), this._progressDiv);
44-
$(this._element).append($("<div>").addClass("wx-table-statusbar").append(this._statusDiv, this._paginationDiv));
45-
84+
// Append status bar at the bottom of the element
85+
element.appendChild(statusbar);
86+
87+
// Show placeholder columns and rows while loading
4688
this._columns = this._previewColumns;
4789
this._rows = this._previewBody;
48-
49-
this._table.addClass("placeholder-glow");
50-
90+
this._table.classList.add("placeholder-glow");
5191
this.render();
52-
53-
this._filterCtrl = new webexpress.webui.SearchCtrl(this._filterDiv[0]);
54-
$(document).on(webexpress.webui.Event.CHANGE_FILTER_EVENT, (event, data) => {
55-
if(data.sender && data.sender === this._filterDiv[0]) {
92+
93+
// Initialize filter control
94+
this._filterCtrl = new webexpress.webui.SearchCtrl(this._filterDiv);
95+
96+
// Listen for filter changes
97+
document.addEventListener(webexpress.webui.Event.CHANGE_FILTER_EVENT, (event) => {
98+
const data = event.detail || {};
99+
if (data.sender && data.sender === this._filterDiv) {
56100
this._filter = data.value;
57101
this._receiveData();
58102
}
59103
});
60-
61-
this._paginationCtrl = new webexpress.webui.PaginationCtrl(this._paginationDiv[0]);
62-
$(document).on(webexpress.webui.Event.CHANGE_PAGE_EVENT, (event, data) => {
63-
if (data.sender && data.sender === this._paginationDiv[0]) {
104+
105+
// Initialize pagination control
106+
this._paginationCtrl = new webexpress.webui.PaginationCtrl(this._paginationDiv);
107+
108+
// Listen for pagination changes
109+
document.addEventListener(webexpress.webui.Event.CHANGE_PAGE_EVENT, (event) => {
110+
const data = event.detail || {};
111+
if (data.sender && data.sender === this._paginationDiv) {
64112
this._page = data.page;
65113
this._receiveData();
66-
67-
// scroll to the top of the table
68-
window.scrollTo(0, $(this._element).offset().top);
114+
// Scroll to top of the table
115+
window.scrollTo(0, element.offsetTop);
69116
}
70117
});
71-
118+
119+
// Initial data load
72120
this._receiveData();
73121
}
74122

75123
/**
76-
* Retrieve data from rest api.
124+
* Retrieve data from REST API and update the table.
77125
*/
78126
_receiveData() {
79-
this._progressDiv.css("visibility", "visible");
80-
81-
$.get(`${this._restUri}?filter=${this._filter}&page=${this._page}`)
82-
.done((response) => {
83-
84-
const page = response.page ?? 0; // current page number
85-
const pageSize = response.pageSize ?? 50; // number of items per page
86-
const total = response.total ?? 0; // total number of items
87-
const totalPages = Math.ceil(total / pageSize); // calculate the total number of pages
88-
const startIndex = page * pageSize + 1; // calculate the index of the first item on the current page
89-
const endIndex = Math.min(startIndex + pageSize - 1, total); // calculate the index of the last item on the current page
127+
this._progressDiv.style.visibility = "visible";
128+
129+
const filter = encodeURIComponent(this._filter ?? "");
130+
const url = `${this._restUri}?filter=${filter}&page=${this._page}`;
131+
132+
fetch(url)
133+
.then(res => {
134+
if (!res.ok) throw new Error("Request failed");
135+
return res.json();
136+
})
137+
.then(response => {
138+
// Extract paging and data info with fallback defaults
139+
const page = response.page ?? 0;
140+
const pageSize = response.pageSize ?? 50;
141+
const total = response.total ?? 0;
142+
const totalPages = Math.ceil(total / pageSize);
143+
const startIndex = page * pageSize + 1;
144+
const endIndex = Math.min(startIndex + pageSize - 1, total);
90145

91146
this._columns = response.columns;
92-
this._titleDiv.text(response.title);
93-
this._statusDiv.text(`${startIndex} - ${endIndex} / ${total}`);
94-
95-
// Trigger event when data has successfully arrived
96-
$(this._element).trigger(webexpress.webui.Event.DATA_ARRIVED_EVENT, {
97-
id: $(this._element).attr("id"),
98-
response: response
147+
this._titleDiv.textContent = response.title;
148+
this._statusDiv.textContent = `${startIndex} - ${endIndex} / ${total}`;
149+
150+
// Fire event for data arrival
151+
const evt = new CustomEvent(webexpress.webui.Event.DATA_ARRIVED_EVENT, {
152+
detail: {
153+
id: this._element.id,
154+
response: response
155+
}
99156
});
157+
this._element.dispatchEvent(evt);
158+
159+
// Update pagination control
100160
this._paginationCtrl.total = totalPages;
101161
this._paginationCtrl.page = page;
102-
103-
this._table.removeClass("placeholder-glow");
104162

105-
// Bind actions to existing commands
163+
this._table.classList.remove("placeholder-glow");
164+
165+
// Bind edit/delete actions to each row, if applicable
106166
this._rows = response.rows.map(row => {
107167
if (Array.isArray(row.options)) {
108168
this._hasOptions = true;
109169
row.options.forEach(option => {
170+
if (option.command === "edit") {
171+
option.action = () => this._editRow(row.id, option.uri);
172+
}
110173
if (option.command === "delete") {
111-
option.action = () => this._deleteRow.call(this, row.id);
174+
option.action = () => this._deleteRow(row.id);
112175
}
113176
});
114177
}
@@ -118,18 +181,41 @@ webexpress.webapp.TableCtrl = class extends webexpress.webui.TableCtrl {
118181
if (this._options?.length > 0) {
119182
this._hasOptions = true;
120183
}
121-
184+
122185
this.render();
123-
124-
this._progressDiv.css("visibility", "hidden");
186+
this._progressDiv.style.visibility = "hidden";
125187
})
126-
.fail((error) => {
188+
.catch(error => {
189+
// Log any errors in retrieving data
127190
console.error("The request could not be completed successfully:", error);
128191
});
129192
}
130193

131194
/**
132-
* Function to delete a row from the table and send a DELETE request to the REST API.
195+
* Edit a row and send a PUT request to the REST API.
196+
* @param {number} rowId - The ID of the row to be edited.
197+
* @param {string} uri - The uri for the form to be used for editing.
198+
*/
199+
_editRow(rowId, uri) {
200+
const editModal = new webexpress.webapp.ModalFormCtrl();
201+
editModal._uri = uri;
202+
editModal._selector = uri?.includes("#") ? "#" + uri.split("#")[1] : "form";
203+
204+
// Bind form submission logic
205+
editModal._form.addEventListener("submit", (event) => {
206+
fetch(`${this._restUri}?id=${rowId}`, { method: "PUT" })
207+
.then(response => {
208+
if (!response.ok) throw new Error("Failed to edit row");
209+
this._receiveData();
210+
})
211+
.catch(error => console.error(`Failed to edit row with ID ${rowId}.`, error));
212+
});
213+
214+
editModal.show();
215+
}
216+
217+
/**
218+
* Delete a row and send a DELETE request to the REST API.
133219
* @param {number} rowId - The ID of the row to be deleted.
134220
*/
135221
_deleteRow(rowId) {
@@ -139,7 +225,6 @@ webexpress.webapp.TableCtrl = class extends webexpress.webui.TableCtrl {
139225
fetch(`${this._restUri}?id=${rowId}`, { method: "DELETE" })
140226
.then(response => {
141227
if (!response.ok) throw new Error("Failed to delete row");
142-
143228
this._receiveData();
144229
})
145230
.catch(error => console.error(`Failed to delete row with ID ${rowId}.`, error));

src/WebExpress.WebApp/Internationalization/de

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ delete.label=Löschen
4343
delete.header=Löschen bestätigen
4444
delete.description=Möchten Sie wirklich löschen?
4545

46+
edit.label=Bearbeiten
47+
4648
search.label=Suche
4749
search.placeholder=Geben Sie hier Ihre Suchbegriffe ein.
4850

src/WebExpress.WebApp/Internationalization/en

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ delete.label=Delete
4343
delete.header=Confirm Delete
4444
delete.description=Are you sure you want to delete?
4545

46+
edit.label=Edit
47+
4648
search.label=Search
4749
search.placeholder=Enter your search terms here.
4850

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using WebExpress.WebCore.WebHtml;
2+
using WebExpress.WebUI.WebControl;
3+
using WebExpress.WebUI.WebPage;
4+
5+
namespace WebExpress.WebApp.WebControl
6+
{
7+
/// <summary>
8+
/// Represents a form control that renders as a hidden HTML element in the visual tree.
9+
/// </summary>
10+
public class ControlRestForm : ControlForm
11+
{
12+
/// <summary>
13+
/// Initializes a new instance of the class.
14+
/// </summary>
15+
public ControlRestForm()
16+
{
17+
}
18+
19+
/// <summary>
20+
/// Initializes a new instance of the class with the specified identifier.
21+
/// </summary>
22+
/// <param name="id">The unique identifier for the control rest form.</param>
23+
public ControlRestForm(string id)
24+
: base(id)
25+
{
26+
}
27+
28+
/// <summary>
29+
/// Convert to html.
30+
/// </summary>
31+
/// <param name="renderContext">The context in which the control is rendered.</param>
32+
/// <param name="visualTree">The visual tree representing the control's structure.</param>
33+
/// <returns>The control as html.</returns>
34+
public override IHtmlNode Render(IRenderControlContext renderContext, IVisualTreeControl visualTree)
35+
{
36+
var html = base.Render(renderContext, visualTree);
37+
38+
return new HtmlElementTextContentDiv(html)
39+
{
40+
Class = "d-none"
41+
};
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)