@@ -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} ) ;
0 commit comments