Implement move extensions
| @@ -465,7 +465,7 @@ async function connectToApi(baseUrl) { | |||
| 465 | 465 | ||
| 466 | function updateStatus(success) { | 466 | function updateStatus(success) { |
| 467 | connectedToApi = success; | 467 | connectedToApi = success; |
| 468 | const _text = success ? 'Connected to API' : 'Could not connect to API'; | 468 | const _text = success ? t`Connected to API` : t`Could not connect to API`; |
| 469 | const _class = success ? 'success' : 'failure'; | 469 | const _class = success ? 'success' : 'failure'; |
| 470 | $('#extensions_status').text(_text); | 470 | $('#extensions_status').text(_text); |
| 471 | $('#extensions_status').attr('class', _class); | 471 | $('#extensions_status').attr('class', _class); |
| @@ -723,7 +723,7 @@ async function showExtensionsDetails() { | |||
| 723 | } | 723 | } |
| 724 | if (stateChanged) { | 724 | if (stateChanged) { |
| 725 | waitingForSave = true; | 725 | waitingForSave = true; |
| 726 | const toast = toastr.info('The page will be reloaded shortly...', 'Extensions state changed'); | 726 | const toast = toastr.info(t`The page will be reloaded shortly...`, t`Extensions state changed`); |
| 727 | await saveSettings(); | 727 | await saveSettings(); |
| 728 | toastr.clear(toast); | 728 | toastr.clear(toast); |
| 729 | waitingForSave = false; | 729 | waitingForSave = false; |
| @@ -735,7 +735,7 @@ async function showExtensionsDetails() { | |||
| 735 | popupPromise = popup.show(); | 735 | popupPromise = popup.show(); |
| 736 | checkForUpdatesManual(abortController.signal).finally(() => htmlLoading.remove()); | 736 | checkForUpdatesManual(abortController.signal).finally(() => htmlLoading.remove()); |
| 737 | } catch (error) { | 737 | } catch (error) { |
| 738 | toastr.error('Error loading extensions. See browser console for details.'); | 738 | toastr.error(t`Error loading extensions. See browser console for details.`); |
| 739 | console.error(error); | 739 | console.error(error); |
| 740 | } | 740 | } |
| 741 | if (popupPromise) { | 741 | if (popupPromise) { |
| @@ -817,7 +817,7 @@ async function onDeleteClick() { | |||
| 817 | } | 817 | } |
| 818 | 818 | ||
| 819 | // use callPopup to create a popup for the user to confirm before delete | 819 | // use callPopup to create a popup for the user to confirm before delete |
| 820 | const confirmation = await callGenericPopup(`Are you sure you want to delete ${extensionName}?`, POPUP_TYPE.CONFIRM, '', {}); | 820 | const confirmation = await callGenericPopup(t`Are you sure you want to delete ${extensionName}?`, POPUP_TYPE.CONFIRM, '', {}); |
| 821 | if (confirmation === POPUP_RESULT.AFFIRMATIVE) { | 821 | if (confirmation === POPUP_RESULT.AFFIRMATIVE) { |
| 822 | await deleteExtension(extensionName); | 822 | await deleteExtension(extensionName); |
| 823 | } | 823 | } |
| @@ -832,7 +832,57 @@ async function onMoveClick() { | |||
| 832 | return; | 832 | return; |
| 833 | } | 833 | } |
| 834 | 834 | ||
| 835 | toastr.info('Not implemented yet'); | 835 | const source = getExtensionType(extensionName); |
| 836 | const destination = source === 'global' ? 'local' : 'global'; | ||
| 837 | |||
| 838 | const confirmationHeader = t`Move extension`; | ||
| 839 | const confirmationText = source == 'global' | ||
| 840 | ? t`Are you sure you want to move ${extensionName} to your local extensions? This will make it available only for you.` | ||
| 841 | : t`Are you sure you want to move ${extensionName} to the global extensions? This will make it available for all users.`; | ||
| 842 | |||
| 843 | const confirmation = await Popup.show.confirm(confirmationHeader, confirmationText); | ||
| 844 | |||
| 845 | if (!confirmation) { | ||
| 846 | return; | ||
| 847 | } | ||
| 848 | |||
| 849 | $(this).find('i').addClass('fa-spin'); | ||
| 850 | await moveExtension(extensionName, source, destination); | ||
| 851 | } | ||
| 852 | |||
| 853 | /** | ||
| 854 | * Moves an extension via the API. | ||
| 855 | * @param {string} extensionName Extension name | ||
| 856 | * @param {string} source Source type | ||
| 857 | * @param {string} destination Destination type | ||
| 858 | * @returns {Promise<void>} | ||
| 859 | */ | ||
| 860 | async function moveExtension(extensionName, source, destination) { | ||
| 861 | try { | ||
| 862 | const result = await fetch('/api/extensions/move', { | ||
| 863 | method: 'POST', | ||
| 864 | headers: getRequestHeaders(), | ||
| 865 | body: JSON.stringify({ | ||
| 866 | extensionName, | ||
| 867 | source, | ||
| 868 | destination, | ||
| 869 | }), | ||
| 870 | }); | ||
| 871 | |||
| 872 | if (!result.ok) { | ||
| 873 | const text = await result.text(); | ||
| 874 | toastr.error(text || result.statusText, t`Extension move failed`, { timeOut: 5000 }); | ||
| 875 | console.error('Extension move failed', result.status, result.statusText, text); | ||
| 876 | return; | ||
| 877 | } | ||
| 878 | |||
| 879 | toastr.success(t`Extension ${extensionName} moved.`); | ||
| 880 | await loadExtensionSettings({}, false, false); | ||
| 881 | await Popup.util.popups.find(popup => popup.content.querySelector('.extensions_info'))?.completeCancelled(); | ||
| 882 | showExtensionsDetails(); | ||
| 883 | } catch (error) { | ||
| 884 | console.error('Error:', error); | ||
| 885 | } | ||
| 836 | } | 886 | } |
| 837 | 887 | ||
| 838 | /** | 888 | /** |
| @@ -853,7 +903,7 @@ export async function deleteExtension(extensionName) { | |||
| 853 | console.error('Error:', error); | 903 | console.error('Error:', error); |
| 854 | } | 904 | } |
| 855 | 905 | ||
| 856 | toastr.success(`Extension ${extensionName} deleted`); | 906 | toastr.success(t`Extension ${extensionName} deleted`); |
| 857 | showExtensionsDetails(); | 907 | showExtensionsDetails(); |
| 858 | // reload the page to remove the extension from the list | 908 | // reload the page to remove the extension from the list |
| 859 | location.reload(); | 909 | location.reload(); |
| @@ -896,7 +946,7 @@ async function getExtensionVersion(extensionName, abortSignal) { | |||
| 896 | export async function installExtension(url, global) { | 946 | export async function installExtension(url, global) { |
| 897 | console.debug('Extension installation started', url); | 947 | console.debug('Extension installation started', url); |
| 898 | 948 | ||
| 899 | toastr.info('Please wait...', 'Installing extension'); | 949 | toastr.info(t`Please wait...`, t`Installing extension`); |
| 900 | 950 | ||
| 901 | const request = await fetch('/api/extensions/install', { | 951 | const request = await fetch('/api/extensions/install', { |
| 902 | method: 'POST', | 952 | method: 'POST', |
| @@ -909,7 +959,7 @@ export async function installExtension(url, global) { | |||
| 909 | 959 | ||
| 910 | if (!request.ok) { | 960 | if (!request.ok) { |
| 911 | const text = await request.text(); | 961 | const text = await request.text(); |
| 912 | toastr.warning(text || request.statusText, 'Extension installation failed', { timeOut: 5000 }); | 962 | toastr.warning(text || request.statusText, t`Extension installation failed`, { timeOut: 5000 }); |
| 913 | console.error('Extension installation failed', request.status, request.statusText, text); | 963 | console.error('Extension installation failed', request.status, request.statusText, text); |
| 914 | return; | 964 | return; |
| 915 | } | 965 | } |
| @@ -80,6 +80,7 @@ router.post('/install', jsonParser, async (request, response) => { | |||
| 80 | const { url, global } = request.body; | 80 | const { url, global } = request.body; |
| 81 | 81 | ||
| 82 | if (global && !request.user.profile.admin) { | 82 | if (global && !request.user.profile.admin) { |
| 83 | console.warn(`User ${request.user.profile.handle} does not have permission to install global extensions.`); | ||
| 83 | return response.status(403).send('Forbidden: No permission to install global extensions.'); | 84 | return response.status(403).send('Forbidden: No permission to install global extensions.'); |
| 84 | } | 85 | } |
| 85 | 86 | ||
| @@ -123,6 +124,7 @@ router.post('/update', jsonParser, async (request, response) => { | |||
| 123 | const { extensionName, global } = request.body; | 124 | const { extensionName, global } = request.body; |
| 124 | 125 | ||
| 125 | if (global && !request.user.profile.admin) { | 126 | if (global && !request.user.profile.admin) { |
| 127 | console.warn(`User ${request.user.profile.handle} does not have permission to update global extensions.`); | ||
| 126 | return response.status(403).send('Forbidden: No permission to update global extensions.'); | 128 | return response.status(403).send('Forbidden: No permission to update global extensions.'); |
| 127 | } | 129 | } |
| 128 | 130 | ||
| @@ -153,6 +155,50 @@ router.post('/update', jsonParser, async (request, response) => { | |||
| 153 | } | 155 | } |
| 154 | }); | 156 | }); |
| 155 | 157 | ||
| 158 | router.post('/move', jsonParser, async (request, response) => { | ||
| 159 | try { | ||
| 160 | const { extensionName, source, destination } = request.body; | ||
| 161 | |||
| 162 | if (!extensionName || !source || !destination) { | ||
| 163 | return response.status(400).send('Bad Request. Not all required parameters are provided.'); | ||
| 164 | } | ||
| 165 | |||
| 166 | if (!request.user.profile.admin) { | ||
| 167 | console.warn(`User ${request.user.profile.handle} does not have permission to move extensions.`); | ||
| 168 | return response.status(403).send('Forbidden: No permission to move extensions.'); | ||
| 169 | } | ||
| 170 | |||
| 171 | const sourceDirectory = source === 'global' ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; | ||
| 172 | const destinationDirectory = destination === 'global' ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; | ||
| 173 | const sourcePath = path.join(sourceDirectory, sanitize(extensionName)); | ||
| 174 | const destinationPath = path.join(destinationDirectory, sanitize(extensionName)); | ||
| 175 | |||
| 176 | if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isDirectory()) { | ||
| 177 | console.error(`Source directory does not exist at ${sourcePath}`); | ||
| 178 | return response.status(404).send('Source directory does not exist.'); | ||
| 179 | } | ||
| 180 | |||
| 181 | if (fs.existsSync(destinationPath)) { | ||
| 182 | console.error(`Destination directory already exists at ${destinationPath}`); | ||
| 183 | return response.status(409).send('Destination directory already exists.'); | ||
| 184 | } | ||
| 185 | |||
| 186 | if (source === destination) { | ||
| 187 | console.error('Source and destination directories are the same'); | ||
| 188 | return response.status(409).send('Source and destination directories are the same.'); | ||
| 189 | } | ||
| 190 | |||
| 191 | fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true }); | ||
| 192 | fs.rmSync(sourcePath, { recursive: true, force: true }); | ||
| 193 | console.log(`Extension has been moved from ${sourcePath} to ${destinationPath}`); | ||
| 194 | |||
| 195 | return response.sendStatus(204); | ||
| 196 | } catch (error) { | ||
| 197 | console.log('Moving extension failed', error); | ||
| 198 | return response.status(500).send('Internal Server Error. Try again later.'); | ||
| 199 | } | ||
| 200 | }); | ||
| 201 | |||
| 156 | /** | 202 | /** |
| 157 | * HTTP POST handler function to get the current git commit hash and branch name for a given extension. | 203 | * HTTP POST handler function to get the current git commit hash and branch name for a given extension. |
| 158 | * It checks whether the repository is up-to-date with the remote, and returns the status along with | 204 | * It checks whether the repository is up-to-date with the remote, and returns the status along with |
| @@ -211,6 +257,7 @@ router.post('/delete', jsonParser, async (request, response) => { | |||
| 211 | const { extensionName, global } = request.body; | 257 | const { extensionName, global } = request.body; |
| 212 | 258 | ||
| 213 | if (global && !request.user.profile.admin) { | 259 | if (global && !request.user.profile.admin) { |
| 260 | console.warn(`User ${request.user.profile.handle} does not have permission to delete global extensions.`); | ||
| 214 | return response.status(403).send('Forbidden: No permission to delete global extensions.'); | 261 | return response.status(403).send('Forbidden: No permission to delete global extensions.'); |
| 215 | } | 262 | } |
| 216 | 263 | ||