Add branch selection on extension installer Closes #3865
| @@ -1120,7 +1120,7 @@ export function initRossMods() { | |||
| 1120 | const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', { | 1120 | const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', { |
| 1121 | customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }], | 1121 | customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }], |
| 1122 | onClose: (popup) => { | 1122 | onClose: (popup) => { |
| 1123 | regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false; | 1123 | regenerateWithCtrlEnter = Boolean(popup.inputResults.get('regenerateWithCtrlEnter') ?? false); |
| 1124 | }, | 1124 | }, |
| 1125 | }); | 1125 | }); |
| 1126 | if (!result) { | 1126 | if (!result) { |
| @@ -1061,7 +1061,7 @@ async function getExtensionVersion(extensionName, abortSignal) { | |||
| 1061 | * @param {boolean} global Is the extension global? | 1061 | * @param {boolean} global Is the extension global? |
| 1062 | * @returns {Promise<void>} | 1062 | * @returns {Promise<void>} |
| 1063 | */ | 1063 | */ |
| 1064 | export async function installExtension(url, global) { | 1064 | export async function installExtension(url, global, branch = '') { |
| 1065 | console.debug('Extension installation started', url); | 1065 | console.debug('Extension installation started', url); |
| 1066 | 1066 | ||
| 1067 | toastr.info(t`Please wait...`, t`Installing extension`); | 1067 | toastr.info(t`Please wait...`, t`Installing extension`); |
| @@ -1072,6 +1072,7 @@ export async function installExtension(url, global) { | |||
| 1072 | body: JSON.stringify({ | 1072 | body: JSON.stringify({ |
| 1073 | url, | 1073 | url, |
| 1074 | global, | 1074 | global, |
| 1075 | branch, | ||
| 1075 | }), | 1076 | }), |
| 1076 | }); | 1077 | }); |
| 1077 | 1078 | ||
| @@ -1406,9 +1407,17 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') { | |||
| 1406 | await popup.complete(POPUP_RESULT.AFFIRMATIVE); | 1407 | await popup.complete(POPUP_RESULT.AFFIRMATIVE); |
| 1407 | }, | 1408 | }, |
| 1408 | }; | 1409 | }; |
| 1410 | /** @type {import('./popup.js').CustomPopupInput} */ | ||
| 1411 | const branchNameInput = { | ||
| 1412 | id: 'extension_branch_name', | ||
| 1413 | label: t`Branch name (optional)`, | ||
| 1414 | type: 'text', | ||
| 1415 | tooltip: 'e.g. main, master, dev', | ||
| 1416 | }; | ||
| 1409 | 1417 | ||
| 1410 | const customButtons = isCurrentUserAdmin ? [installForAllButton] : []; | 1418 | const customButtons = isCurrentUserAdmin ? [installForAllButton] : []; |
| 1411 | const popup = new Popup(html, POPUP_TYPE.INPUT, suggestUrl ?? '', { okButton, customButtons }); | 1419 | const customInputs = [branchNameInput]; |
| 1420 | const popup = new Popup(html, POPUP_TYPE.INPUT, suggestUrl ?? '', { okButton, customButtons, customInputs }); | ||
| 1412 | const input = await popup.show(); | 1421 | const input = await popup.show(); |
| 1413 | 1422 | ||
| 1414 | if (!input) { | 1423 | if (!input) { |
| @@ -1417,7 +1426,8 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') { | |||
| 1417 | } | 1426 | } |
| 1418 | 1427 | ||
| 1419 | const url = String(input).trim(); | 1428 | const url = String(input).trim(); |
| 1420 | await installExtension(url, global); | 1429 | const branchName = String(popup.inputResults.get('extension_branch_name') ?? '').trim(); |
| 1430 | await installExtension(url, global, branchName); | ||
| 1421 | } | 1431 | } |
| 1422 | 1432 | ||
| 1423 | export async function initExtensions() { | 1433 | export async function initExtensions() { |
| @@ -291,7 +291,7 @@ async function installAsset(url, assetType, filename) { | |||
| 291 | try { | 291 | try { |
| 292 | if (category === 'extension') { | 292 | if (category === 'extension') { |
| 293 | console.debug(DEBUG_PREFIX, 'Installing extension ', url); | 293 | console.debug(DEBUG_PREFIX, 'Installing extension ', url); |
| 294 | await installExtension(url); | 294 | await installExtension(url, false); |
| 295 | console.debug(DEBUG_PREFIX, 'Extension installed.'); | 295 | console.debug(DEBUG_PREFIX, 'Extension installed.'); |
| 296 | return; | 296 | return; |
| 297 | } | 297 | } |
| @@ -309,7 +309,7 @@ async function installAsset(url, assetType, filename) { | |||
| 309 | console.debug(DEBUG_PREFIX, 'Importing character ', filename); | 309 | console.debug(DEBUG_PREFIX, 'Importing character ', filename); |
| 310 | const blob = await result.blob(); | 310 | const blob = await result.blob(); |
| 311 | const file = new File([blob], filename, { type: blob.type }); | 311 | const file = new File([blob], filename, { type: blob.type }); |
| 312 | await processDroppedFiles([file], true); | 312 | await processDroppedFiles([file]); |
| 313 | console.debug(DEBUG_PREFIX, 'Character downloaded.'); | 313 | console.debug(DEBUG_PREFIX, 'Character downloaded.'); |
| 314 | } | 314 | } |
| 315 | } | 315 | } |
| @@ -71,7 +71,8 @@ export const POPUP_RESULT = { | |||
| 71 | * @property {string} id - The id for the html element | 71 | * @property {string} id - The id for the html element |
| 72 | * @property {string} label - The label text for the input | 72 | * @property {string} label - The label text for the input |
| 73 | * @property {string?} [tooltip=null] - Optional tooltip icon displayed behind the label | 73 | * @property {string?} [tooltip=null] - Optional tooltip icon displayed behind the label |
| 74 | * @property {boolean?} [defaultState=false] - The default state when opening the popup (false if not set) | 74 | * @property {boolean|string|undefined} [defaultState=false] - The default state when opening the popup (false if not set) |
| 75 | * @property {string?} [type='checkbox'] - The type of the input (default is checkbox) | ||
| 75 | */ | 76 | */ |
| 76 | 77 | ||
| 77 | /** | 78 | /** |
| @@ -157,7 +158,7 @@ export class Popup { | |||
| 157 | 158 | ||
| 158 | /** @type {POPUP_RESULT|number} */ result; | 159 | /** @type {POPUP_RESULT|number} */ result; |
| 159 | /** @type {any} */ value; | 160 | /** @type {any} */ value; |
| 160 | /** @type {Map<string,boolean>?} */ inputResults; | 161 | /** @type {Map<string,string|boolean>?} */ inputResults; |
| 161 | /** @type {any} */ cropData; | 162 | /** @type {any} */ cropData; |
| 162 | 163 | ||
| 163 | /** @type {HTMLElement} */ lastFocus; | 164 | /** @type {HTMLElement} */ lastFocus; |
| @@ -260,13 +261,14 @@ export class Popup { | |||
| 260 | return; | 261 | return; |
| 261 | } | 262 | } |
| 262 | 263 | ||
| 264 | if (!input.type || input.type === 'checkbox') { | ||
| 263 | const label = document.createElement('label'); | 265 | const label = document.createElement('label'); |
| 264 | label.classList.add('checkbox_label', 'justifyCenter'); | 266 | label.classList.add('checkbox_label', 'justifyCenter'); |
| 265 | label.setAttribute('for', input.id); | 267 | label.setAttribute('for', input.id); |
| 266 | const inputElement = document.createElement('input'); | 268 | const inputElement = document.createElement('input'); |
| 267 | inputElement.type = 'checkbox'; | 269 | inputElement.type = 'checkbox'; |
| 268 | inputElement.id = input.id; | 270 | inputElement.id = input.id; |
| 269 | inputElement.checked = input.defaultState ?? false; | 271 | inputElement.checked = Boolean(input.defaultState ?? false); |
| 270 | label.appendChild(inputElement); | 272 | label.appendChild(inputElement); |
| 271 | const labelText = document.createElement('span'); | 273 | const labelText = document.createElement('span'); |
| 272 | labelText.innerText = input.label; | 274 | labelText.innerText = input.label; |
| @@ -282,6 +284,29 @@ export class Popup { | |||
| 282 | } | 284 | } |
| 283 | 285 | ||
| 284 | this.inputControls.appendChild(label); | 286 | this.inputControls.appendChild(label); |
| 287 | } | ||
| 288 | |||
| 289 | if (input.type === 'text') { | ||
| 290 | const label = document.createElement('label'); | ||
| 291 | label.classList.add('text_label', 'justifyCenter'); | ||
| 292 | label.setAttribute('for', input.id); | ||
| 293 | |||
| 294 | const inputElement = document.createElement('input'); | ||
| 295 | inputElement.classList.add('text_pole'); | ||
| 296 | inputElement.type = 'text'; | ||
| 297 | inputElement.id = input.id; | ||
| 298 | inputElement.value = String(input.defaultState ?? ''); | ||
| 299 | inputElement.placeholder = input.tooltip ?? ''; | ||
| 300 | |||
| 301 | const labelText = document.createElement('span'); | ||
| 302 | labelText.innerText = input.label; | ||
| 303 | labelText.dataset.i18n = input.label; | ||
| 304 | |||
| 305 | label.appendChild(labelText); | ||
| 306 | label.appendChild(inputElement); | ||
| 307 | |||
| 308 | this.inputControls.appendChild(label); | ||
| 309 | } | ||
| 285 | }); | 310 | }); |
| 286 | 311 | ||
| 287 | // Set the default button class | 312 | // Set the default button class |
| @@ -529,7 +554,8 @@ export class Popup { | |||
| 529 | this.inputResults = new Map(this.customInputs.map(input => { | 554 | this.inputResults = new Map(this.customInputs.map(input => { |
| 530 | /** @type {HTMLInputElement} */ | 555 | /** @type {HTMLInputElement} */ |
| 531 | const inputControl = this.dlg.querySelector(`#${input.id}`); | 556 | const inputControl = this.dlg.querySelector(`#${input.id}`); |
| 532 | return [inputControl.id, inputControl.checked]; | 557 | const value = input.type === 'text' ? inputControl.value : inputControl.checked; |
| 558 | return [inputControl.id, value]; | ||
| 533 | })); | 559 | })); |
| 534 | } | 560 | } |
| 535 | 561 | ||
| @@ -619,7 +645,7 @@ export class Popup { | |||
| 619 | /** @readonly @type {Popup[]} Remember all popups */ | 645 | /** @readonly @type {Popup[]} Remember all popups */ |
| 620 | popups: [], | 646 | popups: [], |
| 621 | 647 | ||
| 622 | /** @type {{value: any, result: POPUP_RESULT|number?, inputResults: Map<string, boolean>?}?} Last popup result */ | 648 | /** @type {{value: any, result: POPUP_RESULT|number?, inputResults: Map<string, string|boolean>?}?} Last popup result */ |
| 623 | lastResult: null, | 649 | lastResult: null, |
| 624 | 650 | ||
| 625 | /** @returns {boolean} Checks if any modal popup dialog is open */ | 651 | /** @returns {boolean} Checks if any modal popup dialog is open */ |
| @@ -76,7 +76,7 @@ router.post('/install', async (request, response) => { | |||
| 76 | fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions); | 76 | fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions); |
| 77 | } | 77 | } |
| 78 | 78 | ||
| 79 | const { url, global } = request.body; | 79 | const { url, global, branch } = request.body; |
| 80 | 80 | ||
| 81 | if (global && !request.user.profile.admin) { | 81 | if (global && !request.user.profile.admin) { |
| 82 | console.error(`User ${request.user.profile.handle} does not have permission to install global extensions.`); | 82 | console.error(`User ${request.user.profile.handle} does not have permission to install global extensions.`); |
| @@ -90,8 +90,12 @@ router.post('/install', async (request, response) => { | |||
| 90 | return response.status(409).send(`Directory already exists at ${extensionPath}`); | 90 | return response.status(409).send(`Directory already exists at ${extensionPath}`); |
| 91 | } | 91 | } |
| 92 | 92 | ||
| 93 | await git.clone(url, extensionPath, { '--depth': 1 }); | 93 | const cloneOptions = { '--depth': 1 }; |
| 94 | console.info(`Extension has been cloned at ${extensionPath}`); | 94 | if (branch) { |
| 95 | cloneOptions['--branch'] = branch; | ||
| 96 | } | ||
| 97 | await git.clone(url, extensionPath, cloneOptions); | ||
| 98 | console.info(`Extension has been cloned to ${extensionPath} from ${url} at ${branch || '(default)'} branch`); | ||
| 95 | 99 | ||
| 96 | const { version, author, display_name } = await getManifest(extensionPath); | 100 | const { version, author, display_name } = await getManifest(extensionPath); |
| 97 | 101 | ||