panther 4.3.7__py3-none-any.whl → 5.0.0b2__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.
- panther/__init__.py +1 -1
- panther/_load_configs.py +78 -64
- panther/_utils.py +1 -1
- panther/app.py +126 -60
- panther/authentications.py +26 -9
- panther/base_request.py +27 -2
- panther/base_websocket.py +26 -27
- panther/cli/create_command.py +1 -0
- panther/cli/main.py +19 -27
- panther/cli/monitor_command.py +8 -4
- panther/cli/template.py +11 -6
- panther/cli/utils.py +3 -2
- panther/configs.py +7 -9
- panther/db/cursor.py +23 -7
- panther/db/models.py +26 -19
- panther/db/queries/base_queries.py +1 -1
- panther/db/queries/mongodb_queries.py +177 -13
- panther/db/queries/pantherdb_queries.py +5 -5
- panther/db/queries/queries.py +1 -1
- panther/events.py +10 -4
- panther/exceptions.py +24 -2
- panther/generics.py +2 -2
- panther/main.py +90 -117
- panther/middlewares/__init__.py +1 -1
- panther/middlewares/base.py +15 -19
- panther/middlewares/monitoring.py +42 -0
- panther/openapi/__init__.py +1 -0
- panther/openapi/templates/openapi.html +27 -0
- panther/openapi/urls.py +5 -0
- panther/openapi/utils.py +167 -0
- panther/openapi/views.py +101 -0
- panther/pagination.py +1 -1
- panther/panel/middlewares.py +10 -0
- panther/panel/templates/base.html +14 -0
- panther/panel/templates/create.html +21 -0
- panther/panel/templates/create.js +1270 -0
- panther/panel/templates/detail.html +55 -0
- panther/panel/templates/home.html +9 -0
- panther/panel/templates/home.js +30 -0
- panther/panel/templates/login.html +47 -0
- panther/panel/templates/sidebar.html +13 -0
- panther/panel/templates/table.html +73 -0
- panther/panel/templates/table.js +339 -0
- panther/panel/urls.py +10 -5
- panther/panel/utils.py +98 -0
- panther/panel/views.py +143 -0
- panther/request.py +3 -0
- panther/response.py +91 -53
- panther/routings.py +7 -2
- panther/serializer.py +1 -1
- panther/utils.py +34 -26
- panther/websocket.py +3 -0
- {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/METADATA +19 -17
- panther-5.0.0b2.dist-info/RECORD +75 -0
- {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/WHEEL +1 -1
- panther-4.3.7.dist-info/RECORD +0 -57
- {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/entry_points.txt +0 -0
- {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/licenses/LICENSE +0 -0
- {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1270 @@
|
|
1
|
+
const schema = JSON.parse(`{{fields|tojson|safe}}`);
|
2
|
+
function toggleObjectVisibility(checkbox, contentId) {
|
3
|
+
const content = document.getElementById(contentId);
|
4
|
+
content.classList.toggle("hidden", !checkbox.checked);
|
5
|
+
|
6
|
+
// Disable/enable all inputs within the container
|
7
|
+
const inputs = content.querySelectorAll("input, select, textarea");
|
8
|
+
inputs.forEach((input) => {
|
9
|
+
input.disabled = !checkbox.checked;
|
10
|
+
});
|
11
|
+
}
|
12
|
+
|
13
|
+
function createObjectInputs(objectSchema, container, prefix = "") {
|
14
|
+
if (!objectSchema || !objectSchema.fields) return;
|
15
|
+
|
16
|
+
Object.entries(objectSchema.fields).forEach(([fieldName, field]) => {
|
17
|
+
const fullFieldName = prefix ? `${prefix}.${fieldName}` : fieldName;
|
18
|
+
|
19
|
+
// Check if it's an array type
|
20
|
+
if (field.type.includes("array")) {
|
21
|
+
// If items is specified, use it
|
22
|
+
if (field.items) {
|
23
|
+
const itemType = field.items.replace("$", "");
|
24
|
+
createArrayField(fieldName, itemType, container, fullFieldName, field);
|
25
|
+
} else {
|
26
|
+
// Handle array without items specification (simple types)
|
27
|
+
createSimpleArrayField(fieldName, container, fullFieldName, field);
|
28
|
+
}
|
29
|
+
} else if (
|
30
|
+
Array.isArray(field.type) &&
|
31
|
+
field.type.some((t) => t.startsWith("$"))
|
32
|
+
) {
|
33
|
+
const objectType = field.type
|
34
|
+
.find((t) => t.startsWith("$"))
|
35
|
+
.replace("$", "");
|
36
|
+
createNestedObjectField(
|
37
|
+
fieldName,
|
38
|
+
objectType,
|
39
|
+
field,
|
40
|
+
container,
|
41
|
+
fullFieldName
|
42
|
+
);
|
43
|
+
} else {
|
44
|
+
createBasicInput(fieldName, field, container, fullFieldName);
|
45
|
+
}
|
46
|
+
});
|
47
|
+
}
|
48
|
+
|
49
|
+
function toggleArrayVisibility(checkbox, contentId) {
|
50
|
+
const content = document.getElementById(contentId);
|
51
|
+
if (content) {
|
52
|
+
content.classList.toggle("hidden", !checkbox.checked);
|
53
|
+
|
54
|
+
// Also enable/disable inputs
|
55
|
+
const inputs = content.querySelectorAll("input, select, textarea");
|
56
|
+
inputs.forEach((input) => {
|
57
|
+
input.disabled = !checkbox.checked;
|
58
|
+
});
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
function createSimpleArrayField(fieldName, container, fullFieldName, field) {
|
63
|
+
const arrayContainer = document.createElement("div");
|
64
|
+
arrayContainer.className = "border border-gray-400 p-4 rounded-lg space-y-2";
|
65
|
+
|
66
|
+
const spreadsheetId = `${fullFieldName}-container`;
|
67
|
+
|
68
|
+
const header = document.createElement("div");
|
69
|
+
header.className = "flex items-center justify-between mb-4";
|
70
|
+
header.innerHTML = `
|
71
|
+
<h3 class="text-lg font-medium">${field.title || fieldName}</h3>
|
72
|
+
${
|
73
|
+
field.type.includes("null")
|
74
|
+
? `
|
75
|
+
<label class="flex items-center space-x-3">
|
76
|
+
<input type="checkbox"
|
77
|
+
id="${fullFieldName}_toggle"
|
78
|
+
class="form-checkbox h-5 w-5 text-blue-600 bg-gray-700 border-gray-600 rounded"
|
79
|
+
${!field.required ? "" : "checked disabled"}
|
80
|
+
onchange="toggleArrayVisibility(this, '${spreadsheetId}')">
|
81
|
+
<span class="text-sm font-medium">Include ${
|
82
|
+
field.title || fieldName
|
83
|
+
}</span>
|
84
|
+
</label>
|
85
|
+
`
|
86
|
+
: ""
|
87
|
+
}
|
88
|
+
`;
|
89
|
+
|
90
|
+
const content = document.createElement("div");
|
91
|
+
content.innerHTML = `
|
92
|
+
<div class="flex justify-between items-center mb-4">
|
93
|
+
<button type="button"
|
94
|
+
class="bg-green-600 px-3 py-1 rounded text-sm"
|
95
|
+
onclick="addSimpleArrayRow('${fullFieldName}', '${spreadsheetId}')">
|
96
|
+
Add Item
|
97
|
+
</button>
|
98
|
+
</div>
|
99
|
+
<div id="${spreadsheetId}" class="array-items space-y-4">
|
100
|
+
</div>
|
101
|
+
`;
|
102
|
+
|
103
|
+
if (field.type.includes("null") && !field.required) {
|
104
|
+
content.classList.add("hidden");
|
105
|
+
}
|
106
|
+
|
107
|
+
arrayContainer.appendChild(header);
|
108
|
+
arrayContainer.appendChild(content);
|
109
|
+
container.appendChild(arrayContainer);
|
110
|
+
}
|
111
|
+
|
112
|
+
function addSimpleArrayRow(arrayName, containerId) {
|
113
|
+
const container = document.getElementById(containerId);
|
114
|
+
if (!container) {
|
115
|
+
console.error("Invalid container");
|
116
|
+
return;
|
117
|
+
}
|
118
|
+
|
119
|
+
const rowIndex = container.children.length;
|
120
|
+
const rowContainer = document.createElement("div");
|
121
|
+
rowContainer.className = "flex items-center space-x-4";
|
122
|
+
|
123
|
+
const input = document.createElement("input");
|
124
|
+
input.type = "text";
|
125
|
+
input.name = `${arrayName}[${rowIndex}]`;
|
126
|
+
input.className =
|
127
|
+
"flex-grow bg-gray-700 border border-gray-600 rounded px-3 py-2";
|
128
|
+
|
129
|
+
const deleteButton = document.createElement("button");
|
130
|
+
deleteButton.className = "bg-red-600 px-3 py-1 rounded text-sm";
|
131
|
+
deleteButton.textContent = "Delete";
|
132
|
+
deleteButton.onclick = () => {
|
133
|
+
rowContainer.remove();
|
134
|
+
reindexArrayItems(containerId);
|
135
|
+
};
|
136
|
+
rowContainer.appendChild(input);
|
137
|
+
rowContainer.appendChild(deleteButton);
|
138
|
+
container.appendChild(rowContainer);
|
139
|
+
}
|
140
|
+
|
141
|
+
function reindexArrayItems(containerId) {
|
142
|
+
const container = document.getElementById(containerId);
|
143
|
+
const items = container.children;
|
144
|
+
|
145
|
+
// Update the index for each remaining item
|
146
|
+
Array.from(items).forEach((item, newIndex) => {
|
147
|
+
const input = item.querySelector("input");
|
148
|
+
if (input) {
|
149
|
+
const oldName = input.name;
|
150
|
+
const baseName = oldName.split("[")[0];
|
151
|
+
input.name = `${baseName}[${newIndex}]`;
|
152
|
+
}
|
153
|
+
});
|
154
|
+
}
|
155
|
+
function createNestedObjectField(
|
156
|
+
fieldName,
|
157
|
+
objectType,
|
158
|
+
field,
|
159
|
+
container,
|
160
|
+
fullFieldName
|
161
|
+
) {
|
162
|
+
const objectWrapper = document.createElement("div");
|
163
|
+
objectWrapper.className = "space-y-4 border border-gray-400 p-4 rounded-lg";
|
164
|
+
|
165
|
+
const cleanObjectType = objectType.replace(/^\$/, "");
|
166
|
+
|
167
|
+
const header = document.createElement("div");
|
168
|
+
header.className = "flex items-center justify-between mb-4";
|
169
|
+
header.innerHTML = `<h3 class="text-lg font-medium">${
|
170
|
+
field.title || fieldName
|
171
|
+
}</h3>`;
|
172
|
+
|
173
|
+
const contentContainer = document.createElement("div");
|
174
|
+
contentContainer.id = `${fullFieldName}_content`;
|
175
|
+
contentContainer.className = "space-y-4";
|
176
|
+
|
177
|
+
// Add toggle for optional objects
|
178
|
+
if (!field.required) {
|
179
|
+
const toggleContainer = document.createElement("div");
|
180
|
+
toggleContainer.className = "flex items-center space-x-3";
|
181
|
+
|
182
|
+
const checkbox = document.createElement("input");
|
183
|
+
checkbox.type = "checkbox";
|
184
|
+
checkbox.id = `${fullFieldName}_toggle`;
|
185
|
+
checkbox.className = "form-checkbox h-5 w-5 text-blue-600";
|
186
|
+
|
187
|
+
const label = document.createElement("label");
|
188
|
+
label.htmlFor = `${fullFieldName}_toggle`;
|
189
|
+
label.textContent = `Include ${field.title || fieldName}`;
|
190
|
+
|
191
|
+
toggleContainer.appendChild(checkbox);
|
192
|
+
toggleContainer.appendChild(label);
|
193
|
+
header.appendChild(toggleContainer);
|
194
|
+
|
195
|
+
// Toggle handler to enable/disable validation
|
196
|
+
checkbox.addEventListener("change", (e) => {
|
197
|
+
const fields = contentContainer.querySelectorAll(
|
198
|
+
"input, select, textarea"
|
199
|
+
);
|
200
|
+
fields.forEach((field) => {
|
201
|
+
if (!e.target.checked) {
|
202
|
+
// When unchecked, disable and remove required attribute
|
203
|
+
field.disabled = true;
|
204
|
+
field.required = false;
|
205
|
+
field.value = ""; // Clear the value
|
206
|
+
} else {
|
207
|
+
// When checked, enable and restore required if it was originally required
|
208
|
+
field.disabled = false;
|
209
|
+
field.required = field.dataset.originalRequired === "true";
|
210
|
+
}
|
211
|
+
});
|
212
|
+
});
|
213
|
+
|
214
|
+
// Initialize as unchecked
|
215
|
+
checkbox.checked = false;
|
216
|
+
setTimeout(() => checkbox.dispatchEvent(new Event("change")), 0);
|
217
|
+
}
|
218
|
+
|
219
|
+
const nestedSchema = schema.$[cleanObjectType];
|
220
|
+
createObjectInputs(nestedSchema, contentContainer, fullFieldName);
|
221
|
+
|
222
|
+
// Store original required state for all nested fields
|
223
|
+
contentContainer
|
224
|
+
.querySelectorAll("input, select, textarea")
|
225
|
+
.forEach((field) => {
|
226
|
+
field.dataset.originalRequired = field.required;
|
227
|
+
if (!field.required) {
|
228
|
+
field.required = false;
|
229
|
+
}
|
230
|
+
});
|
231
|
+
|
232
|
+
objectWrapper.appendChild(header);
|
233
|
+
objectWrapper.appendChild(contentContainer);
|
234
|
+
container.appendChild(objectWrapper);
|
235
|
+
}
|
236
|
+
|
237
|
+
function createObjectInputs(objectSchema, container, prefix = "") {
|
238
|
+
if (!objectSchema || !objectSchema.fields) return;
|
239
|
+
|
240
|
+
Object.entries(objectSchema.fields).forEach(([fieldName, field]) => {
|
241
|
+
const fullFieldName = prefix ? `${prefix}.${fieldName}` : fieldName;
|
242
|
+
|
243
|
+
// Check if it's an array type
|
244
|
+
if (field.type.includes("array")) {
|
245
|
+
// If items is specified, use it
|
246
|
+
if (field.items) {
|
247
|
+
const itemType = field.items.replace("$", "");
|
248
|
+
createArrayField(fieldName, itemType, container, fullFieldName, field);
|
249
|
+
} else {
|
250
|
+
// Handle array without items specification (simple types)
|
251
|
+
createSimpleArrayField(fieldName, container, fullFieldName, field);
|
252
|
+
}
|
253
|
+
} else if (
|
254
|
+
// Check if type is an array and contains a reference to another type
|
255
|
+
(Array.isArray(field.type) &&
|
256
|
+
field.type.some((t) => t.startsWith("$"))) ||
|
257
|
+
// Or if type is a string and is a reference
|
258
|
+
(typeof field.type === "string" && field.type.startsWith("$"))
|
259
|
+
) {
|
260
|
+
const objectType = Array.isArray(field.type)
|
261
|
+
? field.type.find((t) => t.startsWith("$"))
|
262
|
+
: field.type;
|
263
|
+
createNestedObjectField(
|
264
|
+
fieldName,
|
265
|
+
objectType,
|
266
|
+
field,
|
267
|
+
container,
|
268
|
+
fullFieldName
|
269
|
+
);
|
270
|
+
} else {
|
271
|
+
createBasicInput(fieldName, field, container, fullFieldName);
|
272
|
+
}
|
273
|
+
});
|
274
|
+
}
|
275
|
+
|
276
|
+
function createBasicInput(fieldName, field, container, fullFieldName) {
|
277
|
+
const inputWrapper = document.createElement("div");
|
278
|
+
inputWrapper.className = "space-y-2";
|
279
|
+
if (fieldName === "id" && typeof isUpdate === "undefined") {
|
280
|
+
return;
|
281
|
+
}
|
282
|
+
|
283
|
+
// Hide ID field in update mode
|
284
|
+
if (fieldName === "_id" && isUpdate) {
|
285
|
+
const inputWrapper = document.createElement("div");
|
286
|
+
inputWrapper.className = "space-y-2";
|
287
|
+
inputWrapper.style.display = "none"; // Hide the entire wrapper
|
288
|
+
|
289
|
+
inputWrapper.innerHTML = `
|
290
|
+
<label class="block">
|
291
|
+
<span class="text-sm font-medium">ID</span>
|
292
|
+
<input type="text" name="_id" readonly
|
293
|
+
class="w-full mt-1 p-2 bg-slate-900 rounded text-gray-300">
|
294
|
+
</label>
|
295
|
+
`;
|
296
|
+
|
297
|
+
container.appendChild(inputWrapper);
|
298
|
+
return;
|
299
|
+
}
|
300
|
+
|
301
|
+
let inputHTML = "";
|
302
|
+
const defaultValue =
|
303
|
+
field.default !== undefined ? `value="${field.default}"` : "";
|
304
|
+
const requiredText = field.required
|
305
|
+
? `<span class="text-red-500 text-sm ml-2">* Required</span>`
|
306
|
+
: "";
|
307
|
+
if (field.type.includes("boolean")) {
|
308
|
+
inputHTML = `
|
309
|
+
<label class="flex items-center space-x-3">
|
310
|
+
<input type="checkbox" name="${fullFieldName}"
|
311
|
+
${field.default ? "checked" : ""}
|
312
|
+
class="form-checkbox h-5 w-5 text-blue-600 bg-gray-700 border-gray-600 rounded">
|
313
|
+
<span class="text-sm font-medium">${field.title || fieldName}</span>
|
314
|
+
${requiredText}
|
315
|
+
</label>
|
316
|
+
`;
|
317
|
+
} else if (field.type.includes("string")) {
|
318
|
+
inputHTML = `
|
319
|
+
<label class="block">
|
320
|
+
<span class="text-sm font-medium">${
|
321
|
+
field.title || fieldName
|
322
|
+
} ${requiredText}</span>
|
323
|
+
<input type="text" name="${fullFieldName}"
|
324
|
+
${defaultValue}
|
325
|
+
${field.required ? "required" : ""}
|
326
|
+
class="w-full mt-1 p-2 bg-slate-900 rounded text-gray-300">
|
327
|
+
</label>
|
328
|
+
`;
|
329
|
+
} else if (field.type.includes("integer")) {
|
330
|
+
const min = field.min !== undefined ? `min="${field.min}"` : "";
|
331
|
+
const max = field.max !== undefined ? `max="${field.max}"` : "";
|
332
|
+
|
333
|
+
inputHTML = `
|
334
|
+
<label class="block">
|
335
|
+
<span class="text-sm font-medium">${
|
336
|
+
field.title || fieldName
|
337
|
+
} ${requiredText}</span>
|
338
|
+
<input type="number" name="${fullFieldName}"
|
339
|
+
${defaultValue} ${min} ${max}
|
340
|
+
${field.required ? "required" : ""}
|
341
|
+
class="w-full mt-1 p-2 bg-slate-900 rounded text-gray-300">
|
342
|
+
</label>
|
343
|
+
`;
|
344
|
+
}
|
345
|
+
|
346
|
+
inputWrapper.innerHTML = inputHTML;
|
347
|
+
container.appendChild(inputWrapper);
|
348
|
+
}
|
349
|
+
|
350
|
+
function createArrayField(fieldName, itemType, container, fullFieldName) {
|
351
|
+
const arrayContainer = document.createElement("div");
|
352
|
+
arrayContainer.className = "border border-gray-700 p-4 rounded-lg space-y-4";
|
353
|
+
|
354
|
+
const spreadsheetId = `${fullFieldName}-container`;
|
355
|
+
|
356
|
+
// Make sure itemType exists in schema.$
|
357
|
+
if (!schema.$ || !schema.$[itemType]) {
|
358
|
+
console.error(`Schema type ${itemType} not found`);
|
359
|
+
return;
|
360
|
+
}
|
361
|
+
|
362
|
+
arrayContainer.innerHTML = `
|
363
|
+
<div class="flex justify-between items-center mb-4">
|
364
|
+
<h3 class="text-lg font-medium">${fieldName}</h3>
|
365
|
+
<button type="button"
|
366
|
+
class="bg-green-600 px-3 py-1 rounded text-sm"
|
367
|
+
onclick="addArrayRow('${fullFieldName}', '${itemType}', '${spreadsheetId}')">
|
368
|
+
Add Item
|
369
|
+
</button>
|
370
|
+
</div>
|
371
|
+
<div id="${spreadsheetId}" class="array-items space-y-4"></div>
|
372
|
+
`;
|
373
|
+
|
374
|
+
container.appendChild(arrayContainer);
|
375
|
+
}
|
376
|
+
|
377
|
+
function addArrayRow(arrayName, itemType, containerId) {
|
378
|
+
const container = document.getElementById(containerId);
|
379
|
+
if (!container || !schema.$ || !schema.$[itemType]) {
|
380
|
+
console.error("Invalid container or schema type");
|
381
|
+
return;
|
382
|
+
}
|
383
|
+
|
384
|
+
const itemSchema = schema.$[itemType];
|
385
|
+
const rowIndex = container.children.length;
|
386
|
+
const rowContainer = document.createElement("div");
|
387
|
+
rowContainer.className =
|
388
|
+
"flex gap-2 items-start space-x-4 py-4 pl-3 border pr-2 bg-gray-800/80 border-gray-500 rounded-lg";
|
389
|
+
|
390
|
+
const itemContent = document.createElement("div");
|
391
|
+
itemContent.className = "flex-grow flex flex-col gap-8 w-full";
|
392
|
+
|
393
|
+
createObjectInputs(itemSchema, itemContent, `${arrayName}[${rowIndex}]`);
|
394
|
+
|
395
|
+
const deleteButton = document.createElement("button");
|
396
|
+
deleteButton.className = "bg-red-600 m-1 p-1 min-w-8 min-h-8 rounded text-sm";
|
397
|
+
deleteButton.textContent = "X";
|
398
|
+
deleteButton.onclick = () => {
|
399
|
+
rowContainer.remove();
|
400
|
+
reindexComplexArrayItems(containerId);
|
401
|
+
};
|
402
|
+
rowContainer.appendChild(itemContent);
|
403
|
+
rowContainer.appendChild(deleteButton);
|
404
|
+
container.appendChild(rowContainer);
|
405
|
+
}
|
406
|
+
function reindexComplexArrayItems(containerId) {
|
407
|
+
const container = document.getElementById(containerId);
|
408
|
+
const items = container.children;
|
409
|
+
|
410
|
+
Array.from(items).forEach((item, newIndex) => {
|
411
|
+
const inputs = item.querySelectorAll("input, select, textarea");
|
412
|
+
inputs.forEach((input) => {
|
413
|
+
const oldName = input.name;
|
414
|
+
const fieldPart = oldName.split("]")[1]; // Get the part after the index
|
415
|
+
const baseName = oldName.split("[")[0];
|
416
|
+
input.name = `${baseName}[${newIndex}]${fieldPart || ""}`;
|
417
|
+
});
|
418
|
+
});
|
419
|
+
}
|
420
|
+
function openObjectModal(fieldName, objectType) {
|
421
|
+
// Create modal for editing nested object
|
422
|
+
const modal = document.createElement("div");
|
423
|
+
modal.className =
|
424
|
+
"fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center";
|
425
|
+
modal.innerHTML = `
|
426
|
+
<div class="bg-gray-800 p-6 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
427
|
+
<h3 class="text-lg font-medium mb-4">Edit ${fieldName}</h3>
|
428
|
+
<div class="object-inputs space-y-4"></div>
|
429
|
+
<div class="mt-4 flex justify-end space-x-2">
|
430
|
+
<button type="button" class="bg-gray-600 px-4 py-2 rounded" onclick="this.closest('.fixed').remove()">
|
431
|
+
Cancel
|
432
|
+
</button>
|
433
|
+
<button type="button" class="bg-blue-600 px-4 py-2 rounded" onclick="saveObjectModal(this)">
|
434
|
+
Save
|
435
|
+
</button>
|
436
|
+
</div>
|
437
|
+
</div>
|
438
|
+
`;
|
439
|
+
|
440
|
+
const objectSchema = schema.$[objectType];
|
441
|
+
createObjectInputs(
|
442
|
+
objectSchema,
|
443
|
+
modal.querySelector(".object-inputs"),
|
444
|
+
fieldName
|
445
|
+
);
|
446
|
+
|
447
|
+
document.body.appendChild(modal);
|
448
|
+
}
|
449
|
+
|
450
|
+
const dynamicInputs = document.getElementById("dynamicInputs");
|
451
|
+
|
452
|
+
createObjectInputs(schema, dynamicInputs);
|
453
|
+
|
454
|
+
// Check if the page is in update mode
|
455
|
+
if (typeof isUpdate !== "undefined" && isUpdate) {
|
456
|
+
// Populate the form with existing data for update mode
|
457
|
+
populateFormWithExistingData(existingData);
|
458
|
+
} else {
|
459
|
+
console.log("Create mode: No existing data to populate.");
|
460
|
+
}
|
461
|
+
|
462
|
+
// Function to populate the form with existing data
|
463
|
+
function populateFormWithExistingData(data) {
|
464
|
+
console.log("Populating form with data:", data);
|
465
|
+
|
466
|
+
// Handle simple fields first
|
467
|
+
Object.entries(data).forEach(([key, value]) => {
|
468
|
+
const input = document.querySelector(`[name="${key}"]`);
|
469
|
+
if (input) {
|
470
|
+
if (input.type === "checkbox") {
|
471
|
+
input.checked = Boolean(value);
|
472
|
+
} else {
|
473
|
+
input.value = value !== null ? value : "";
|
474
|
+
}
|
475
|
+
}
|
476
|
+
});
|
477
|
+
|
478
|
+
// Special case for the `_id` field
|
479
|
+
// Find the ID input field
|
480
|
+
const idInput = document.querySelector(`[name="_id"]`);
|
481
|
+
if (idInput) {
|
482
|
+
// If we're in update mode and have an ID, set the value
|
483
|
+
if (data && data.id) {
|
484
|
+
idInput.value = data.id;
|
485
|
+
}
|
486
|
+
|
487
|
+
// Hide the input itself
|
488
|
+
idInput.style.display = "none";
|
489
|
+
|
490
|
+
// Also hide the parent container (which likely contains the label)
|
491
|
+
const parentContainer =
|
492
|
+
idInput.closest(".form-group") || idInput.parentElement;
|
493
|
+
if (parentContainer) {
|
494
|
+
parentContainer.style.display = "none";
|
495
|
+
}
|
496
|
+
|
497
|
+
// Keep the field in the form data by making it readonly but not disabled
|
498
|
+
idInput.setAttribute("readonly", true);
|
499
|
+
}
|
500
|
+
|
501
|
+
// Handle nested objects and arrays
|
502
|
+
populateNestedData(data);
|
503
|
+
}
|
504
|
+
|
505
|
+
function populateNestedData(data, prefix = "") {
|
506
|
+
Object.entries(data).forEach(([key, value]) => {
|
507
|
+
const fullPath = prefix ? `${prefix}.${key}` : key;
|
508
|
+
|
509
|
+
// Handle arrays
|
510
|
+
if (Array.isArray(value)) {
|
511
|
+
// Find the array container
|
512
|
+
const containerID = `${fullPath}-container`;
|
513
|
+
const container = document.getElementById(containerID);
|
514
|
+
|
515
|
+
if (container) {
|
516
|
+
// Ensure toggle is checked for this array if it exists
|
517
|
+
const toggle = document.getElementById(`${fullPath}_toggle`);
|
518
|
+
if (toggle) {
|
519
|
+
toggle.checked = true;
|
520
|
+
toggleArrayVisibility(toggle, containerID);
|
521
|
+
}
|
522
|
+
|
523
|
+
// Clear any existing items
|
524
|
+
container.innerHTML = "";
|
525
|
+
|
526
|
+
// Recreate each array item
|
527
|
+
value.forEach((item, index) => {
|
528
|
+
if (typeof item === "object" && item !== null) {
|
529
|
+
// Complex object in array
|
530
|
+
const itemType = determineItemType(item);
|
531
|
+
if (itemType) {
|
532
|
+
addArrayRow(fullPath, itemType, containerID);
|
533
|
+
|
534
|
+
// After adding the row, populate its fields
|
535
|
+
// The new row should be the last child of the container
|
536
|
+
const newRow = container.lastElementChild;
|
537
|
+
if (newRow) {
|
538
|
+
// Find all inputs in this row and set their values
|
539
|
+
Object.entries(item).forEach(([itemKey, itemValue]) => {
|
540
|
+
const itemPath = `${fullPath}[${index}].${itemKey}`;
|
541
|
+
populateItemField(newRow, itemPath, itemValue);
|
542
|
+
});
|
543
|
+
}
|
544
|
+
}
|
545
|
+
} else {
|
546
|
+
// Simple value in array
|
547
|
+
addSimpleArrayRow(fullPath, containerID);
|
548
|
+
// Get the last added row and set its value
|
549
|
+
const rowInput = container.lastElementChild.querySelector("input");
|
550
|
+
if (rowInput) {
|
551
|
+
rowInput.value = item;
|
552
|
+
}
|
553
|
+
}
|
554
|
+
});
|
555
|
+
}
|
556
|
+
}
|
557
|
+
// Handle nested objects
|
558
|
+
else if (typeof value === "object" && value !== null) {
|
559
|
+
// Find the content container for this object
|
560
|
+
const contentId = `${fullPath}_content`;
|
561
|
+
const content = document.getElementById(contentId);
|
562
|
+
|
563
|
+
if (content) {
|
564
|
+
// Enable the toggle if it exists
|
565
|
+
const toggle = document.getElementById(`${fullPath}_toggle`);
|
566
|
+
if (toggle) {
|
567
|
+
toggle.checked = true;
|
568
|
+
// Manually enable all fields in this container
|
569
|
+
const inputs = content.querySelectorAll("input, select, textarea");
|
570
|
+
inputs.forEach((input) => {
|
571
|
+
input.disabled = false;
|
572
|
+
});
|
573
|
+
}
|
574
|
+
|
575
|
+
// Populate the fields in this object
|
576
|
+
Object.entries(value).forEach(([nestedKey, nestedValue]) => {
|
577
|
+
const nestedPath = `${fullPath}.${nestedKey}`;
|
578
|
+
populateItemField(content, nestedPath, nestedValue);
|
579
|
+
});
|
580
|
+
}
|
581
|
+
}
|
582
|
+
});
|
583
|
+
}
|
584
|
+
|
585
|
+
function populateItemField(container, fieldPath, value) {
|
586
|
+
const input = container.querySelector(`[name="${fieldPath}"]`);
|
587
|
+
if (input) {
|
588
|
+
if (input.type === "checkbox") {
|
589
|
+
input.checked = Boolean(value);
|
590
|
+
} else {
|
591
|
+
input.value = value !== null ? value : "";
|
592
|
+
}
|
593
|
+
}
|
594
|
+
}
|
595
|
+
|
596
|
+
function determineItemType(item) {
|
597
|
+
// Try to match the item structure with schema definitions
|
598
|
+
for (const [typeName, typeSchema] of Object.entries(schema.$)) {
|
599
|
+
if (typeSchema && typeSchema.fields) {
|
600
|
+
const fieldNames = Object.keys(typeSchema.fields);
|
601
|
+
// If most of the item keys match the schema fields, assume it's this type
|
602
|
+
const matchingFields = fieldNames.filter((field) => field in item);
|
603
|
+
if (
|
604
|
+
matchingFields.length > 0 &&
|
605
|
+
matchingFields.length / fieldNames.length >= 0.5
|
606
|
+
) {
|
607
|
+
return typeName;
|
608
|
+
}
|
609
|
+
}
|
610
|
+
}
|
611
|
+
return null;
|
612
|
+
}
|
613
|
+
|
614
|
+
document
|
615
|
+
.getElementById(isUpdate ? "updateForm" : "createForm")
|
616
|
+
.addEventListener("submit", async (e) => {
|
617
|
+
e.preventDefault();
|
618
|
+
|
619
|
+
// Create an object to hold our updated data
|
620
|
+
const updatedData = {};
|
621
|
+
|
622
|
+
// Extract the current form values
|
623
|
+
const formData = new FormData(e.target);
|
624
|
+
|
625
|
+
// Debug log
|
626
|
+
console.log("Form data entries:");
|
627
|
+
for (let [key, value] of formData.entries()) {
|
628
|
+
console.log(`FormData Key: ${key}, Value: ${value}`);
|
629
|
+
}
|
630
|
+
|
631
|
+
// Process each form field
|
632
|
+
for (let [key, value] of formData.entries()) {
|
633
|
+
// Skip disabled fields (they won't be included in FormData anyway)
|
634
|
+
const field = e.target.querySelector(`[name="${key}"]`);
|
635
|
+
if (field && field.disabled) continue;
|
636
|
+
|
637
|
+
// Parse the key to handle nested structures
|
638
|
+
const parts = key.split(/[\[\].]/).filter(Boolean);
|
639
|
+
|
640
|
+
// Start at the root of our data object
|
641
|
+
let current = updatedData;
|
642
|
+
|
643
|
+
// Build the nested structure
|
644
|
+
for (let i = 0; i < parts.length; i++) {
|
645
|
+
const part = parts[i];
|
646
|
+
|
647
|
+
if (i === parts.length - 1) {
|
648
|
+
// We're at the leaf node, set the actual value
|
649
|
+
|
650
|
+
// Handle boolean values from checkboxes
|
651
|
+
if (field && field.type === "checkbox") {
|
652
|
+
value = field.checked;
|
653
|
+
}
|
654
|
+
// Convert string "true"/"false" to boolean
|
655
|
+
else if (value === "true" || value === "false") {
|
656
|
+
value = value === "true";
|
657
|
+
}
|
658
|
+
// Convert numeric strings to numbers
|
659
|
+
else if (!isNaN(value) && value !== "") {
|
660
|
+
value = Number(value);
|
661
|
+
}
|
662
|
+
|
663
|
+
// Set the value in our data structure
|
664
|
+
current[part] = value;
|
665
|
+
} else {
|
666
|
+
// We're building the nested structure
|
667
|
+
|
668
|
+
// Check if the next part is a number (array index)
|
669
|
+
if (/^\d+$/.test(parts[i + 1])) {
|
670
|
+
// Create array if it doesn't exist
|
671
|
+
current[part] = current[part] || [];
|
672
|
+
} else {
|
673
|
+
// Create object if it doesn't exist
|
674
|
+
current[part] = current[part] || {};
|
675
|
+
}
|
676
|
+
|
677
|
+
// Move deeper into the structure
|
678
|
+
current = current[part];
|
679
|
+
}
|
680
|
+
}
|
681
|
+
}
|
682
|
+
|
683
|
+
// Process unchecked checkboxes (they don't appear in FormData)
|
684
|
+
const allCheckboxes = e.target.querySelectorAll('input[type="checkbox"]');
|
685
|
+
allCheckboxes.forEach((checkbox) => {
|
686
|
+
if (checkbox.disabled) return; // Skip disabled checkboxes
|
687
|
+
|
688
|
+
const name = checkbox.name;
|
689
|
+
const parts = name.split(/[\[\].]/).filter(Boolean);
|
690
|
+
|
691
|
+
// Check if this checkbox's value is already in updatedData
|
692
|
+
// If not, it means it was unchecked
|
693
|
+
let current = updatedData;
|
694
|
+
let exists = true;
|
695
|
+
|
696
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
697
|
+
if (!current[parts[i]]) {
|
698
|
+
exists = false;
|
699
|
+
break;
|
700
|
+
}
|
701
|
+
current = current[parts[i]];
|
702
|
+
}
|
703
|
+
|
704
|
+
const lastPart = parts[parts.length - 1];
|
705
|
+
if (exists && !(lastPart in current)) {
|
706
|
+
current[lastPart] = false;
|
707
|
+
}
|
708
|
+
});
|
709
|
+
|
710
|
+
// Copy over the ID field to ensure it's included
|
711
|
+
if (isUpdate && data && data.id) {
|
712
|
+
updatedData.id = data.id;
|
713
|
+
}
|
714
|
+
|
715
|
+
try {
|
716
|
+
const response = await fetch(window.location.pathname, {
|
717
|
+
method: isUpdate ? "PUT" : "POST",
|
718
|
+
headers: {
|
719
|
+
"Content-Type": "application/json",
|
720
|
+
},
|
721
|
+
body: JSON.stringify(updatedData),
|
722
|
+
});
|
723
|
+
|
724
|
+
if (response.ok) {
|
725
|
+
const result = await response.json();
|
726
|
+
console.log("Success:", result);
|
727
|
+
showToast(
|
728
|
+
"Success",
|
729
|
+
`Your data has been ${
|
730
|
+
isUpdate ? "updated" : "submitted"
|
731
|
+
} successfully!`,
|
732
|
+
"success"
|
733
|
+
);
|
734
|
+
} else {
|
735
|
+
const errorText = await response.text();
|
736
|
+
console.error("Error:", response.status, response.statusText);
|
737
|
+
showToast("Error", `Error ${response.status}: ${errorText}`, "error");
|
738
|
+
}
|
739
|
+
} catch (error) {
|
740
|
+
console.error("Fetch error:", error);
|
741
|
+
showToast(
|
742
|
+
"Error",
|
743
|
+
"An unexpected error occurred. Please try again.",
|
744
|
+
"error"
|
745
|
+
);
|
746
|
+
}
|
747
|
+
});
|
748
|
+
|
749
|
+
// Toast function
|
750
|
+
function showToast(title, message, type) {
|
751
|
+
const toastContainer =
|
752
|
+
document.getElementById("toastContainer") || createToastContainer();
|
753
|
+
const toast = document.createElement("div");
|
754
|
+
toast.className = `toast ${
|
755
|
+
type === "success" ? "border-green-600" : "border-red-600"
|
756
|
+
} p-4 mb-4 rounded shadow-lg bg-gray-900 text-gray-100 border-l-4 p-4 rounded-lg shadow-md animate-fadeIn`;
|
757
|
+
toast.innerHTML = `
|
758
|
+
<strong class="block text-lg">${title}</strong>
|
759
|
+
<span class="block text-sm">${message}</span>
|
760
|
+
`;
|
761
|
+
toastContainer.appendChild(toast);
|
762
|
+
|
763
|
+
// Automatically remove the toast after 5 seconds
|
764
|
+
setTimeout(() => {
|
765
|
+
toast.remove();
|
766
|
+
}, 5000);
|
767
|
+
}
|
768
|
+
|
769
|
+
function createToastContainer() {
|
770
|
+
const container = document.createElement("div");
|
771
|
+
container.id = "toastContainer";
|
772
|
+
container.className = "fixed top-4 left-4 z-50 space-y-4";
|
773
|
+
document.body.appendChild(container);
|
774
|
+
return container;
|
775
|
+
}
|
776
|
+
|
777
|
+
document.getElementById("deleteButton").addEventListener("click", async () => {
|
778
|
+
if (!data.id) {
|
779
|
+
console.error("No ID found for deletion.");
|
780
|
+
showToast("Error", "No ID found for deletion.", "error");
|
781
|
+
return;
|
782
|
+
}
|
783
|
+
|
784
|
+
const confirmDelete = confirm("Are you sure you want to delete this record?");
|
785
|
+
if (!confirmDelete) return;
|
786
|
+
|
787
|
+
try {
|
788
|
+
const response = await fetch(window.location.pathname, {
|
789
|
+
method: "DELETE",
|
790
|
+
headers: {
|
791
|
+
"Content-Type": "application/json",
|
792
|
+
},
|
793
|
+
});
|
794
|
+
|
795
|
+
if (response.ok) {
|
796
|
+
console.log("Record deleted successfully.");
|
797
|
+
showToast("Success", "Record deleted successfully!", "success");
|
798
|
+
const currentUrl = window.location.pathname;
|
799
|
+
const urlParts = currentUrl.split("/").filter((part) => part !== "");
|
800
|
+
urlParts.pop();
|
801
|
+
const redirectUrl = "/" + urlParts.join("/") + "/";
|
802
|
+
|
803
|
+
console.log("Redirecting to:", redirectUrl);
|
804
|
+
|
805
|
+
setTimeout(() => {
|
806
|
+
window.location.href = redirectUrl;
|
807
|
+
}, 2000);
|
808
|
+
} else {
|
809
|
+
const errorText = await response.text();
|
810
|
+
console.error("Error:", response.status, response.statusText);
|
811
|
+
showToast("Error", `Error ${response.status}: ${errorText}`, "error");
|
812
|
+
}
|
813
|
+
} catch (error) {
|
814
|
+
console.error("Fetch error:", error);
|
815
|
+
showToast(
|
816
|
+
"Error",
|
817
|
+
"An unexpected error occurred. Please try again.",
|
818
|
+
"error"
|
819
|
+
);
|
820
|
+
}
|
821
|
+
});
|
822
|
+
|
823
|
+
// Form Update Real-Time Logger and Debugger
|
824
|
+
|
825
|
+
// Create a logging container
|
826
|
+
function createLogContainer() {
|
827
|
+
const existingContainer = document.getElementById("formDataLogger");
|
828
|
+
if (existingContainer) return existingContainer;
|
829
|
+
|
830
|
+
const logContainer = document.createElement("div");
|
831
|
+
logContainer.id = "formDataLogger";
|
832
|
+
logContainer.className =
|
833
|
+
"fixed bottom-4 right-4 w-96 max-h-96 bg-gray-800 border border-gray-600 rounded-lg shadow-lg p-3 overflow-y-auto z-50";
|
834
|
+
logContainer.style.maxHeight = "400px";
|
835
|
+
|
836
|
+
logContainer.innerHTML = `
|
837
|
+
<div class="flex justify-between items-center mb-2 pb-2 border-b border-gray-600">
|
838
|
+
<h3 class="text-sm font-bold text-white">Form Data Logger</h3>
|
839
|
+
<div class="flex gap-2">
|
840
|
+
<button id="captureFormState" class="text-xs bg-blue-600 px-2 py-1 rounded hover:bg-blue-700">Capture State</button>
|
841
|
+
<button id="clearLogBtn" class="text-xs bg-gray-700 px-2 py-1 rounded hover:bg-gray-600">Clear</button>
|
842
|
+
<button id="toggleLogBtn" class="text-xs bg-gray-700 px-2 py-1 rounded hover:bg-gray-600">Hide</button>
|
843
|
+
</div>
|
844
|
+
</div>
|
845
|
+
<div id="logEntries" class="space-y-2 text-xs"></div>
|
846
|
+
`;
|
847
|
+
|
848
|
+
document.body.appendChild(logContainer);
|
849
|
+
|
850
|
+
// Add button event listeners
|
851
|
+
document.getElementById("clearLogBtn").addEventListener("click", () => {
|
852
|
+
document.getElementById("logEntries").innerHTML = "";
|
853
|
+
});
|
854
|
+
|
855
|
+
document.getElementById("toggleLogBtn").addEventListener("click", (e) => {
|
856
|
+
const logEntries = document.getElementById("logEntries");
|
857
|
+
if (logEntries.style.display === "none") {
|
858
|
+
logEntries.style.display = "block";
|
859
|
+
e.target.textContent = "Hide";
|
860
|
+
} else {
|
861
|
+
logEntries.style.display = "none";
|
862
|
+
e.target.textContent = "Show";
|
863
|
+
}
|
864
|
+
});
|
865
|
+
|
866
|
+
document.getElementById("captureFormState").addEventListener("click", () => {
|
867
|
+
const currentState = captureFormState();
|
868
|
+
logMessage("Form State Snapshot", currentState, "snapshot");
|
869
|
+
console.log("Current Form State:", currentState);
|
870
|
+
});
|
871
|
+
|
872
|
+
return logContainer;
|
873
|
+
}
|
874
|
+
|
875
|
+
// Capture current form state (all fields)
|
876
|
+
function captureFormState() {
|
877
|
+
const form = document.getElementById(isUpdate ? "updateForm" : "createForm");
|
878
|
+
if (!form) return null;
|
879
|
+
|
880
|
+
const formState = {};
|
881
|
+
|
882
|
+
// Get all input elements
|
883
|
+
const inputs = form.querySelectorAll("input, select, textarea");
|
884
|
+
inputs.forEach((input) => {
|
885
|
+
if (!input.name) return;
|
886
|
+
|
887
|
+
// Skip disabled inputs if we want to capture only enabled fields
|
888
|
+
// if (input.disabled) return;
|
889
|
+
|
890
|
+
if (input.type === "checkbox") {
|
891
|
+
formState[input.name] = input.checked;
|
892
|
+
} else {
|
893
|
+
formState[input.name] = input.value;
|
894
|
+
}
|
895
|
+
});
|
896
|
+
|
897
|
+
// Process the form data into a nested structure
|
898
|
+
const structuredData = {};
|
899
|
+
|
900
|
+
Object.entries(formState).forEach(([key, value]) => {
|
901
|
+
// Parse the key to handle nested structures
|
902
|
+
const parts = key.split(/[\[\].]/).filter(Boolean);
|
903
|
+
|
904
|
+
// Start at the root of our data object
|
905
|
+
let current = structuredData;
|
906
|
+
|
907
|
+
// Build the nested structure
|
908
|
+
for (let i = 0; i < parts.length; i++) {
|
909
|
+
const part = parts[i];
|
910
|
+
|
911
|
+
if (i === parts.length - 1) {
|
912
|
+
// We're at the leaf node, set the actual value
|
913
|
+
|
914
|
+
// Handle special value conversions
|
915
|
+
if (typeof value === "string") {
|
916
|
+
if (value === "true" || value === "false") {
|
917
|
+
value = value === "true";
|
918
|
+
} else if (!isNaN(value) && value !== "") {
|
919
|
+
value = Number(value);
|
920
|
+
}
|
921
|
+
}
|
922
|
+
|
923
|
+
// Set the value in our data structure
|
924
|
+
current[part] = value;
|
925
|
+
} else {
|
926
|
+
// We're building the nested structure
|
927
|
+
|
928
|
+
// Check if the next part is a number (array index)
|
929
|
+
if (/^\d+$/.test(parts[i + 1])) {
|
930
|
+
// Create array if it doesn't exist
|
931
|
+
current[part] = current[part] || [];
|
932
|
+
} else {
|
933
|
+
// Create object if it doesn't exist
|
934
|
+
current[part] = current[part] || {};
|
935
|
+
}
|
936
|
+
|
937
|
+
// Move deeper into the structure
|
938
|
+
current = current[part];
|
939
|
+
}
|
940
|
+
}
|
941
|
+
});
|
942
|
+
|
943
|
+
return structuredData;
|
944
|
+
}
|
945
|
+
|
946
|
+
// Log a message to the log container
|
947
|
+
function logMessage(title, data, type = "info") {
|
948
|
+
const logEntries = document.getElementById("logEntries");
|
949
|
+
if (!logEntries) return;
|
950
|
+
|
951
|
+
const timestamp = new Date().toLocaleTimeString();
|
952
|
+
const logEntry = document.createElement("div");
|
953
|
+
|
954
|
+
// Different styling based on type
|
955
|
+
if (type === "error") {
|
956
|
+
logEntry.className = "p-2 rounded bg-red-900/50 border-l-4 border-red-600";
|
957
|
+
} else if (type === "warning") {
|
958
|
+
logEntry.className =
|
959
|
+
"p-2 rounded bg-yellow-900/50 border-l-4 border-yellow-600";
|
960
|
+
} else if (type === "success") {
|
961
|
+
logEntry.className =
|
962
|
+
"p-2 rounded bg-green-900/50 border-l-4 border-green-600";
|
963
|
+
} else if (type === "snapshot") {
|
964
|
+
logEntry.className =
|
965
|
+
"p-2 rounded bg-blue-900/50 border-l-4 border-blue-600";
|
966
|
+
} else {
|
967
|
+
logEntry.className = "p-2 rounded bg-gray-800 border border-gray-700";
|
968
|
+
}
|
969
|
+
|
970
|
+
// Format data object to string
|
971
|
+
let dataHtml = "";
|
972
|
+
if (data === null || data === undefined) {
|
973
|
+
dataHtml = '<span class="text-gray-400">null or undefined</span>';
|
974
|
+
} else if (typeof data === "object") {
|
975
|
+
try {
|
976
|
+
const jsonStr = JSON.stringify(data, null, 2);
|
977
|
+
if (jsonStr === "{}") {
|
978
|
+
dataHtml = '<span class="text-orange-400">Empty object {}</span>';
|
979
|
+
} else {
|
980
|
+
dataHtml = `<pre class="text-xs mt-1 p-2 bg-gray-900 rounded overflow-x-auto">${escapeHtml(
|
981
|
+
jsonStr
|
982
|
+
)}</pre>`;
|
983
|
+
}
|
984
|
+
} catch (e) {
|
985
|
+
dataHtml = `<span class="text-red-400">Error stringifying: ${e.message}</span>`;
|
986
|
+
}
|
987
|
+
} else {
|
988
|
+
dataHtml = `<span>${escapeHtml(String(data))}</span>`;
|
989
|
+
}
|
990
|
+
|
991
|
+
logEntry.innerHTML = `
|
992
|
+
<div class="flex justify-between">
|
993
|
+
<span class="font-medium text-white">${escapeHtml(title)}</span>
|
994
|
+
<span class="text-gray-400 text-xs">${timestamp}</span>
|
995
|
+
</div>
|
996
|
+
<div class="mt-1">${dataHtml}</div>
|
997
|
+
`;
|
998
|
+
|
999
|
+
logEntries.prepend(logEntry);
|
1000
|
+
|
1001
|
+
// Limit the number of log entries
|
1002
|
+
const maxEntries = 50;
|
1003
|
+
while (logEntries.children.length > maxEntries) {
|
1004
|
+
logEntries.removeChild(logEntries.lastChild);
|
1005
|
+
}
|
1006
|
+
}
|
1007
|
+
|
1008
|
+
// Escape HTML to prevent XSS
|
1009
|
+
function escapeHtml(str) {
|
1010
|
+
return String(str)
|
1011
|
+
.replace(/&/g, "&")
|
1012
|
+
.replace(/</g, "<")
|
1013
|
+
.replace(/>/g, ">")
|
1014
|
+
.replace(/"/g, """)
|
1015
|
+
.replace(/'/g, "'");
|
1016
|
+
}
|
1017
|
+
|
1018
|
+
// Monitor form element changes
|
1019
|
+
function attachFormMonitors() {
|
1020
|
+
const form = document.getElementById(isUpdate ? "updateForm" : "createForm");
|
1021
|
+
if (!form) {
|
1022
|
+
logMessage(
|
1023
|
+
"Form not found",
|
1024
|
+
`Could not find form with ID: ${isUpdate ? "updateForm" : "createForm"}`,
|
1025
|
+
"error"
|
1026
|
+
);
|
1027
|
+
return;
|
1028
|
+
}
|
1029
|
+
|
1030
|
+
// Monitor input changes
|
1031
|
+
form.addEventListener("input", (e) => {
|
1032
|
+
if (!e.target.name) return;
|
1033
|
+
|
1034
|
+
const fieldName = e.target.name;
|
1035
|
+
const newValue =
|
1036
|
+
e.target.type === "checkbox" ? e.target.checked : e.target.value;
|
1037
|
+
|
1038
|
+
logMessage(`Field Changed: ${fieldName}`, { value: newValue }, "info");
|
1039
|
+
});
|
1040
|
+
|
1041
|
+
// Monitor checkbox toggles
|
1042
|
+
form.querySelectorAll('input[type="checkbox"]').forEach((checkbox) => {
|
1043
|
+
if (checkbox.id && checkbox.id.includes("_toggle")) {
|
1044
|
+
checkbox.addEventListener("change", () => {
|
1045
|
+
const targetId = checkbox.id.replace("_toggle", "_content");
|
1046
|
+
logMessage(`Toggle Section: ${targetId}`, {
|
1047
|
+
checked: checkbox.checked,
|
1048
|
+
visible: checkbox.checked,
|
1049
|
+
});
|
1050
|
+
});
|
1051
|
+
}
|
1052
|
+
});
|
1053
|
+
|
1054
|
+
// Intercept form submission
|
1055
|
+
const originalSubmit = form.onsubmit;
|
1056
|
+
form.onsubmit = async function (e) {
|
1057
|
+
e.preventDefault();
|
1058
|
+
|
1059
|
+
logMessage(
|
1060
|
+
"Form Submission Started",
|
1061
|
+
{
|
1062
|
+
time: new Date().toISOString(),
|
1063
|
+
isUpdate: isUpdate || false,
|
1064
|
+
},
|
1065
|
+
"info"
|
1066
|
+
);
|
1067
|
+
|
1068
|
+
// Capture and log the complete form state
|
1069
|
+
const formState = captureFormState();
|
1070
|
+
logMessage(
|
1071
|
+
"Form Submission Data",
|
1072
|
+
formState,
|
1073
|
+
formState && Object.keys(formState).length ? "success" : "warning"
|
1074
|
+
);
|
1075
|
+
|
1076
|
+
// Collect FormData entries for debugging
|
1077
|
+
const formData = new FormData(form);
|
1078
|
+
const formDataEntries = {};
|
1079
|
+
for (let [key, value] of formData.entries()) {
|
1080
|
+
formDataEntries[key] = value;
|
1081
|
+
}
|
1082
|
+
|
1083
|
+
logMessage("Raw FormData Entries", formDataEntries);
|
1084
|
+
|
1085
|
+
// Check for empty submission
|
1086
|
+
if (!formState || Object.keys(formState).length === 0) {
|
1087
|
+
logMessage(
|
1088
|
+
"Empty Form Submission",
|
1089
|
+
"The form data is empty! Check for form field naming issues.",
|
1090
|
+
"error"
|
1091
|
+
);
|
1092
|
+
}
|
1093
|
+
|
1094
|
+
// Call the original submit handler if it exists
|
1095
|
+
if (originalSubmit) {
|
1096
|
+
return originalSubmit.call(this, e);
|
1097
|
+
}
|
1098
|
+
};
|
1099
|
+
|
1100
|
+
// Override the existing updateData function if it exists
|
1101
|
+
if (typeof window.updateData === "function") {
|
1102
|
+
const originalUpdateData = window.updateData;
|
1103
|
+
window.updateData = function (data) {
|
1104
|
+
logMessage("updateData Called", data, "info");
|
1105
|
+
return originalUpdateData.apply(this, arguments);
|
1106
|
+
};
|
1107
|
+
}
|
1108
|
+
|
1109
|
+
// Override the populateFormWithExistingData function
|
1110
|
+
if (typeof window.populateFormWithExistingData === "function") {
|
1111
|
+
const originalPopulate = window.populateFormWithExistingData;
|
1112
|
+
window.populateFormWithExistingData = function (data) {
|
1113
|
+
logMessage("Populating Form with Data", data, data ? "info" : "warning");
|
1114
|
+
|
1115
|
+
if (!data || Object.keys(data).length === 0) {
|
1116
|
+
logMessage(
|
1117
|
+
"Empty Data for Form Population",
|
1118
|
+
"This might explain missing form values",
|
1119
|
+
"warning"
|
1120
|
+
);
|
1121
|
+
}
|
1122
|
+
|
1123
|
+
// Call original function
|
1124
|
+
const result = originalPopulate.apply(this, arguments);
|
1125
|
+
|
1126
|
+
// Capture the form state after population
|
1127
|
+
setTimeout(() => {
|
1128
|
+
const formState = captureFormState();
|
1129
|
+
logMessage("Form State After Population", formState, "snapshot");
|
1130
|
+
|
1131
|
+
// Check if fields are disabled when they shouldn't be
|
1132
|
+
const disabledInputs = form.querySelectorAll(
|
1133
|
+
"input:disabled, select:disabled, textarea:disabled"
|
1134
|
+
);
|
1135
|
+
if (disabledInputs.length > 0) {
|
1136
|
+
const disabledFields = Array.from(disabledInputs)
|
1137
|
+
.map((el) => el.name)
|
1138
|
+
.filter(Boolean);
|
1139
|
+
if (disabledFields.length > 0) {
|
1140
|
+
logMessage("Found Disabled Fields", disabledFields, "warning");
|
1141
|
+
}
|
1142
|
+
}
|
1143
|
+
}, 100);
|
1144
|
+
|
1145
|
+
return result;
|
1146
|
+
};
|
1147
|
+
}
|
1148
|
+
|
1149
|
+
// Debug the fetch call
|
1150
|
+
const originalFetch = window.fetch;
|
1151
|
+
window.fetch = function (...args) {
|
1152
|
+
const [url, options] = args;
|
1153
|
+
|
1154
|
+
if (
|
1155
|
+
options &&
|
1156
|
+
options.method &&
|
1157
|
+
(options.method === "POST" || options.method === "PUT")
|
1158
|
+
) {
|
1159
|
+
logMessage(
|
1160
|
+
`Fetch Request (${options.method})`,
|
1161
|
+
{
|
1162
|
+
url: url,
|
1163
|
+
method: options.method,
|
1164
|
+
bodyData: options.body ? JSON.parse(options.body) : null,
|
1165
|
+
},
|
1166
|
+
"info"
|
1167
|
+
);
|
1168
|
+
}
|
1169
|
+
|
1170
|
+
const fetchPromise = originalFetch.apply(this, args);
|
1171
|
+
|
1172
|
+
fetchPromise
|
1173
|
+
.then((response) => {
|
1174
|
+
if (!response.ok) {
|
1175
|
+
logMessage(
|
1176
|
+
"Fetch Error",
|
1177
|
+
{
|
1178
|
+
status: response.status,
|
1179
|
+
statusText: response.statusText,
|
1180
|
+
},
|
1181
|
+
"error"
|
1182
|
+
);
|
1183
|
+
} else {
|
1184
|
+
logMessage(
|
1185
|
+
"Fetch Success",
|
1186
|
+
{
|
1187
|
+
status: response.status,
|
1188
|
+
},
|
1189
|
+
"success"
|
1190
|
+
);
|
1191
|
+
}
|
1192
|
+
})
|
1193
|
+
.catch((error) => {
|
1194
|
+
logMessage(
|
1195
|
+
"Fetch Failed",
|
1196
|
+
{
|
1197
|
+
error: error.message,
|
1198
|
+
},
|
1199
|
+
"error"
|
1200
|
+
);
|
1201
|
+
});
|
1202
|
+
|
1203
|
+
return fetchPromise;
|
1204
|
+
};
|
1205
|
+
}
|
1206
|
+
|
1207
|
+
// Initialize the logger
|
1208
|
+
function initFormLogger() {
|
1209
|
+
createLogContainer();
|
1210
|
+
attachFormMonitors();
|
1211
|
+
|
1212
|
+
// Log initial system state
|
1213
|
+
logMessage(
|
1214
|
+
"Form Logger Initialized",
|
1215
|
+
{
|
1216
|
+
url: window.location.href,
|
1217
|
+
isUpdate: typeof isUpdate !== "undefined" ? isUpdate : false,
|
1218
|
+
time: new Date().toISOString(),
|
1219
|
+
},
|
1220
|
+
"info"
|
1221
|
+
);
|
1222
|
+
|
1223
|
+
// Check if existingData is available
|
1224
|
+
if (typeof existingData !== "undefined") {
|
1225
|
+
logMessage(
|
1226
|
+
"Existing Data Found",
|
1227
|
+
existingData,
|
1228
|
+
existingData ? "info" : "warning"
|
1229
|
+
);
|
1230
|
+
} else {
|
1231
|
+
logMessage("No Existing Data", "existingData is not defined", "warning");
|
1232
|
+
}
|
1233
|
+
|
1234
|
+
// Log initial schema
|
1235
|
+
if (typeof schema !== "undefined") {
|
1236
|
+
logMessage("Schema Structure", {
|
1237
|
+
hasFields: schema.fields ? true : false,
|
1238
|
+
nestedTypes: schema.$ ? Object.keys(schema.$) : [],
|
1239
|
+
});
|
1240
|
+
}
|
1241
|
+
|
1242
|
+
// Check form action is correctly set
|
1243
|
+
setTimeout(() => {
|
1244
|
+
const form = document.getElementById(
|
1245
|
+
isUpdate ? "updateForm" : "createForm"
|
1246
|
+
);
|
1247
|
+
if (form) {
|
1248
|
+
logMessage("Form Configuration", {
|
1249
|
+
action: form.action || window.location.pathname,
|
1250
|
+
method: form.method || "POST/PUT via JS",
|
1251
|
+
id: form.id,
|
1252
|
+
containsInputs:
|
1253
|
+
form.querySelectorAll("input, select, textarea").length > 0,
|
1254
|
+
});
|
1255
|
+
}
|
1256
|
+
}, 500);
|
1257
|
+
}
|
1258
|
+
|
1259
|
+
// Run on page load
|
1260
|
+
document.addEventListener("DOMContentLoaded", function () {
|
1261
|
+
setTimeout(initFormLogger, 100); // Slight delay to ensure page is fully loaded
|
1262
|
+
});
|
1263
|
+
|
1264
|
+
// For immediate execution if the page is already loaded
|
1265
|
+
if (
|
1266
|
+
document.readyState === "complete" ||
|
1267
|
+
document.readyState === "interactive"
|
1268
|
+
) {
|
1269
|
+
setTimeout(initFormLogger, 100);
|
1270
|
+
}
|