comparison publishable/resources/js/ServerTable.js @ 4:84c75d9d90be

Changing usage to be bootstrap 5, not everything is reviewed but it's been started
author luka
date Tue, 19 Aug 2025 20:33:35 -0400
parents
children f282c6ef1671
comparison
equal deleted inserted replaced
3:e107504fa22c 4:84c75d9d90be
1 class ServerTable {
2 /**
3 * @param {HTMLElement} rootEl - The container element for the table
4 * @param {Object} options - Configuration options
5 * @param {string} options.endpoint - API endpoint for data
6 * @param {Array} options.columns - Array of column configs: [{name, label, ...}]
7 * @param {number} [options.pageSize=10] - Default rows per page
8 * @param {Array} [options.initialSort=[]] - Default sort: [{col, dir}]
9 * @param {Object} [options.headers={}] - Additional headers for requests
10 * @param {string} [options.groupBy={}] - Which column to group by
11 * @param {Function} [options.groupRender={}] - Function to render the grouping
12 */
13 constructor(rootEl, options) {
14 this.rootEl = rootEl;
15 this.endpoint = options.endpoint;
16 this.columns = options.columns || [];
17 this.pageSize = options.pageSize || 10;
18 this.sort = options.initialSort || [];
19 this.filters = options.filters || {};
20 this.currentPage = 1;
21 this.headers = options.headers || {};
22 this.groupBy = options.groupBy;
23 this.groupRender =
24 options.groupRender ||
25 ((g, rows) =>
26 `<tr class="st-group-row"><td colspan="${this.columns.length}">Group: ${g}</td></tr>`);
27
28 this.skeleton = options.skeleton || `
29 <div class="st-table-container">
30 <table>
31 <thead>
32 </thead>
33 <tbody>
34 <!-- Data rows will go here -->
35 </tbody>
36 </table>
37 <div class="st-controls">
38 <span class="st-pagination"></span>
39 <span class="st-status"></span>
40 </div>
41 </div>
42 `;
43
44 this.state = {
45 loading: false,
46 error: null,
47 totalRecords: 0,
48 records: [],
49 };
50
51 // Render initial skeleton and fetch initial data
52 this.renderSkeleton();
53 this.fetchData();
54 }
55
56 renderSkeleton() {
57 // Build base structure: table shell, controls area
58 this.rootEl.innerHTML = this.skeleton;
59 // Store references
60 this.head = this.rootEl.querySelector("thead");
61 this.tbody = this.rootEl.querySelector("tbody");
62 this.statusEl = this.rootEl.querySelector(".st-status");
63 this.paginationEl = this.rootEl.querySelector(".st-pagination");
64
65 // Draw the header
66 this.head.innerHTML = `
67 <tr>
68 ${this.columns.map((col) => `<th scope="col">${col.label || col.name}</th>`).join("")}
69 </tr>
70 `;
71 }
72
73 async fetchData() {
74 this.setLoading(true);
75 const payload = {
76 page: this.currentPage,
77 page_size: this.pageSize,
78 sort: this.sort,
79 filters: this.filters,
80 };
81
82 try {
83 const res = await fetch(this.endpoint, {
84 method: "POST",
85 headers: {
86 ...this.headers,
87 "Content-Type": "application/json",
88 },
89 body: JSON.stringify(payload),
90 });
91
92 if (!res.ok) {
93 throw new Error(`Server responded with ${res.status}`);
94 }
95
96 const data = await res.json();
97 // Minimal shape validation
98 if (
99 !Array.isArray(data.records) ||
100 typeof data.total_records !== "number"
101 ) {
102 throw new Error("Malformed server response");
103 }
104
105 // Save data in state
106 this.state.records = data.records;
107 this.state.totalRecords = data.total_records;
108 this.state.error = null;
109
110 this.renderRows();
111 this.updateControls();
112 } catch (err) {
113 this.state.error = err.message;
114 this.renderError();
115 } finally {
116 this.setLoading(false);
117 }
118 }
119
120 setLoading(loading) {
121 this.state.loading = loading;
122 this.statusEl.textContent = loading ? "Loading..." : "";
123 }
124
125 renderRows() {
126 const { records } = this.state;
127 const cols = this.columns;
128 const groupBy = this.groupBy;
129 const groupRender =
130 typeof this.groupRender === "function"
131 ? this.groupRender
132 : (g, rows) =>
133 `<tr class="st-group-row"><td colspan="${cols.length}">Project: ${g}</td></tr>`;
134
135 if (!records || records.length === 0) {
136 this.tbody.innerHTML = `<tr><td colspan="${cols.length}">No data</td></tr>`;
137 return;
138 }
139
140 // Group if needed
141 if (groupBy) {
142 // Find grouping field or function
143 const getGroupValue =
144 typeof groupBy === "function" ? groupBy : (row) => row[groupBy];
145 let lastGroup = undefined;
146 let out = "";
147 let groupRows = [];
148
149 for (let i = 0; i < records.length; i++) {
150 const row = records[i];
151 const groupVal = getGroupValue(row);
152
153 // On group transition, flush previous group
154 if (i === 0 || groupVal !== lastGroup) {
155 if (i > 0) {
156 // Optionally do something with groupRows if groupRender wants it
157 }
158 // Insert group header row
159 out += groupRender(groupVal, []);
160 lastGroup = groupVal;
161 groupRows = [];
162 }
163
164 groupRows.push(row);
165
166 out += `<tr>${cols
167 .map((col, ci) => {
168 // If grouping by this column, suppress repeated values (leave blank except for first row in group)
169 if (
170 typeof groupBy === "string" &&
171 col.name === groupBy &&
172 groupVal === lastGroup &&
173 groupRows.length > 1
174 ) {
175 return `<td></td>`;
176 }
177 return `<td>${
178 typeof col.render === "function"
179 ? col.render(row, col, i)
180 : row[col.name]
181 }</td>`;
182 })
183 .join("")}</tr>`;
184 }
185 this.tbody.innerHTML = out;
186 } else {
187 // No grouping
188 this.tbody.innerHTML = records
189 .map(
190 (row, i) =>
191 `<tr>${cols
192 .map(
193 (col) =>
194 `<td>${
195 typeof col.render === "function"
196 ? col.render(row, col, i)
197 : row[col.name]
198 }</td>`,
199 )
200 .join("")}</tr>`,
201 )
202 .join("");
203 }
204 }
205 renderError() {
206 this.tbody.innerHTML = `<tr><td colspan="${this.columns.length}" style="color:red">${this.state.error}</td></tr>`;
207 }
208
209 updateControls() {
210 // Basic pagination info (full controls to come later)
211 const from = 1 + (this.currentPage - 1) * this.pageSize;
212 const to = Math.min(
213 this.currentPage * this.pageSize,
214 this.state.totalRecords,
215 );
216 this.paginationEl.textContent = `Showing ${from}-${to} of ${this.state.totalRecords}`;
217 }
218
219 // PUBLIC: force reload
220 reload() {
221 this.fetchData();
222 }
223
224 // PUBLIC: update filters, resets to page 1
225 setFilters(newFilters) {
226 this.filters = newFilters;
227 this.currentPage = 1;
228 this.fetchData();
229 }
230 }
231
232 // Example usage (not part of module export):
233 /*
234 const table = new ServerTable(document.getElementById('my-table'), {
235 endpoint: '/tickets/get_data',
236 columns: [
237 {name: 'id', label: 'ID'},
238 {name: 'subject', label: 'Subject'},
239 {name: 'project', label: 'Project'},
240 {name: 'created_at', label: 'Created At'},
241 ],
242 pageSize: 10,
243 initialSort: [{col: 'created_at', dir: 'desc'}]
244 });
245 */
246
247 window.ServerTable = ServerTable;