add_build.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. const Features = (() => {
  2. let features = []; // Flat array of feature objects from API
  3. let features_by_id = {}; // Map feature IDs to feature objects
  4. let categories_grouped = {}; // Features grouped by category name
  5. let selected_options = 0;
  6. function resetDictionaries() {
  7. // clear old dictionaries
  8. features_by_id = {};
  9. categories_grouped = {};
  10. // Build lookup maps from flat feature array
  11. features.forEach((feature) => {
  12. features_by_id[feature.id] = feature;
  13. // Group by category
  14. const cat_name = feature.category.name;
  15. if (!categories_grouped[cat_name]) {
  16. categories_grouped[cat_name] = {
  17. name: cat_name,
  18. description: feature.category.description,
  19. features: []
  20. };
  21. }
  22. categories_grouped[cat_name].features.push(feature);
  23. });
  24. }
  25. function updateRequiredFor() {
  26. features.forEach((feature) => {
  27. if (feature.dependencies && feature.dependencies.length > 0) {
  28. feature.dependencies.forEach((dependency_id) => {
  29. let dep = getOptionById(dependency_id);
  30. if (dep && dep.requiredFor == undefined) {
  31. dep.requiredFor = [];
  32. }
  33. if (dep) {
  34. dep.requiredFor.push(feature.id);
  35. }
  36. });
  37. }
  38. });
  39. }
  40. function reset(new_features) {
  41. features = new_features;
  42. selected_options = 0;
  43. resetDictionaries();
  44. updateRequiredFor();
  45. }
  46. function getOptionById(id) {
  47. return features_by_id[id];
  48. }
  49. function getCategoryByName(category_name) {
  50. return categories_grouped[category_name];
  51. }
  52. function getAllCategories() {
  53. return Object.values(categories_grouped);
  54. }
  55. function getCategoryIdByName(category_name) {
  56. return 'category_'+category_name.split(" ").join("_");
  57. }
  58. function featureIsDisabledByDefault(feature_id) {
  59. let feature = getOptionById(feature_id);
  60. return feature && !feature.default.enabled;
  61. }
  62. function featureisEnabledByDefault(feature_id) {
  63. return !featureIsDisabledByDefault(feature_id);
  64. }
  65. function enableDependenciesForFeature(feature_id) {
  66. let feature = getOptionById(feature_id);
  67. if (!feature || !feature.dependencies || feature.dependencies.length === 0) {
  68. return;
  69. }
  70. feature.dependencies.forEach((dependency_id) => {
  71. const check = true;
  72. checkUncheckOptionById(dependency_id, check);
  73. });
  74. }
  75. function handleOptionStateChange(feature_id, triggered_by_ui) {
  76. // feature_id is the feature ID from the API
  77. let element = document.getElementById(feature_id);
  78. if (!element) return;
  79. let feature = getOptionById(feature_id);
  80. if (!feature) return;
  81. if (element.checked) {
  82. selected_options += 1;
  83. enableDependenciesForFeature(feature.id);
  84. } else {
  85. selected_options -= 1;
  86. if (triggered_by_ui) {
  87. askToDisableDependentsForFeature(feature.id);
  88. } else {
  89. disabledDependentsForFeature(feature.id);
  90. }
  91. }
  92. updateCategoryCheckboxState(feature.category.name);
  93. updateGlobalCheckboxState();
  94. }
  95. function askToDisableDependentsForFeature(feature_id) {
  96. let enabled_dependent_features = getEnabledDependentFeaturesFor(feature_id);
  97. if (enabled_dependent_features.length <= 0) {
  98. return;
  99. }
  100. let feature = getOptionById(feature_id);
  101. let feature_display_name = feature ? feature.name : feature_id;
  102. // Get display names for dependent features
  103. let dependent_names = enabled_dependent_features.map(dep_id => {
  104. let dep_feature = getOptionById(dep_id);
  105. return dep_feature ? dep_feature.name : dep_id;
  106. });
  107. document.getElementById('modalBody').innerHTML = "The feature(s) <strong>"+dependent_names.join(", ")+"</strong> is/are dependant on <strong>"+feature_display_name+"</strong>" +
  108. " and hence will be disabled too.<br><strong>Do you want to continue?</strong>";
  109. document.getElementById('modalDisableButton').onclick = () => { disabledDependentsForFeature(feature_id); };
  110. document.getElementById('modalCancelButton').onclick = document.getElementById('modalCloseButton').onclick = () => {
  111. const check = true;
  112. if (feature) {
  113. checkUncheckOptionById(feature.id, check);
  114. }
  115. };
  116. var confirmationModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('dependencyCheckModal'));
  117. confirmationModal.show();
  118. }
  119. function disabledDependentsForFeature(feature_id) {
  120. let feature = getOptionById(feature_id);
  121. if (!feature || feature.requiredFor == undefined) {
  122. return;
  123. }
  124. let dependents = feature.requiredFor;
  125. dependents.forEach((dependent_id) => {
  126. const check = false;
  127. checkUncheckOptionById(dependent_id, check);
  128. });
  129. }
  130. function updateCategoryCheckboxState(category_name) {
  131. let category = getCategoryByName(category_name);
  132. if (category == undefined) {
  133. console.log("Could not find category by given name");
  134. return;
  135. }
  136. let checked_options_count = 0;
  137. category.features.forEach((feature) => {
  138. // Use ID to find the element
  139. let element = document.getElementById(feature.id);
  140. if (element && element.checked) {
  141. checked_options_count += 1;
  142. }
  143. });
  144. let category_checkbox_element = document.getElementById(getCategoryIdByName(category_name));
  145. if (category_checkbox_element == undefined) {
  146. console.log("Could not find element for given category");
  147. return;
  148. }
  149. let indeterminate_state = false;
  150. switch(checked_options_count) {
  151. case 0:
  152. category_checkbox_element.checked = false;
  153. break;
  154. case category.features.length:
  155. category_checkbox_element.checked = true;
  156. break;
  157. default:
  158. indeterminate_state = true;
  159. break;
  160. }
  161. category_checkbox_element.indeterminate = indeterminate_state;
  162. }
  163. function updateGlobalCheckboxState() {
  164. const total_options = Object.keys(features_by_id).length;
  165. let global_checkbox = document.getElementById("check-uncheck-all");
  166. let indeterminate_state = false;
  167. switch (selected_options) {
  168. case 0:
  169. global_checkbox.checked = false;
  170. break
  171. case total_options:
  172. global_checkbox.checked = true;
  173. break;
  174. default:
  175. indeterminate_state = true;
  176. break;
  177. }
  178. global_checkbox.indeterminate = indeterminate_state;
  179. }
  180. function getEnabledDependentFeaturesHelper(feature_id, visited, dependent_features) {
  181. if (visited[feature_id] != undefined) {
  182. return;
  183. }
  184. let feature = getOptionById(feature_id);
  185. if (!feature) return;
  186. // Use ID to check the checkbox
  187. let element = document.getElementById(feature.id);
  188. if (!element || element.checked == false) {
  189. return;
  190. }
  191. visited[feature_id] = true;
  192. dependent_features.push(feature_id);
  193. if (feature.requiredFor == null) {
  194. return;
  195. }
  196. feature.requiredFor.forEach((dependent_feature_id) => {
  197. getEnabledDependentFeaturesHelper(dependent_feature_id, visited, dependent_features);
  198. });
  199. }
  200. function getEnabledDependentFeaturesFor(feature_id) {
  201. let dependent_features = [];
  202. let visited = {};
  203. let feature = getOptionById(feature_id);
  204. if (feature && feature.requiredFor) {
  205. feature.requiredFor.forEach((dependent_feature_id) => {
  206. getEnabledDependentFeaturesHelper(dependent_feature_id, visited, dependent_features);
  207. });
  208. }
  209. return dependent_features;
  210. }
  211. function applyDefaults() {
  212. features.forEach(feature => {
  213. const check = featureisEnabledByDefault(feature.id);
  214. checkUncheckOptionById(feature.id, check);
  215. });
  216. }
  217. function checkUncheckOptionById(id, check) {
  218. let feature = getOptionById(id);
  219. if (!feature) return;
  220. // Use ID to find the element
  221. let element = document.getElementById(feature.id);
  222. if (element == undefined || element.checked == check) {
  223. return;
  224. }
  225. element.checked = check;
  226. const triggered_by_ui = false;
  227. handleOptionStateChange(feature.id, triggered_by_ui);
  228. }
  229. function checkUncheckAll(check) {
  230. getAllCategories().forEach(category => {
  231. checkUncheckCategory(category.name, check);
  232. });
  233. }
  234. function checkUncheckCategory(category_name, check) {
  235. getCategoryByName(category_name).features.forEach(feature => {
  236. checkUncheckOptionById(feature.id, check);
  237. });
  238. }
  239. return {reset, handleOptionStateChange, getCategoryIdByName, applyDefaults, checkUncheckAll, checkUncheckCategory, getOptionById};
  240. })();
  241. var init_categories_expanded = false;
  242. function init() {
  243. fetchVehicles();
  244. }
  245. // enables or disables the elements with ids passed as an array
  246. // if enable is true, the elements are enabled and vice-versa
  247. function enableDisableElementsById(ids, enable) {
  248. for (let i=0; i<ids.length; i++) {
  249. let element = document.getElementById(ids[i]);
  250. if (element) {
  251. element.disabled = (!enable);
  252. }
  253. }
  254. }
  255. // sets a spinner inside the division with given id
  256. // also sets a custom message inside the division
  257. // this indicates that an ajax call related to that element is in progress
  258. function setSpinnerToDiv(id, message) {
  259. let element = document.getElementById(id);
  260. if (element) {
  261. element.innerHTML = '<div class="container-fluid d-flex align-content-between">' +
  262. '<strong>'+message+'</strong>' +
  263. '<div class="spinner-border ms-auto" role="status" aria-hidden="true"></div>' +
  264. '</div>';
  265. }
  266. }
  267. function fetchVehicles() {
  268. // following elemets will be blocked (disabled) when we make the request
  269. let elements_to_block = ['vehicle', 'version', 'board', 'submit', 'reset_def', 'exp_col_button'];
  270. enableDisableElementsById(elements_to_block, false);
  271. let request_url = '/api/v1/vehicles';
  272. setSpinnerToDiv('vehicle_list', 'Fetching vehicles...');
  273. sendAjaxRequestForJsonResponse(request_url)
  274. .then((json_response) => {
  275. let all_vehicles = json_response;
  276. let new_vehicle = all_vehicles.find(vehicle => vehicle.name === "Copter") ? "copter": all_vehicles[0].id;
  277. updateVehicles(all_vehicles, new_vehicle);
  278. })
  279. .catch((message) => {
  280. console.log("Vehicle update failed. "+message);
  281. })
  282. .finally(() => {
  283. enableDisableElementsById(elements_to_block, true);
  284. });
  285. }
  286. function updateVehicles(all_vehicles, new_vehicle_id) {
  287. let vehicle_element = document.getElementById('vehicle');
  288. let old_vehicle_id = vehicle_element ? vehicle_element.value : '';
  289. fillVehicles(all_vehicles, new_vehicle_id);
  290. if (old_vehicle_id != new_vehicle_id) {
  291. onVehicleChange(new_vehicle_id);
  292. }
  293. }
  294. function onVehicleChange(new_vehicle_id) {
  295. // following elemets will be blocked (disabled) when we make the request
  296. let elements_to_block = ['vehicle', 'version', 'board', 'submit', 'reset_def', 'exp_col_button'];
  297. enableDisableElementsById(elements_to_block, false);
  298. let request_url = '/api/v1/vehicles/'+new_vehicle_id+'/versions';
  299. setSpinnerToDiv('version_list', 'Fetching versions...');
  300. sendAjaxRequestForJsonResponse(request_url)
  301. .then((json_response) => {
  302. let all_versions = json_response;
  303. all_versions = sortVersions(all_versions);
  304. const new_version = all_versions[0].id;
  305. updateVersions(all_versions, new_version);
  306. })
  307. .catch((message) => {
  308. console.log("Version update failed. "+message);
  309. })
  310. .finally(() => {
  311. enableDisableElementsById(elements_to_block, true);
  312. });
  313. }
  314. function updateVersions(all_versions, new_version) {
  315. let version_element = document.getElementById('version');
  316. let old_version = version_element ? version_element.value : '';
  317. fillVersions(all_versions, new_version);
  318. if (old_version != new_version) {
  319. onVersionChange(new_version);
  320. }
  321. }
  322. function onVersionChange(new_version) {
  323. // following elemets will be blocked (disabled) when we make the request
  324. let elements_to_block = ['vehicle', 'version', 'board', 'submit', 'reset_def', 'exp_col_button'];
  325. enableDisableElementsById(elements_to_block, false);
  326. let vehicle_id = document.getElementById("vehicle").value;
  327. let version_id = new_version;
  328. // Fetch boards first
  329. let boards_url = `/api/v1/vehicles/${vehicle_id}/versions/${version_id}/boards`;
  330. setSpinnerToDiv('board_list', 'Fetching boards...');
  331. // Clear build options and show loading state
  332. let temp_container = document.createElement('div');
  333. temp_container.id = "temp_container";
  334. temp_container.setAttribute('class', 'container-fluid w-25 mt-3');
  335. let features_list_element = document.getElementById('build_options');
  336. features_list_element.innerHTML = "";
  337. features_list_element.appendChild(temp_container);
  338. setSpinnerToDiv('temp_container', 'Fetching features...');
  339. // Fetch boards
  340. sendAjaxRequestForJsonResponse(boards_url)
  341. .then((boards_response) => {
  342. // Keep full board objects with id and name
  343. let boards = boards_response;
  344. let new_board = boards.length > 0 ? boards[0].id : null;
  345. updateBoards(boards, new_board);
  346. })
  347. .catch((message) => {
  348. console.log("Boards update failed. "+message);
  349. })
  350. .finally(() => {
  351. enableDisableElementsById(elements_to_block, true);
  352. });
  353. }
  354. function updateBoards(all_boards, new_board) {
  355. let board_element = document.getElementById('board');
  356. let old_board = board_element ? board_element.value : '';
  357. fillBoards(all_boards, new_board);
  358. if (old_board != new_board) {
  359. onBoardChange(new_board);
  360. }
  361. }
  362. function onBoardChange(new_board) {
  363. // When board changes, fetch features for the new board
  364. let vehicle_id = document.getElementById('vehicle').value;
  365. let version_id = document.getElementById('version').value;
  366. let temp_container = document.createElement('div');
  367. temp_container.id = "temp_container";
  368. temp_container.setAttribute('class', 'container-fluid w-25 mt-3');
  369. let features_list_element = document.getElementById('build_options');
  370. features_list_element.innerHTML = "";
  371. features_list_element.appendChild(temp_container);
  372. setSpinnerToDiv('temp_container', 'Fetching features...');
  373. let features_url = `/api/v1/vehicles/${vehicle_id}/versions/${version_id}/boards/${new_board}/features`;
  374. sendAjaxRequestForJsonResponse(features_url)
  375. .then((features_response) => {
  376. Features.reset(features_response);
  377. fillBuildOptions(features_response);
  378. Features.applyDefaults();
  379. })
  380. .catch((message) => {
  381. console.log("Features update failed. "+message);
  382. });
  383. }
  384. function fillBoards(boards, default_board_id) {
  385. let output = document.getElementById('board_list');
  386. output.innerHTML = '<label for="board" class="form-label"><strong>Select Board</strong></label>' +
  387. '<select name="board" id="board" class="form-select" aria-label="Select Board" onchange="onBoardChange(this.value);"></select>';
  388. let boardList = document.getElementById("board")
  389. boards.forEach(board => {
  390. let opt = document.createElement('option');
  391. opt.value = board.id;
  392. opt.innerHTML = board.name;
  393. opt.selected = (board.id === default_board_id);
  394. boardList.appendChild(opt);
  395. });
  396. }
  397. var toggle_all_categories = (() => {
  398. let all_categories_expanded = init_categories_expanded;
  399. function toggle_method() {
  400. // toggle global state
  401. all_categories_expanded = !all_categories_expanded;
  402. let all_collapse_elements = document.getElementsByClassName('feature-group');
  403. for (let i=0; i<all_collapse_elements.length; i+=1) {
  404. let collapse_element = all_collapse_elements[i];
  405. collapse_instance = bootstrap.Collapse.getOrCreateInstance(collapse_element);
  406. if (all_categories_expanded && !collapse_element.classList.contains('show')) {
  407. collapse_instance.show();
  408. } else if (!all_categories_expanded && collapse_element.classList.contains('show')) {
  409. collapse_instance.hide();
  410. }
  411. }
  412. }
  413. return toggle_method;
  414. })();
  415. function createCategoryCard(category_name, features_in_category, expanded) {
  416. options_html = "";
  417. features_in_category.forEach(feature => {
  418. options_html += '<div class="form-check">' +
  419. '<input class="form-check-input feature-checkbox" type="checkbox" value="1" name="'+feature.id+'" id="'+feature.id+'" onclick="Features.handleOptionStateChange(this.id, true);">' +
  420. '<label class="form-check-label ms-2" for="'+feature.id+'">' +
  421. (feature.description || feature.name) +
  422. '</label>' +
  423. '</div>';
  424. });
  425. let id_prefix = Features.getCategoryIdByName(category_name);
  426. let card_element = document.createElement('div');
  427. card_element.setAttribute('class', 'card ' + (expanded == true ? 'h-100' : ''));
  428. card_element.id = id_prefix + '_card';
  429. card_element.innerHTML = '<div class="card-header ps-3">' +
  430. '<div class="d-flex justify-content-between">' +
  431. '<div class="d-inline-flex">' +
  432. '<span class="align-middle me-3"><input class="form-check-input" type="checkbox" id="'+Features.getCategoryIdByName(category_name)+'" onclick="Features.checkUncheckCategory(\''+category_name+'\', this.checked);"></span>' +
  433. '<strong>' +
  434. '<label for="check-uncheck-category">' + category_name + '</label>' +
  435. '</strong>' +
  436. '</div>' +
  437. '<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#'+id_prefix+'_collapse" aria-expanded="false" aria-controls="'+id_prefix+'_collapse">' +
  438. '<i class="bi bi-chevron-'+(expanded == true ? 'up' : 'down')+'" id="'+id_prefix+'_icon'+'"></i>' +
  439. '</button>' +
  440. '</div>' +
  441. '</div>';
  442. let collapse_element = document.createElement('div');
  443. collapse_element.setAttribute('class', 'feature-group collapse '+(expanded == true ? 'show' : ''));
  444. collapse_element.id = id_prefix + '_collapse';
  445. collapse_element.innerHTML = '<div class="container-fluid px-3 py-2">'+options_html+'</div>';
  446. card_element.appendChild(collapse_element);
  447. // add relevent event listeners
  448. collapse_element.addEventListener('hide.bs.collapse', () => {
  449. card_element.classList.remove('h-100');
  450. document.getElementById(id_prefix+'_icon').setAttribute('class', 'bi bi-chevron-down');
  451. });
  452. collapse_element.addEventListener('shown.bs.collapse', () => {
  453. card_element.classList.add('h-100');
  454. document.getElementById(id_prefix+'_icon').setAttribute('class', 'bi bi-chevron-up');
  455. });
  456. return card_element;
  457. }
  458. function fillBuildOptions(features) {
  459. let output = document.getElementById('build_options');
  460. output.innerHTML = `<div class="d-flex mb-3 justify-content-between">
  461. <div class="d-flex d-flex align-items-center">
  462. <p class="card-text"><strong>Available features for the current selection are:</strong></p>
  463. </div>
  464. <button type="button" class="btn btn-outline-primary" id="exp_col_button" onclick="toggle_all_categories();"><i class="bi bi-chevron-expand me-2"></i>Expand/Collapse all categories</button>
  465. </div>`;
  466. // Group features by category
  467. let categories_map = {};
  468. features.forEach(feature => {
  469. const cat_name = feature.category.name;
  470. if (!categories_map[cat_name]) {
  471. categories_map[cat_name] = [];
  472. }
  473. categories_map[cat_name].push(feature);
  474. });
  475. // Convert to array and display
  476. let categories = Object.entries(categories_map).map(([name, feats]) => ({name, features: feats}));
  477. categories.forEach((category, cat_idx) => {
  478. if (cat_idx % 4 == 0) {
  479. let new_row = document.createElement('div');
  480. new_row.setAttribute('class', 'row');
  481. new_row.id = 'category_'+parseInt(cat_idx/4)+'_row';
  482. output.appendChild(new_row);
  483. }
  484. let col_element = document.createElement('div');
  485. col_element.setAttribute('class', 'col-md-3 col-sm-6 mb-2');
  486. col_element.appendChild(createCategoryCard(category.name, category.features, init_categories_expanded));
  487. document.getElementById('category_'+parseInt(cat_idx/4)+'_row').appendChild(col_element);
  488. });
  489. }
  490. // returns a Promise
  491. // the promise is resolved when we recieve status code 200 from the AJAX request
  492. // the JSON response for the request is returned in such case
  493. // the promise is rejected when the status code is not 200
  494. // the status code is returned in such case
  495. function sendAjaxRequestForJsonResponse(url) {
  496. return new Promise((resolve, reject) => {
  497. var xhr = new XMLHttpRequest();
  498. xhr.open('GET', url);
  499. // disable cache, thanks to: https://stackoverflow.com/questions/22356025/force-cache-control-no-cache-in-chrome-via-xmlhttprequest-on-f5-reload
  500. xhr.setRequestHeader("Cache-Control", "no-cache, no-store, max-age=0");
  501. xhr.setRequestHeader("Expires", "Tue, 01 Jan 1980 1:00:00 GMT");
  502. xhr.setRequestHeader("Pragma", "no-cache");
  503. xhr.onload = function () {
  504. if (xhr.status == 200) {
  505. resolve(JSON.parse(xhr.response));
  506. } else {
  507. reject("Got response:"+xhr.response+" (Status Code: "+xhr.status+")");
  508. }
  509. }
  510. xhr.send();
  511. });
  512. }
  513. function fillVehicles(vehicles, vehicle_id_to_select) {
  514. var output = document.getElementById('vehicle_list');
  515. output.innerHTML = '<label for="vehicle" class="form-label"><strong>Select Vehicle</strong></label>' +
  516. '<select name="vehicle" id="vehicle" class="form-select" aria-label="Select Vehicle" onchange="onVehicleChange(this.value);"></select>';
  517. vehicleList = document.getElementById("vehicle");
  518. vehicles.forEach(vehicle => {
  519. opt = document.createElement('option');
  520. opt.value = vehicle.id;
  521. opt.innerHTML = vehicle.name;
  522. opt.selected = (vehicle.id === vehicle_id_to_select);
  523. vehicleList.appendChild(opt);
  524. });
  525. }
  526. function compareVersionNums(a, b) {
  527. const versionRegex = /(\d+)\.(\d+)\.(\d+)/;
  528. const [, aMajor, aMinor, aPatch] = a.match(versionRegex).map(Number);
  529. const [, bMajor, bMinor, bPatch] = b.match(versionRegex).map(Number);
  530. if (aMajor !== bMajor) return bMajor - aMajor;
  531. if (aMinor !== bMinor) return bMinor - aMinor;
  532. return bPatch - aPatch;
  533. }
  534. function sortVersions(versions) {
  535. const order = {
  536. "beta" : 0,
  537. "latest": 1,
  538. "stable": 2,
  539. "tag" : 3,
  540. }
  541. versions.sort((a, b) => {
  542. // sort the version types in order mentioned above
  543. if (a.type != b.type) {
  544. return order[a.type] - order[b.type];
  545. }
  546. // for numbered versions, do reverse sorting to make sure recent versions come first
  547. if (a.type == "stable" || b.type == "beta") {
  548. return compareVersionNums(a.name.split(" ")[1], b.name.split(" ")[1]);
  549. }
  550. return a.name.localeCompare(b.name);
  551. });
  552. // Push the first stable version in the list to the top
  553. const firstStableIndex = versions.findIndex(v => v.name.split(" ")[0].toLowerCase() === "stable");
  554. if (firstStableIndex !== -1) {
  555. const stableVersion = versions.splice(firstStableIndex, 1)[0];
  556. versions.unshift(stableVersion);
  557. }
  558. return versions;
  559. }
  560. function fillVersions(versions, version_to_select) {
  561. var output = document.getElementById('version_list');
  562. output.innerHTML = '<label for="version" class="form-label"><strong>Select Version</strong></label>' +
  563. '<select name="version" id="version" class="form-select" aria-label="Select Version" onchange="onVersionChange(this.value);"></select>';
  564. versionList = document.getElementById("version");
  565. versions.forEach(version => {
  566. opt = document.createElement('option');
  567. opt.value = version.id;
  568. opt.innerHTML = version.name;
  569. opt.selected = (version.id === version_to_select);
  570. versionList.appendChild(opt);
  571. });
  572. }
  573. // Handle form submission
  574. async function handleFormSubmit(event) {
  575. event.preventDefault();
  576. const submitButton = document.getElementById('submit');
  577. const originalButtonText = submitButton.innerHTML;
  578. try {
  579. // Disable submit button and show loading state
  580. submitButton.disabled = true;
  581. submitButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Submitting...';
  582. // Collect form data
  583. const vehicle_id = document.getElementById('vehicle').value;
  584. const version_id = document.getElementById('version').value;
  585. const board_id = document.getElementById('board').value;
  586. // Collect selected features - checkboxes now have feature IDs directly
  587. const selected_features = [];
  588. const checkboxes = document.querySelectorAll('.feature-checkbox:checked');
  589. checkboxes.forEach(checkbox => {
  590. // The checkbox ID is already the feature define (ID)
  591. selected_features.push(checkbox.id);
  592. });
  593. // Create build request payload
  594. const buildRequest = {
  595. vehicle_id: vehicle_id,
  596. version_id: version_id,
  597. board_id: board_id,
  598. selected_features: selected_features
  599. };
  600. // Send POST request to API
  601. const response = await fetch('/api/v1/builds', {
  602. method: 'POST',
  603. headers: {
  604. 'Content-Type': 'application/json',
  605. },
  606. body: JSON.stringify(buildRequest)
  607. });
  608. if (!response.ok) {
  609. const errorData = await response.json();
  610. throw new Error(errorData.detail || 'Failed to submit build');
  611. }
  612. const result = await response.json();
  613. // Redirect to viewlog page
  614. window.location.href = `/?build_id=${result.build_id}`;
  615. } catch (error) {
  616. console.error('Error submitting build:', error);
  617. alert('Failed to submit build: ' + error.message);
  618. // Re-enable submit button
  619. submitButton.disabled = false;
  620. submitButton.innerHTML = originalButtonText;
  621. }
  622. }
  623. // Initialize form submission handler
  624. document.addEventListener('DOMContentLoaded', () => {
  625. const buildForm = document.getElementById('build-form');
  626. if (buildForm) {
  627. buildForm.addEventListener('submit', handleFormSubmit);
  628. }
  629. });