Skip to content

Commit bf88495

Browse files
Paul-SchillingPaul Schilling
authored andcommitted
JSON Configurator: Added zip import functionality.
1 parent e2b5cc6 commit bf88495

File tree

3 files changed

+239
-12
lines changed

3 files changed

+239
-12
lines changed

docs/assets/css/v2.css

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,24 @@ input[type=number] {
1313
.v2-tooltip {
1414
--bs-tooltip-bg: var(--bs-secondary);
1515
}
16+
17+
/* Button container for fixed buttons */
18+
/* Add this to your Google Sites custom CSS */
19+
#fixed-buttons button:hover {
20+
opacity: 0.9;
21+
}
22+
23+
#fixed-buttons {
24+
pointer-events: auto !important;
25+
}
26+
27+
.btn {
28+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
29+
font-size: 14px;
30+
font-weight: 500;
31+
}
32+
33+
/* Add top margin to main content to prevent buttons from overlapping */
34+
.container-md {
35+
margin-top: 60px; /* Provides space for the fixed buttons */
36+
}

docs/assets/js/v2.js

Lines changed: 205 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,19 @@ function jsonify(name) {
6363
}
6464
}
6565
if (subStruct == null) {
66-
data[struct][val.name.substring(val.name.lastIndexOf('_') + 1)] =
67-
isNumeric(val.value) ?
68-
parseFloat(val.value) :
69-
(val.value === "" ? null : val.value);
66+
const field = val.name.substring(val.name.lastIndexOf('_') + 1);
67+
const $input = $(`#${name}-form [name="${val.name}"]`);
68+
const isCanBusField = val.name.toLowerCase().includes('canbus');
69+
data[struct][field] = isCanBusField && $input.attr('data-is-null') === 'true' ?
70+
null :
71+
(isNumeric(val.value) ? parseFloat(val.value) : (val.value === "" ? null : val.value));
7072
} else {
71-
data[struct][subStruct][val.name.substring(
72-
val.name.lastIndexOf('_') + 1)] =
73-
isNumeric(val.value) ?
74-
parseFloat(val.value) :
75-
(val.value === "" ? null : val.value);
73+
const field = val.name.substring(val.name.lastIndexOf('_') + 1);
74+
const $input = $(`#${name}-form [name="${val.name}"]`);
75+
const isCanBusField = val.name.toLowerCase().includes('canbus');
76+
data[struct][subStruct][field] = isCanBusField && $input.attr('data-is-null') === 'true' ?
77+
null :
78+
(isNumeric(val.value) ? parseFloat(val.value) : (val.value === "" ? null : val.value));
7679
}
7780
} else {
7881
data[val.name] = isNumeric(val.value) ?
@@ -154,6 +157,196 @@ function zipDownload() {
154157
console.log("Downloaded YAGSL Config zip");
155158
}
156159

160+
function addZipImporter() {
161+
// If import button already exists, don't recreate; just ensure it's placed correctly
162+
let existingImport = document.getElementById('import-zip-button');
163+
let fileInput = document.getElementById('zip-file-input');
164+
165+
// Create hidden file input if missing
166+
if (!fileInput) {
167+
fileInput = document.createElement('input');
168+
fileInput.type = 'file';
169+
fileInput.accept = '.zip,application/zip';
170+
fileInput.id = 'zip-file-input';
171+
fileInput.style.display = 'none';
172+
document.body.appendChild(fileInput);
173+
}
174+
// Attach change listener once
175+
if (!fileInput.__yagsl_listener_attached) {
176+
fileInput.addEventListener('change', (e) => {
177+
const f = e.target.files && e.target.files[0];
178+
if (f) {
179+
importZipFile(f);
180+
}
181+
fileInput.value = '';
182+
});
183+
fileInput.__yagsl_listener_attached = true;
184+
}
185+
186+
// Helper to create a button only when needed
187+
function makeButton(id, className, text, inlineStyle) {
188+
const btn = document.createElement('button');
189+
btn.id = id;
190+
btn.type = 'button';
191+
btn.className = className;
192+
btn.textContent = text;
193+
if (inlineStyle) btn.style.cssText = inlineStyle;
194+
return btn;
195+
}
196+
197+
if (!existingImport) {
198+
existingImport = makeButton('import-zip-button', 'btn btn-primary btn-lg mt-2 mb-3', 'Import ZIP', null);
199+
existingImport.addEventListener('click', () => fileInput.click());
200+
}
201+
202+
// No test button: removed per user request
203+
204+
// Place buttons next to existing download button if present
205+
const downloadContainer = document.getElementById('download-button');
206+
if (downloadContainer) {
207+
// Force layout with !important to ensure consistency
208+
downloadContainer.style.cssText = `
209+
display: flex !important;
210+
justify-content: center !important;
211+
align-items: center !important;
212+
gap: 10px !important;
213+
flex-wrap: nowrap !important;
214+
margin-bottom: 1rem !important;
215+
`;
216+
217+
// Avoid re-appending if already inside
218+
if (!downloadContainer.contains(existingImport)) downloadContainer.appendChild(existingImport);
219+
if (!downloadContainer.contains(fileInput)) downloadContainer.appendChild(fileInput);
220+
221+
// Find and move the Run Import Tests button if it exists (use the known id)
222+
const runImportButton = document.getElementById('run-import-tests');
223+
if (runImportButton && !downloadContainer.contains(runImportButton)) {
224+
downloadContainer.appendChild(runImportButton);
225+
}
226+
return;
227+
}
228+
229+
// Fallback: create fixed container at top-right (used for embedded mode)
230+
let fixedButtons = document.getElementById('fixed-buttons');
231+
if (!fixedButtons) {
232+
fixedButtons = document.createElement('div');
233+
fixedButtons.id = 'fixed-buttons';
234+
fixedButtons.style.cssText = `
235+
position: fixed;
236+
top: 20px;
237+
right: 20px;
238+
z-index: 1000;
239+
display: flex;
240+
gap: 10px;
241+
flex-direction: column;
242+
`;
243+
document.body.appendChild(fixedButtons);
244+
}
245+
246+
if (!fixedButtons.contains(existingImport)) fixedButtons.appendChild(existingImport);
247+
}
248+
249+
// Modify the initialization to ensure DOM is loaded
250+
function initializeUI() {
251+
if (document.readyState === 'loading') {
252+
document.addEventListener('DOMContentLoaded', setupButtons);
253+
} else {
254+
setupButtons();
255+
}
256+
}
257+
258+
function setupButtons() {
259+
updateAll();
260+
setInterval(updateAll, 500);
261+
}
262+
263+
function importZipFile(file) {
264+
const jszip = new JSZip();
265+
jszip.loadAsync(file).then((zip) => {
266+
// mapping of expected zip paths to form names
267+
const mapping = {
268+
'swerve/controllerproperties.json': 'controllerproperties',
269+
'swerve/swervedrive.json': 'swervedrive',
270+
'swerve/modules/physicalproperties.json': 'physicalproperties',
271+
'swerve/modules/frontleft.json': 'frontleft',
272+
'swerve/modules/frontright.json': 'frontright',
273+
'swerve/modules/backleft.json': 'backleft',
274+
'swerve/modules/backright.json': 'backright',
275+
'swerve/modules/pidfproperties.json': 'pidfproperties'
276+
};
277+
278+
// try each expected file; when present, load and populate
279+
Object.keys(mapping).forEach((path) => {
280+
const entry = zip.file(path);
281+
if (entry) {
282+
entry.async('string').then((content) => {
283+
try {
284+
const obj = JSON.parse(content);
285+
const name = mapping[path];
286+
// update JSON textarea display
287+
$(`#${name}-json`).text(JSON.stringify(obj, null, 2));
288+
// populate form inputs where possible
289+
populateFormFromObject(name, obj);
290+
updateAll();
291+
} catch (e) {
292+
console.error('Failed to parse JSON from', path, e);
293+
}
294+
});
295+
}
296+
});
297+
298+
}).catch((err) => {
299+
console.error('Failed to read ZIP', err);
300+
alert('Failed to read ZIP file. See console for details.');
301+
});
302+
}
303+
304+
// Populate a form identified by `name` from a parsed JSON object.
305+
// Handles up to two levels of nesting to match the serialize naming convention
306+
// used elsewhere in the app (struct_subStruct_field).
307+
function populateFormFromObject(name, obj) {
308+
if (!obj || typeof obj !== 'object') return;
309+
const $form = $(`#${name}-form`);
310+
if ($form.length === 0) return;
311+
312+
function setInputValue($input, value) {
313+
if (!$input.length) return;
314+
if ($input.attr('type') === 'checkbox') {
315+
$input.prop('checked', !!value);
316+
} else {
317+
// Always remove data-is-null first
318+
$input.removeAttr('data-is-null');
319+
320+
// Special handling for CAN bus fields
321+
const isCanBusField = $input.attr('name').toLowerCase().includes('canbus');
322+
if (isCanBusField && value === null) {
323+
// For null CAN bus values, set empty value but mark as null
324+
$input.val('');
325+
$input.attr('data-is-null', 'true');
326+
} else {
327+
// For all other fields (including non-null CAN bus)
328+
const finalValue = (value === null || value === undefined || value === '') ? '' : value;
329+
$input.val(finalValue);
330+
}
331+
}
332+
}
333+
334+
function processNestedObject(baseKey, nestedObj) {
335+
Object.entries(nestedObj).forEach(([key, value]) => {
336+
const fullKey = baseKey ? `${baseKey}_${key}` : key;
337+
if (value !== null && typeof value === 'object') {
338+
// Recursively process nested objects
339+
processNestedObject(fullKey, value);
340+
} else {
341+
const $input = $form.find(`[name="${fullKey}"]`);
342+
setInputValue($input, value);
343+
}
344+
});
345+
}
346+
347+
processNestedObject('', obj);
348+
}
349+
157350
$(function () {
158351
const tooltipTriggerList = document.querySelectorAll(
159352
'[data-bs-toggle="tooltip"]'); // Initialize tooltips: https://getbootstrap.com/docs/5.3/components/tooltips/#enable-tooltips
@@ -165,6 +358,7 @@ $(function () {
165358
return false;
166359
});
167360

168-
updateAll();
169-
setInterval(updateAll, 500);
361+
// Single initialization point for all UI elements
362+
addZipImporter();
363+
setupButtons();
170364
});

docs/index.html

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1283,11 +1283,14 @@ <h1 class="display-6">Module Tuning</h1>
12831283
</div>
12841284
</div>
12851285
</div>
1286-
<div id="download-button">
1286+
<div id="download-button" class="d-flex align-items-center justify-content-center gap-2">
12871287
<button class="btn btn-primary btn-lg mt-2 mb-3" id="download" onclick="zipDownload()"
12881288
type="button">Download
12891289
</button>
12901290
</div>
1291+
<div class="text-muted text-center small mt-2">
1292+
Build: November 3, 2025
1293+
</div>
12911294
</div>
12921295
<script src="https://unpkg.com/@popperjs/core@2"></script>
12931296
<script crossorigin="anonymous" src="https://kit.fontawesome.com/1d72776854.js"></script>
@@ -1300,6 +1303,15 @@ <h1 class="display-6">Module Tuning</h1>
13001303
<script
13011304
src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.6.0/jszip.min.js"></script>
13021305
<script src="assets/js/v2.js"></script>
1306+
<script>
1307+
// Check if the import-test parameter is explicitly set to true
1308+
const urlParams = new URLSearchParams(window.location.search);
1309+
if (urlParams.get('import-test') === 'true') {
1310+
const script = document.createElement('script');
1311+
script.src = 'assets/js/import-test.js';
1312+
document.body.appendChild(script);
1313+
}
1314+
</script>
13031315

13041316
</body>
13051317

0 commit comments

Comments
 (0)