diff --git a/package.json b/package.json index 4895684f9..abb41ea3b 100644 --- a/package.json +++ b/package.json @@ -1224,13 +1224,6 @@ "category": "IBM i", "icon": "$(files)" }, - { - "command": "code-for-ibmi.deleteMember", - "enablement": "code-for-ibmi:connected", - "title": "Delete...", - "category": "IBM i", - "icon": "$(symbol-file)" - }, { "command": "code-for-ibmi.updateMemberText", "enablement": "code-for-ibmi:connected", @@ -1435,7 +1428,7 @@ "icon": "$(filter)" }, { - "command": "code-for-ibmi.deleteFilter", + "command": "code-for-ibmi.objectBrowser.delete", "enablement": "code-for-ibmi:connected", "title": "Delete...", "category": "IBM i", @@ -1523,13 +1516,6 @@ "category": "IBM i", "icon": "$(symbol-file)" }, - { - "command": "code-for-ibmi.deleteObject", - "enablement": "code-for-ibmi:connected", - "title": "Delete...", - "category": "IBM i", - "icon": "$(symbol-file)" - }, { "command": "code-for-ibmi.renameObject", "enablement": "code-for-ibmi:connected", @@ -1655,6 +1641,21 @@ "key": "ctrl+r", "mac": "cmd+r", "when": "editorLangId == cl" + }, + { + "command": "code-for-ibmi.deleteConnection", + "key": "delete", + "when": "focusedView === connectionBrowser && listHasSelectionOrFocus" + }, + { + "command": "code-for-ibmi.deleteIFS", + "key": "delete", + "when": "focusedView === ifsBrowser && listHasSelectionOrFocus" + }, + { + "command": "code-for-ibmi.objectBrowser.delete", + "key": "delete", + "when": "focusedView === objectBrowser && listHasSelectionOrFocus" } ], "viewsContainers": { @@ -1909,10 +1910,6 @@ "command": "code-for-ibmi.moveFilterToBottom", "when": "never" }, - { - "command": "code-for-ibmi.deleteFilter", - "when": "never" - }, { "command": "code-for-ibmi.createMember", "when": "never" @@ -1921,10 +1918,6 @@ "command": "code-for-ibmi.copyMember", "when": "never" }, - { - "command": "code-for-ibmi.deleteMember", - "when": "never" - }, { "command": "code-for-ibmi.updateMemberText", "when": "never" @@ -1953,10 +1946,6 @@ "command": "code-for-ibmi.copyObject", "when": "never" }, - { - "command": "code-for-ibmi.deleteObject", - "when": "never" - }, { "command": "code-for-ibmi.renameObject", "when": "never" @@ -2170,7 +2159,7 @@ "view/item/context": [ { "command": "code-for-ibmi.renameConnection", - "when": "view == connectionBrowser && viewItem == server", + "when": "view == connectionBrowser && !listMultiSelection && viewItem == server", "group": "2_delete@1" }, { @@ -2180,22 +2169,22 @@ }, { "command": "code-for-ibmi.showAdditionalSettings", - "when": "view == connectionBrowser && viewItem == server", + "when": "view == connectionBrowser && !listMultiSelection && viewItem == server", "group": "1_manage@1" }, { "command": "code-for-ibmi.showLoginSettings", - "when": "view == connectionBrowser && viewItem == server", + "when": "view == connectionBrowser && !listMultiSelection && viewItem == server", "group": "1_manage@2" }, { "command": "code-for-ibmi.connectTo", - "when": "view == connectionBrowser && viewItem == server", + "when": "view == connectionBrowser && !listMultiSelection && viewItem == server", "group": "inline" }, { "command": "code-for-ibmi.connectToAndReload", - "when": "view == connectionBrowser && viewItem == server", + "when": "view == connectionBrowser && !listMultiSelection && viewItem == server", "group": "3_connect@1" }, { @@ -2274,7 +2263,7 @@ "group": "4_filters@2" }, { - "command": "code-for-ibmi.deleteFilter", + "command": "code-for-ibmi.objectBrowser.delete", "when": "view == objectBrowser && viewItem =~ /^filter.*$/", "group": "4_filters@3" }, @@ -2318,10 +2307,15 @@ "when": "view == objectBrowser && viewItem =~ /^SPF.*$/", "group": "4_sourceFileStuff@1" }, + { + "command": "code-for-ibmi.objectBrowser.delete", + "when": "view == objectBrowser && viewItem == SPF", + "group": "5_sourceFileStuff@1" + }, { "submenu": "code-for-ibmi.sortMembers", "when": "view == objectBrowser && (viewItem =~ /^SPF/ || viewItem =~ /^member/)", - "group": "5_sourceFileStuff@1" + "group": "6_sourceFileStuff@1" }, { "submenu": "code-for-ibmi.openMember", @@ -2349,7 +2343,7 @@ "group": "2_memberStuff@2" }, { - "command": "code-for-ibmi.deleteMember", + "command": "code-for-ibmi.objectBrowser.delete", "when": "view == objectBrowser && viewItem == member", "group": "2_memberStuff@4" }, @@ -2519,18 +2513,18 @@ "group": "1_objActions@2" }, { - "command": "code-for-ibmi.deleteObject", - "when": "view == objectBrowser && (viewItem =~ /^object/ || viewItem == SPF)", + "command": "code-for-ibmi.objectBrowser.delete", + "when": "view == objectBrowser && viewItem =~ /^object/", "group": "1_objActions@5" }, { "command": "code-for-ibmi.renameObject", - "when": "view == objectBrowser && (viewItem =~ /^object/ || viewItem == SPF)", + "when": "view == objectBrowser && viewItem =~ /^object/", "group": "1_objActions@3" }, { "command": "code-for-ibmi.moveObject", - "when": "view == objectBrowser && (viewItem =~ /^object(?!.lib)/ || viewItem == SPF)", + "when": "view == objectBrowser && viewItem =~ /^object(?!.lib)/", "group": "1_objActions@4" }, { @@ -2602,4 +2596,4 @@ "HalcyonTechLtd.vscode-ibmi-walkthroughs", "vscode.git" ] -} +} \ No newline at end of file diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index 9ab60aa2d..ec25e9ed4 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -506,7 +506,7 @@ export default class IBMiContent { let createOBJLIST; if (sourceFilesOnly) { //DSPFD only - createOBJLIST =`select PHFILE as NAME, ` + + createOBJLIST = `select PHFILE as NAME, ` + `'*FILE' as TYPE, ` + `PHFILA as ATTRIBUTE, ` + `PHTXT as TEXT, ` + diff --git a/src/extension.ts b/src/extension.ts index 9e8045d83..b857eaa9d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -21,12 +21,13 @@ import { updateLocale } from "./locale"; import * as Sandbox from "./sandbox"; import { initialise } from "./testing"; import { CodeForIBMi, ConnectionData } from "./typings"; -import { ObjectBrowserProvider } from "./views/ConnectionBrowser"; +import { initializeConnectionBrowser } from "./views/ConnectionBrowser"; import { LibraryListProvider } from "./views/LibraryListView"; import { ProfilesView } from "./views/ProfilesView"; import { HelpView } from "./views/helpView"; import { initializeIFSBrowser } from "./views/ifsBrowser"; import { initializeObjectBrowser } from "./views/objectBrowser"; +import { SettingsUI } from "./webviews/settings"; export async function activate(context: ExtensionContext): Promise { // Use the console to output diagnostic information (console.log) and errors (console.error) @@ -41,15 +42,13 @@ export async function activate(context: ExtensionContext): Promise GlobalStorage.get().setLastConnections(lastConnections); commands.executeCommand(`setContext`, `code-for-ibmi:hasPreviousConnection`, lastConnections.length > 0); }; - + + SettingsUI.init(context); + initializeConnectionBrowser(context); initializeObjectBrowser(context) initializeIFSBrowser(context); - context.subscriptions.push( - window.registerTreeDataProvider( - `connectionBrowser`, - new ObjectBrowserProvider(context) - ), + context.subscriptions.push( window.registerTreeDataProvider( `helpView`, new HelpView() diff --git a/src/locale/ids/da.ts b/src/locale/ids/da.ts index bb142dd93..3267f5d8e 100644 --- a/src/locale/ids/da.ts +++ b/src/locale/ids/da.ts @@ -58,6 +58,7 @@ export const da: Locale = { 'connectionBrowser.renameConnection.noConnParmsFound': `Ingen parametre til forbindelsen '{0}' blev fundet`, 'connectionBrowser.renameConnection.error': `Fejl ved omdøbning af forbindelsen '{0}'! {1}`, 'connectionBrowser.deleteConnection.warning': `Er du sikker på at du vil slette forbindelsen '{0}'?`, + 'connectionBrowser.deleteConnection.multiple.warning': `Er du sikker på at du vil slette disse {0} forbindelser?`, 'connectionBrowser.ServerItem.tooltip': ` (forrige forbindelse)`, 'connectionBrowser.ServerItem.title': `Forbind`, // helpView: @@ -96,7 +97,7 @@ export const da: Locale = { 'ifsBrowser.deleteIFS.multi.warningMessage': `Er du sikker på at du vil slette de {0} valgte filer?`, 'ifsBrowser.deleteIFS.deletionPrompt': `Når du har slettet mappen, kan den ikke retableres.\nVenligst tast \"{0}\" for at bekræfte sletning.`, 'ifsBrowser.deleteIFS.deletionPrompt2': ` (Tryk \'Escape\' for at afbryde)`, - 'ifsBrowser.deleteIFS.infoMessage': `Slettede {0}.`, + 'ifsBrowser.deleteIFS.progress': `Sletning af {0} element(er)...`, 'ifsBrowser.deleteIFS.errorMessage': `Fejl ved sletning af streamfile! {0}`, 'ifsBrowser.deleteIFS.cancelled': `Sletning afbrudt.`, 'ifsBrowser.deleteIFS.default.home.dir':'{0} var den aktuelle mappe; det er nu {1}.', @@ -143,7 +144,9 @@ export const da: Locale = { 'LibraryListView.cleanupLibraryList.removedLibs': `De følgende biblioteker blev fjernet fra den opdaterede biblioteksliste, da de ikke er gyldige: {0}`, 'LibraryListView.cleanupLibraryList.validated': `Bibliotekslisten blev valideret uden fejl.`, // objectBrowser: - 'objectBrowser.deleteFilter.infoMessage': `Slet filter '{0}'?`, + 'objectBrowser.delete.confirm':'Er du sikker på at du vil slette {0}?', + 'objectBrowser.delete.multiple.confirm':'Er du sikker på at du vil slette de {0} valgte elementer?', + 'objectBrowser.delete.progress':'Sletter', 'objectBrowser.createMember.prompt': `Navn på nyt member (member.ext)`, 'objectBrowser.createMember.progressTitle': `Opretter member {0}...`, 'objectBrowser.createMember.errorMessage': `Fejl ved oprettelse af member {0}: {1}`, @@ -153,8 +156,6 @@ export const da: Locale = { 'objectBrowser.copyMember.overwrite': `Vil du overskrive member {0}?`, 'objectBrowser.copyMember.errorMessage2': `Member {0} eksisterer allerede!`, 'objectBrowser.copyMember.errorMessage3': `Fejl ved oprettelse af member {0}: {1}`, - 'objectBrowser.deleteMember.warningMessage': `Er du sikker på at du vil slette member {0}?`, - 'objectBrowser.deleteMember.infoMessage': `Slettede member {0}.`, 'objectBrowser.deleteMember.errorMessage': `Fejl ved sletning af member! {0}`, 'objectBrowser.updateMemberText.prompt': `Opdater beskrivelse af member {0}, *BLANK for ingen beskrivelse`, 'objectBrowser.updateMemberText.errorMessage': `Fejl ved ændring af member beskrivelse! {0}`, @@ -194,10 +195,7 @@ export const da: Locale = { 'objectBrowser.copyObject.infoMessage': `Kopierede objekt {0} {1} til {2}.`, 'objectBrowser.copyObject.infoMessage2': `Kopierede objekt {0} {1} til {2}. Opfrisk object browser.`, 'objectBrowser.copyObject.errorMessage4': `Fejl ved kopiering af objekt {0}! {1}`, - 'objectBrowser.deleteObject.warningMessage': `Er du sikker på at du vil slette {0} {1}?`, - 'objectBrowser.deleteObject.infoMessage': `Slettede {0} {1}.`, 'objectBrowser.deleteObject.errorMessage': `Fejl ved sletning af objekt! {0}`, - 'objectBrowser.deleteObject.progress':'Sletter objekt {0} {1}...', 'objectBrowser.renameObject.prompt': `Omdøb objekt`, 'objectBrowser.renameObject.errorMessage': `Objektnavn må være 10 tegn eller mindre.`, 'objectBrowser.renameObject.infoMessage': `Omdøbte objekt {0} {1} til {2}.`, diff --git a/src/locale/ids/en.ts b/src/locale/ids/en.ts index 2a1ee60fb..86ef8f0cd 100644 --- a/src/locale/ids/en.ts +++ b/src/locale/ids/en.ts @@ -58,6 +58,7 @@ export const en: Locale = { 'connectionBrowser.renameConnection.noConnParmsFound': `No parameters for connection '{0}' was found`, 'connectionBrowser.renameConnection.error': `Error renaming connection '{0}'! {1}`, 'connectionBrowser.deleteConnection.warning': `Are you sure you want to delete the connection '{0}'?`, + 'connectionBrowser.deleteConnection.multiple.warning': `Are you sure you want to delete these {0} connections?`, 'connectionBrowser.ServerItem.tooltip': ` (previous connection)`, 'connectionBrowser.ServerItem.title': `Connect`, // helpView: @@ -96,7 +97,7 @@ export const en: Locale = { 'ifsBrowser.deleteIFS.multi.warningMessage': `Are you sure you want to delete the {0} selected files?`, 'ifsBrowser.deleteIFS.deletionPrompt': `Once you delete the directory, it cannot be restored.\nPlease type \"{0}\" to confirm deletion.`, 'ifsBrowser.deleteIFS.deletionPrompt2': ` (Press \'Escape\' to cancel)`, - 'ifsBrowser.deleteIFS.infoMessage': `Deleted {0}.`, + 'ifsBrowser.deleteIFS.progress': `Deleting {0} element(s)...`, 'ifsBrowser.deleteIFS.errorMessage': `Error deleting streamfile! {0}`, 'ifsBrowser.deleteIFS.cancelled': `Deletion canceled.`, 'ifsBrowser.deleteIFS.default.home.dir':'{0} was the working directory; it is now {1}.', @@ -143,7 +144,9 @@ export const en: Locale = { 'LibraryListView.cleanupLibraryList.removedLibs': `The following libraries were removed from the updated library list as they are invalid: {0}`, 'LibraryListView.cleanupLibraryList.validated': `Library list were validated without any errors.`, // objectBrowser: - 'objectBrowser.deleteFilter.infoMessage': `Delete filter '{0}'?`, + 'objectBrowser.delete.confirm':'Are you sure you want to delete {0}?', + 'objectBrowser.delete.multiple.confirm':'Are you sure you want to delete these {0} elements?', + 'objectBrowser.delete.progress':'Deleting', 'objectBrowser.createMember.prompt': `Name of new source member (member.ext)`, 'objectBrowser.createMember.progressTitle': `Creating member {0}...`, 'objectBrowser.createMember.errorMessage': `Error creating member {0}: {1}`, @@ -153,8 +156,6 @@ export const en: Locale = { 'objectBrowser.copyMember.overwrite': `Are you sure you want overwrite member {0}?`, 'objectBrowser.copyMember.errorMessage2': `Member {0} already exists!`, 'objectBrowser.copyMember.errorMessage3': `Error creating member {0}: {1}`, - 'objectBrowser.deleteMember.warningMessage': `Are you sure you want to delete {0}?`, - 'objectBrowser.deleteMember.infoMessage': `Deleted {0}.`, 'objectBrowser.deleteMember.errorMessage': `Error deleting member! {0}`, 'objectBrowser.updateMemberText.prompt': `Change member description for {0}, *BLANK for no description`, 'objectBrowser.updateMemberText.errorMessage': `Error changing member description! {0}`, @@ -194,10 +195,7 @@ export const en: Locale = { 'objectBrowser.copyObject.infoMessage': `Copied object {0} {1} to {2}.`, 'objectBrowser.copyObject.infoMessage2': `Copied object {0} {1} to {2}. Refresh object browser.`, 'objectBrowser.copyObject.errorMessage4': `Error copying object {0}! {1}`, - 'objectBrowser.deleteObject.warningMessage': `Are you sure you want to delete {0} {1}?`, - 'objectBrowser.deleteObject.infoMessage': `Deleted {0} {1}.`, 'objectBrowser.deleteObject.errorMessage': `Error deleting object! {0}`, - 'objectBrowser.deleteObject.progress':'Deleting object {0} {1}...', 'objectBrowser.renameObject.prompt': `Rename object`, 'objectBrowser.renameObject.errorMessage': `Object name must be 10 chars or less.`, 'objectBrowser.renameObject.infoMessage': `Renamed object {0} {1} to {2}.`, diff --git a/src/locale/ids/fr.ts b/src/locale/ids/fr.ts index 1660ecd0f..87d90a3e3 100644 --- a/src/locale/ids/fr.ts +++ b/src/locale/ids/fr.ts @@ -58,6 +58,7 @@ export const fr: Locale = { 'connectionBrowser.renameConnection.noConnParmsFound': `Aucun paramètre pour la connexion '{0}' n'a été trouvé`, 'connectionBrowser.renameConnection.error': `Erreur lors du renommage de la connexion '{0}'! {1}`, 'connectionBrowser.deleteConnection.warning': `Êtes vous sûr de vouloir supprimer la connexion '{0}'?`, + 'connectionBrowser.deleteConnection.multiple.warning': `Êtes vous sûr de vouloir supprimer ces {0} connexions?`, 'connectionBrowser.ServerItem.tooltip': ` (précédente connexion)`, 'connectionBrowser.ServerItem.title': `Se Connecter`, // helpView: @@ -96,7 +97,7 @@ export const fr: Locale = { 'ifsBrowser.deleteIFS.multi.warningMessage': `Êtes-vous sûr de vouloir suprimer les {0} fichiers sélectionnés?`, 'ifsBrowser.deleteIFS.deletionPrompt': `La suppression du répertoire est irréversible!\nEntrez \"{0}\" pour confirmer la suppression.`, 'ifsBrowser.deleteIFS.deletionPrompt2': ` (Appuyer sur \'Escape\' pour annuler)`, - 'ifsBrowser.deleteIFS.infoMessage': `{0} supprimé.`, + 'ifsBrowser.deleteIFS.progress': `Suppression de {0} élément(s)...`, 'ifsBrowser.deleteIFS.errorMessage': `Erreur lors de la suppression du fichier! {0}`, 'ifsBrowser.deleteIFS.cancelled': `Suppression annulée.`, 'ifsBrowser.deleteIFS.default.home.dir':'{0} était le répertoire de travail; c\'est désormais {1}.', @@ -143,7 +144,9 @@ export const fr: Locale = { 'LibraryListView.cleanupLibraryList.removedLibs': `Les bibliothèques suivantes ont été retirées de la liste des bibliothèques mise à jour car elles sont invalides: {0}`, 'LibraryListView.cleanupLibraryList.validated': `La liste des bibliothèques a été validée avec succès.`, // objectBrowser: - 'objectBrowser.deleteFilter.infoMessage': `Supprimer le filtre '{0}'?`, + 'objectBrowser.delete.confirm':'Êtes-vous sûr de vouloir supprimer {0}?', + 'objectBrowser.delete.multiple.confirm':'Êtes-vous sûr de vouloir supprimer les {0} éléments sélectionnés?', + 'objectBrowser.delete.progress':'Suppression', 'objectBrowser.createMember.prompt': `Nom du nouveau membre source (membre.ext)`, 'objectBrowser.createMember.progressTitle': `Creation du membre {0}...`, 'objectBrowser.createMember.errorMessage': `Erreur lors de la creation du membre {0}: {1}`, @@ -153,8 +156,6 @@ export const fr: Locale = { 'objectBrowser.copyMember.overwrite': `Êtes-vous sûr de vouloir écraser le membre {0}?`, 'objectBrowser.copyMember.errorMessage2': `Le membre {0} existe déjà!`, 'objectBrowser.copyMember.errorMessage3': `Erreur lors de la création du membre {0}: {1}`, - 'objectBrowser.deleteMember.warningMessage': `Êtes-vous sûr de vouloir supprimer {0}?`, - 'objectBrowser.deleteMember.infoMessage': `Membre {0} supprimé.`, 'objectBrowser.deleteMember.errorMessage': `Erreur lors de la suppression du membre! {0}`, 'objectBrowser.updateMemberText.prompt': `Changer la description du membre {0}, *BLANK pour effacer la description`, 'objectBrowser.updateMemberText.errorMessage': `Erreur lors de la mise à jour du texte du membre! {0}`, @@ -194,10 +195,7 @@ export const fr: Locale = { 'objectBrowser.copyObject.infoMessage': `Objet {0} {1} copié vers {2}.`, 'objectBrowser.copyObject.infoMessage2': `Objet {0} {1} copié vers {2}. Rafraîchissement de l'explorateur d'objet.`, 'objectBrowser.copyObject.errorMessage4': `Erreur lors de la copie de l'objet {0}! {1}`, - 'objectBrowser.deleteObject.warningMessage': `Êtes-vous sûr de vouloir supprimer {0} {1}?`, - 'objectBrowser.deleteObject.infoMessage': `{0} {1} supprimé.`, 'objectBrowser.deleteObject.errorMessage': `Erreur lors de la suppression de l'objet! {0}`, - 'objectBrowser.deleteObject.progress':`Suppression de l'objet {0} {1}...`, 'objectBrowser.renameObject.prompt': `Renamer l'objet`, 'objectBrowser.renameObject.errorMessage': `Le nom de l'objet doit faire au maximum 10 caractères de long.`, 'objectBrowser.renameObject.infoMessage': `Objet {0} {1} renommé {2}.`, diff --git a/src/views/ConnectionBrowser.ts b/src/views/ConnectionBrowser.ts index 936b785d8..942537c17 100644 --- a/src/views/ConnectionBrowser.ts +++ b/src/views/ConnectionBrowser.ts @@ -6,169 +6,183 @@ import { GlobalStorage } from '../api/Storage'; import { instance } from '../instantiate'; import { t } from "../locale"; import { Login } from '../webviews/login'; -import { SettingsUI } from '../webviews/settings'; -export class ObjectBrowserProvider { - private _attemptingConnection: boolean; - private readonly _emitter: vscode.EventEmitter; - readonly onDidChangeTreeData: vscode.Event; - - constructor(context: vscode.ExtensionContext) { - this._attemptingConnection = false; - this._emitter = new vscode.EventEmitter(); - this.onDidChangeTreeData = this._emitter.event; - - SettingsUI.init(context); - - context.subscriptions.push( - vscode.commands.registerCommand(`code-for-ibmi.connect`, () => { - if (!this._attemptingConnection) { - Login.show(context); +export function initializeConnectionBrowser(context: vscode.ExtensionContext) { + const connectionBrowser = new ConnectionBrowser(); + const connectionTreeViewer = vscode.window.createTreeView( + `connectionBrowser`, { + treeDataProvider: connectionBrowser, + showCollapseAll: false, + canSelectMany: true + }); + + context.subscriptions.push( + connectionTreeViewer, + vscode.commands.registerCommand(`code-for-ibmi.connect`, () => { + if (!connectionBrowser.attemptingConnection) { + Login.show(context); + } + }), + + vscode.commands.registerCommand(`code-for-ibmi.connectToPrevious`, async () => { + const lastConnection = GlobalStorage.get().getLastConnections()?.[0]; + if (lastConnection) { + return await vscode.commands.executeCommand(`code-for-ibmi.connectTo`, lastConnection.name); + } + }), + + vscode.commands.registerCommand(`code-for-ibmi.connectTo`, async (name?: string | Server, reloadServerSettings?: boolean) => { + if (!connectionBrowser.attemptingConnection) { + connectionBrowser.attemptingConnection = true; + + if (!name) { + const lastConnections = GlobalStorage.get().getLastConnections() || []; + if (lastConnections && lastConnections.length) { + name = (await vscode.window.showQuickPick([{ kind: vscode.QuickPickItemKind.Separator, label: t(`connectionBrowser.connectTo.lastConnection`) }, + ...lastConnections.map(lc => ({ label: lc.name, description: t(`connectionBrowser.connectTo.lastUsed`, new Date(lc.timestamp).toLocaleString()) }))], + { title: t(`connectionBrowser.connectTo.title`) } + ))?.label; + } } - }), - vscode.commands.registerCommand(`code-for-ibmi.connectToPrevious`, async () => { - const lastConnection = GlobalStorage.get().getLastConnections()?.[0]; - if (lastConnection) { - return await vscode.commands.executeCommand(`code-for-ibmi.connectTo`, lastConnection.name); + switch (typeof name) { + case `string`: // Name of connection object + await Login.LoginToPrevious(name, context, reloadServerSettings); + break; + case `object`: // A Server object + await Login.LoginToPrevious(name.name, context, reloadServerSettings); + break; + default: + vscode.window.showErrorMessage(t(`connectionBrowser.connectTo.error`)); + break; } - }), - - vscode.commands.registerCommand(`code-for-ibmi.connectTo`, async (name?: string | Server, reloadServerSettings?: boolean) => { - if (!this._attemptingConnection) { - this._attemptingConnection = true; - - if (!name) { - const lastConnections = GlobalStorage.get().getLastConnections() || []; - if (lastConnections && lastConnections.length) { - name = (await vscode.window.showQuickPick([{ kind: vscode.QuickPickItemKind.Separator, label: t(`connectionBrowser.connectTo.lastConnection`) }, - ...lastConnections.map(lc => ({ label: lc.name, description: t(`connectionBrowser.connectTo.lastUsed`, new Date(lc.timestamp).toLocaleString()) }))], - { title: t(`connectionBrowser.connectTo.title`) } - ))?.label; - } - } - switch (typeof name) { - case `string`: // Name of connection object - await Login.LoginToPrevious(name, context, reloadServerSettings); - break; - case `object`: // A Server object - await Login.LoginToPrevious(name.name, context, reloadServerSettings); - break; - default: - vscode.window.showErrorMessage(t(`connectionBrowser.connectTo.error`)); - break; + connectionBrowser.attemptingConnection = false; + } + }), + + vscode.commands.registerCommand(`code-for-ibmi.connectToAndReload`, async (server: Server) => { + if (!connectionBrowser.attemptingConnection && server) { + const reloadServerSettings = true; + vscode.commands.executeCommand(`code-for-ibmi.connectTo`, server.name, reloadServerSettings); + } + }), + + vscode.commands.registerCommand(`code-for-ibmi.refreshConnections`, () => { + connectionBrowser.refresh(); + }), + + vscode.commands.registerCommand(`code-for-ibmi.renameConnection`, async (server: Server) => { + if (!connectionBrowser.attemptingConnection && server) { + const existingConnections = GlobalConfiguration.get(`connections`) || []; + const newName = await vscode.window.showInputBox({ + prompt: t(`connectionBrowser.renameConnection.prompt`, server.name), + value: server.name, + validateInput: newName => { + if (newName === server.name) { + return t(`connectionBrowser.renameConnection.invalid.input`); + } else if (existingConnections.findIndex(item => item.name === newName) !== -1) { + return t(`connectionBrowser.renameConnection.alreadyExists`, newName); + } } - - this._attemptingConnection = false; - } - }), - - vscode.commands.registerCommand(`code-for-ibmi.connectToAndReload`, async (server: Server) => { - if (!this._attemptingConnection && server) { - const reloadServerSettings = true; - vscode.commands.executeCommand(`code-for-ibmi.connectTo`, server.name, reloadServerSettings); - } - }), - - vscode.commands.registerCommand(`code-for-ibmi.refreshConnections`, () => { - this.refresh(); - }), - - vscode.commands.registerCommand(`code-for-ibmi.renameConnection`, async (server: Server) => { - if (!this._attemptingConnection && server) { - const existingConnections = GlobalConfiguration.get(`connections`) || []; - const newName = await vscode.window.showInputBox({ - prompt: t(`connectionBrowser.renameConnection.prompt`, server.name), - value: server.name, - validateInput: newName => { - if (newName === server.name) { - return t(`connectionBrowser.renameConnection.invalid.input`); - } else if (existingConnections.findIndex(item => item.name === newName) !== -1) { - return t(`connectionBrowser.renameConnection.alreadyExists`, newName); - } + }); + + if (newName) { + try { + let index; + // First rename the connection details + const connections = GlobalConfiguration.get(`connections`) || []; + index = connections.findIndex(connection => connection.name === server.name); + if (index === -1) throw (t(`connectionBrowser.renameConnection.noConnectionFound`, server.name)); + connections[index].name = newName; + + // Then rename the connection settings + const connectionSettings = GlobalConfiguration.get(`connectionSettings`) || []; + index = connectionSettings.findIndex(connection => connection.name === server.name); + if (index === -1) throw (t(`connectionBrowser.renameConnection.noConnParmsFound`, server.name)); + connectionSettings[index].name = newName; + + // Then get the cached connection settings + const cachedConnectionSettings = GlobalStorage.get().getServerSettingsCache(server.name); + + // Then get the password key + const secret = await context.secrets.get(`${server.name}_password`); + + // No errors - update the settings. + await GlobalConfiguration.set(`connectionSettings`, connectionSettings); + await GlobalConfiguration.set(`connections`, connections); + if (cachedConnectionSettings) { + GlobalStorage.get().setServerSettingsCache(newName, cachedConnectionSettings); + GlobalStorage.get().deleteServerSettingsCache(server.name); } - }); - - if (newName) { - try { - let index; - // First rename the connection details - const connections = GlobalConfiguration.get(`connections`) || []; - index = connections.findIndex(connection => connection.name === server.name); - if (index === -1) throw(t(`connectionBrowser.renameConnection.noConnectionFound`, server.name)); - connections[index].name = newName; - - // Then rename the connection settings - const connectionSettings = GlobalConfiguration.get(`connectionSettings`) || []; - index = connectionSettings.findIndex(connection => connection.name === server.name); - if (index === -1) throw(t(`connectionBrowser.renameConnection.noConnParmsFound`, server.name)); - connectionSettings[index].name = newName; - - // Then get the cached connection settings - const cachedConnectionSettings = GlobalStorage.get().getServerSettingsCache(server.name); - - // Then get the password key - const secret = await context.secrets.get(`${server.name}_password`); - - // No errors - update the settings. - await GlobalConfiguration.set(`connectionSettings`, connectionSettings); - await GlobalConfiguration.set(`connections`, connections); - if(cachedConnectionSettings) { - GlobalStorage.get().setServerSettingsCache(newName, cachedConnectionSettings); - GlobalStorage.get().deleteServerSettingsCache(server.name); - } - if (secret) { - await context.secrets.store(`${newName}_password`, secret); - await context.secrets.delete(`${server.name}_password`); - } - - this.refresh(); - } catch (e: any) { - vscode.window.showErrorMessage( - t(`connectionBrowser.renameConnection.error`, server.name, e.message || String(e))); + if (secret) { + await context.secrets.store(`${newName}_password`, secret); + await context.secrets.delete(`${server.name}_password`); } + + connectionBrowser.refresh(); + } catch (e: any) { + vscode.window.showErrorMessage( + t(`connectionBrowser.renameConnection.error`, server.name, e.message || String(e))); } } - }), - - vscode.commands.registerCommand(`code-for-ibmi.sortConnections`, async () => { - const connections = GlobalConfiguration.get(`connections`) || []; - connections.sort((conn1, conn2) => conn1.name.localeCompare(conn2.name)); - await GlobalConfiguration.set(`connections`, connections); - this.refresh(); - }), - - vscode.commands.registerCommand(`code-for-ibmi.deleteConnection`, (server: Server) => { - if (!this._attemptingConnection && server) { - vscode.window.showWarningMessage( - t(`connectionBrowser.deleteConnection.warning`, server.name), - t(`Yes`), t(`No`) - ).then(async (value) => { - if (value === t(`Yes`)) { - // First remove the connection details - const connections = GlobalConfiguration.get(`connections`) || []; - const newConnections = connections.filter(connection => connection.name !== server.name); - await GlobalConfiguration.set(`connections`, newConnections); - - // Also remove the connection settings - const connectionSettings = GlobalConfiguration.get(`connectionSettings`) || []; - const newConnectionSettings = connectionSettings.filter(connection => connection.name !== server.name); - await GlobalConfiguration.set(`connectionSettings`, newConnectionSettings); - - // Also remove the cached connection settings - GlobalStorage.get().deleteServerSettingsCache(server.name); - - // Then remove the password - await context.secrets.delete(`${server.name}_password`); + } + }), + + vscode.commands.registerCommand(`code-for-ibmi.sortConnections`, async () => { + const connections = GlobalConfiguration.get(`connections`) || []; + connections.sort((conn1, conn2) => conn1.name.localeCompare(conn2.name)); + await GlobalConfiguration.set(`connections`, connections); + connectionBrowser.refresh(); + }), + + vscode.commands.registerCommand(`code-for-ibmi.deleteConnection`, async (single?: Server, servers?: Server[]) => { + const toBeDeleted: Server[] = []; + if (servers) { + toBeDeleted.push(...servers); + } + else if (single) { + toBeDeleted.push(single); + } + else { + toBeDeleted.push(...connectionTreeViewer.selection); + } + + if (!connectionBrowser.attemptingConnection && toBeDeleted.length) { + const message = toBeDeleted.length === 1 ? t(`connectionBrowser.deleteConnection.warning`, toBeDeleted[0].name) : t(`connectionBrowser.deleteConnection.multiple.warning`, toBeDeleted.length); + const detail = toBeDeleted.length === 1 ? undefined : toBeDeleted.map(server => `- ${server.name}`).join("\n"); + if (await vscode.window.showWarningMessage(message, { modal: true, detail }, t(`Yes`))) { + for (const server of toBeDeleted) { + // First remove the connection details + const connections = GlobalConfiguration.get(`connections`) || []; + const newConnections = connections.filter(connection => connection.name !== server.name); + await GlobalConfiguration.set(`connections`, newConnections); + + // Also remove the connection settings + const connectionSettings = GlobalConfiguration.get(`connectionSettings`) || []; + const newConnectionSettings = connectionSettings.filter(connection => connection.name !== server.name); + await GlobalConfiguration.set(`connectionSettings`, newConnectionSettings); + + // Also remove the cached connection settings + GlobalStorage.get().deleteServerSettingsCache(server.name); + + // Then remove the password + await context.secrets.delete(`${server.name}_password`); + } - this.refresh(); - } - }); + connectionBrowser.refresh(); } - }) - ); + } + }) + ); +} + +class ConnectionBrowser implements vscode.TreeDataProvider { + public attemptingConnection: boolean = false; + private readonly _emitter: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._emitter.event; + constructor() { instance.onEvent("disconnected", () => this.refresh()) } diff --git a/src/views/ifsBrowser.ts b/src/views/ifsBrowser.ts index 81727945c..f48c7db17 100644 --- a/src/views/ifsBrowser.ts +++ b/src/views/ifsBrowser.ts @@ -9,7 +9,7 @@ import { GlobalStorage } from "../api/Storage"; import { Tools } from "../api/Tools"; import { instance, setSearchResults } from "../instantiate"; import { t } from "../locale"; -import { BrowserItem, BrowserItemParameters, CommandResult, FocusOptions, IFSFile, IFS_BROWSER_MIMETYPE, OBJECT_BROWSER_MIMETYPE, WithPath } from "../typings"; +import { BrowserItem, BrowserItemParameters, FocusOptions, IFSFile, IFS_BROWSER_MIMETYPE, OBJECT_BROWSER_MIMETYPE, WithPath } from "../typings"; const URI_LIST_MIMETYPE = "text/uri-list"; const URI_LIST_SEPARATOR = "\r\n"; @@ -100,13 +100,13 @@ class IFSItem extends BrowserItem implements WithPath { constructor(readonly file: IFSFile, parameters: BrowserItemParameters) { super(file.name, parameters); this.path = file.path; - this.tooltip = new vscode.MarkdownString(Tools.generateTooltipHtmlTable(this.path, { + this.tooltip = new vscode.MarkdownString(Tools.generateTooltipHtmlTable(this.path, { size: file.size, modified: file.modified ? new Date(file.modified.getTime() - file.modified.getTimezoneOffset() * 60 * 1000).toISOString().slice(0, 19).replace(`T`, ` `) : ``, owner: file.owner ? file.owner.toUpperCase() : `` })); this.tooltip.supportHtml = true; - } + } sortBy(sort: SortOptions) { if (this.sort.order !== sort.order) { @@ -162,7 +162,7 @@ class IFSDirectoryItem extends IFSItem { if (content) { try { const showHidden = instance.getConfig()?.showHiddenFiles; - const filterIFSFile = (file:IFSFile, type: "directory" | "streamfile") => file.type === type && (showHidden || !file.name.startsWith(`.`) || alwaysShow(file.name)); + const filterIFSFile = (file: IFSFile, type: "directory" | "streamfile") => file.type === type && (showHidden || !file.name.startsWith(`.`) || alwaysShow(file.name)); const objects = await content.getFileList(this.path, this.sort, handleFileListErrors); const directories = objects.filter(f => filterIFSFile(f, "directory")); const streamFiles = objects.filter(f => filterIFSFile(f, "streamfile")); @@ -216,8 +216,8 @@ class IFSBrowserDragAndDrop implements vscode.TreeDragAndDropController if (ifsBrowserItems) { this.moveOrCopyItems(ifsBrowserItems.value as IFSItem[], toDirectory) } else if (objectBrowserItems) { - const memberUris = (await objectBrowserItems.asString()).split(URI_LIST_SEPARATOR).map(uri => vscode.Uri.parse(uri)); - this.copyMembers(memberUris, toDirectory) + const memberUris = (await objectBrowserItems.asString()).split(URI_LIST_SEPARATOR).map(uri => vscode.Uri.parse(uri)); + this.copyMembers(memberUris, toDirectory) } else { const explorerItems = dataTransfer.get(URI_LIST_MIMETYPE); @@ -286,7 +286,7 @@ class IFSBrowserDragAndDrop implements vscode.TreeDragAndDropController noLibList: true }); if (result.code !== 0) { - throw(t(`ifsBrowser.copyToStreamfile.failed`, toDirectory.path, result!.stderr)); + throw (t(`ifsBrowser.copyToStreamfile.failed`, toDirectory.path, result!.stderr)); } }; @@ -546,58 +546,68 @@ export function initializeIFSBrowser(context: vscode.ExtensionContext) { const connection = instance.getConnection(); const config = instance.getConfig(); if (connection && config) { - items = items || [singleItem]; - if (!items.find(n => isProtected(n.path))) { - let deletionConfirmed = false; - const proceed = items.length > 1 ? - await vscode.window.showWarningMessage(t(`ifsBrowser.deleteIFS.multi.warningMessage`, items.length), t(`Yes`), t(`Cancel`)) === t(`Yes`) : - await vscode.window.showWarningMessage(t(`ifsBrowser.deleteIFS.warningMessage`, items[0].path), t(`Yes`), t(`Cancel`)) === t(`Yes`); - if (proceed) { - for (const item of items) { - if ((GlobalConfiguration.get(`safeDeleteMode`)) && item.contextValue === `directory`) { //Check if path is directory - const dirName = path.basename(item.path) //Get the name of the directory to be deleted - - const deletionPrompt = t(`ifsBrowser.deleteIFS.deletionPrompt`, dirName); - const input = await vscode.window.showInputBox({ - placeHolder: dirName, - prompt: deletionPrompt, - validateInput: text => { - return (text === dirName) ? null : deletionPrompt + t(`ifsBrowser.deleteIFS.deletionPrompt2`); - } - }); - deletionConfirmed = (input === dirName); - } - else { - // If deleting a file rather than a directory, skip the name entry - deletionConfirmed = true; - } + if (items || singleItem) { + items = (items || [singleItem]).filter(reduceIFSPath); + } + else { + items = (ifsTreeViewer.selection.filter(selected => selected instanceof IFSItem) as IFSItem[]).filter(reduceIFSPath); + } + + if (items && items.length) { + if (!items.find(n => isProtected(n.path))) { + let deletionConfirmed = false; + const message = items.length === 1 ? t(`ifsBrowser.deleteIFS.warningMessage`, items[0].path) : t(`ifsBrowser.deleteIFS.multi.warningMessage`, items.length); + const detail = items.length === 1 ? undefined : items.map(i => `- ${i.path}`).join("\n"); + if (await vscode.window.showWarningMessage(message, { modal: true, detail }, t(`Yes`))) { + const toBeDeleted: string[] = []; + for (const item of items) { + if ((GlobalConfiguration.get(`safeDeleteMode`)) && item.file.type === `directory`) { //Check if path is directory + const dirName = path.basename(item.path) //Get the name of the directory to be deleted + + const deletionPrompt = t(`ifsBrowser.deleteIFS.deletionPrompt`, dirName); + const input = await vscode.window.showInputBox({ + placeHolder: dirName, + prompt: deletionPrompt, + validateInput: text => { + return (text === dirName) ? null : deletionPrompt + t(`ifsBrowser.deleteIFS.deletionPrompt2`); + } + }); + deletionConfirmed = (input === dirName); + } + else { + // If deleting a file rather than a directory, skip the name entry + // Do not delete a file if one of its parent directory is going to be deleted + deletionConfirmed = true; + } - if (deletionConfirmed) { - try { - const removeResult = await connection.sendCommand({ command: `rm -rf ${Tools.escapePath(item.path)}` }) - if (removeResult.code === 0) { - vscode.window.showInformationMessage(t(`ifsBrowser.deleteIFS.infoMessage`, item.path)); - } - else { - throw removeResult.stderr; - } - } catch (e) { - vscode.window.showErrorMessage(t(`ifsBrowser.deleteIFS.errorMessage`, e)); + if (deletionConfirmed) { + toBeDeleted.push(item.path); } } - else { - vscode.window.showInformationMessage(t(`ifsBrowser.deleteIFS.cancelled`)); + + try { + const removeResult = await vscode.window.withProgress({ title: t('ifsBrowser.deleteIFS.progress', toBeDeleted.length), location: vscode.ProgressLocation.Notification }, async () => { + return await connection.sendCommand({ command: `rm -rf ${toBeDeleted.map(path => Tools.escapePath(path)).join(" ")}` }); + }); + if (removeResult.code !== 0) { + throw removeResult.stderr; + } + if (GlobalConfiguration.get(`autoRefresh`)) { + items.map(item => item.parent) + .filter(Tools.distinct) + .forEach(async parent => parent?.refresh?.()); + } + } catch (e) { + vscode.window.showErrorMessage(t(`ifsBrowser.deleteIFS.errorMessage`, e)); } } - if (GlobalConfiguration.get(`autoRefresh`)) { - items.map(item => item.parent) - .filter(Tools.distinct) - .forEach(async parent => parent?.refresh?.()); + else { + vscode.window.showInformationMessage(t(`ifsBrowser.deleteIFS.cancelled`)); } } - } - else { - vscode.window.showErrorMessage(t(`ifsBrowser.deleteIFS.dirNotAllowed`, items.filter(n => isProtected(n.path)).map(n => n.path).join(`\n`))); + else { + vscode.window.showErrorMessage(t(`ifsBrowser.deleteIFS.dirNotAllowed`, items.filter(n => isProtected(n.path)).map(n => n.path).join(`\n`))); + } } } }), @@ -807,4 +817,18 @@ async function showOpenDialog() { } }) } +} + +/** + * Filters the content of an IFSItem array to keep only: + * - Folders + * - Files whose parent folders are not in the array + */ +function reduceIFSPath(item: IFSItem, index: number, array: IFSItem[]) { + if (item.file.type === "directory") { + return true; + } + else { + return !array.filter(i => i.file.type === "directory").some(folder => item.file.path.startsWith(folder.file.path)); + } } \ No newline at end of file diff --git a/src/views/objectBrowser.ts b/src/views/objectBrowser.ts index 629a80759..3765b5baf 100644 --- a/src/views/objectBrowser.ts +++ b/src/views/objectBrowser.ts @@ -49,11 +49,7 @@ const objectIcons = { '': `circle-large-outline` } -function isProtected(filter: ConnectionConfiguration.ObjectFilters) { - return filter.protected || getContent().isProtectedPath(filter.library); -} - -class ObjectBrowserItem extends BrowserItem { +abstract class ObjectBrowserItem extends BrowserItem { constructor(readonly filter: ConnectionConfiguration.ObjectFilters, label: string, params?: BrowserItemParameters) { super(label, params); } @@ -65,6 +61,10 @@ class ObjectBrowserItem extends BrowserItem { reveal(options?: FocusOptions) { return vscode.commands.executeCommand(`code-for-ibmi.revealInObjectBrowser`, this, options); } + + abstract toString(): string; + abstract delete(): Promise; + abstract isProtected(): boolean; } class ObjectBrowser implements vscode.TreeDataProvider { @@ -163,12 +163,16 @@ class CreateFilterItem extends BrowserItem { class ObjectBrowserFilterItem extends ObjectBrowserItem { constructor(filter: ConnectionConfiguration.ObjectFilters) { - super(filter, filter.name, { icon: isProtected(filter) ? `lock-small` : '', state: vscode.TreeItemCollapsibleState.Collapsed }); - this.contextValue = `filter${isProtected(filter) ? `_readonly` : ``}`; + super(filter, filter.name, { icon: filter.protected ? `lock-small` : '', state: vscode.TreeItemCollapsibleState.Collapsed }); + this.contextValue = `filter${this.isProtected() ? `_readonly` : ``}`; this.description = `${filter.library}/${filter.object}/${filter.member}.${filter.memberType || `*`} (${filter.types.join(`, `)})`; this.tooltip = ``; } + isProtected(): boolean { + return this.filter.protected; + } + async getChildren(): Promise { const libraryFilter = parseFilter(this.filter.library); if (libraryFilter.noFilter) { @@ -181,6 +185,24 @@ class ObjectBrowserFilterItem extends ObjectBrowserItem { }); } } + + toString(): string { + return `${this.filter.name} (filter)`; + } + + async delete() { + const config = getConfig(); + const filter = this.filter; + const index = config.objectFilters.findIndex(f => f.name === filter.name); + + if (index > -1) { + config.objectFilters.splice(index, 1); + await ConnectionConfiguration.update(config); + + } + + return true; + } } class ObjectBrowserSourcePhysicalFileItem extends ObjectBrowserItem implements ObjectItem { @@ -191,7 +213,7 @@ class ObjectBrowserSourcePhysicalFileItem extends ObjectBrowserItem implements O const type = object.type.startsWith(`*`) ? object.type.substring(1) : object.type; super(parent.filter, correctCase(object.name), { parent, icon: `file-directory`, state: vscode.TreeItemCollapsibleState.Collapsed }); - this.contextValue = `SPF${isProtected(this.filter) ? `_readonly` : ``}`; + this.contextValue = `SPF${this.isProtected() ? `_readonly` : ``}`; this.updateDescription(); this.path = [object.library, object.name].join(`/`); @@ -209,6 +231,10 @@ class ObjectBrowserSourcePhysicalFileItem extends ObjectBrowserItem implements O }); } + isProtected(): boolean { + return this.filter.protected || getContent().isProtectedPath(this.object.library); + } + sortBy(sort: SortOptions) { if (this.sort.order !== sort.order) { this.sort.order = sort.order; @@ -273,6 +299,14 @@ class ObjectBrowserSourcePhysicalFileItem extends ObjectBrowserItem implements O } } } + + toString(): string { + return `${this.path} (${this.object.type})`; + } + + async delete() { + return deleteObject(this.object); + } } class ObjectBrowserObjectItem extends ObjectBrowserItem implements ObjectItem { @@ -287,7 +321,7 @@ class ObjectBrowserObjectItem extends ObjectBrowserItem implements ObjectItem { this.path = [object.library, object.name].join(`/`); this.updateDescription(); - this.contextValue = `object.${type.toLowerCase()}${object.attribute ? `.${object.attribute}` : ``}${isProtected(this.filter) ? `_readonly` : ``}`; + this.contextValue = `object.${type.toLowerCase()}${object.attribute ? `.${object.attribute}` : ``}${this.isProtected() ? `_readonly` : ``}`; this.tooltip = new vscode.MarkdownString(Tools.generateTooltipHtmlTable(this.path, { type: object.type, attribute: object.attribute, @@ -315,6 +349,10 @@ class ObjectBrowserObjectItem extends ObjectBrowserItem implements ObjectItem { } } + isProtected(): boolean { + return this.filter.protected || getContent().isProtectedPath(this.object.library); + } + updateDescription() { this.description = this.object.text.trim() + (this.object.attribute ? ` (${this.object.attribute})` : ``); } @@ -324,14 +362,22 @@ class ObjectBrowserObjectItem extends ObjectBrowserItem implements ObjectItem { objectFilter.library = this.object.name; return await listObjects(this, objectFilter); } + + toString(): string { + return `${this.path} (${this.object.type})`; + } + + async delete() { + return deleteObject(this.object); + } } class ObjectBrowserMemberItem extends ObjectBrowserItem implements MemberItem { readonly path: string; readonly sortBy: (sort: SortOptions) => void; - + readonly readonly: boolean; constructor(parent: ObjectBrowserSourcePhysicalFileItem, readonly member: IBMiMember, writable: boolean) { - const readonly = isProtected(parent.filter) || !writable; + const readonly = !writable || parent.isProtected(); super(parent.filter, correctCase(`${member.name}.${member.extension}`), { icon: readonly ? `lock-small` : "", parent }); this.contextValue = `member${readonly ? `_readonly` : ``}`; this.description = member.text; @@ -353,6 +399,32 @@ class ObjectBrowserMemberItem extends ObjectBrowserItem implements MemberItem { title: `Open Member`, arguments: [this, (readonly ? "browse" : undefined) as DefaultOpenMode] }; + + this.readonly = readonly; + } + + isProtected(): boolean { + return this.readonly; + } + + toString(): string { + return this.path; + } + + async delete() { + const connection = getConnection(); + const { library, file, name } = connection.parserMemberPath(this.path); + + const removeResult = await connection.runCommand({ + command: `RMVM FILE(${library}/${file}) MBR(${name})`, + noLibList: true + }); + + if (removeResult.code !== 0) { + vscode.window.showErrorMessage(t(`objectBrowser.deleteMember.errorMessage`, removeResult.stderr)); + } + + return removeResult.code === 0; } } @@ -362,8 +434,8 @@ class ObjectBrowserMemberItemDragAndDrop implements vscode.TreeDragAndDropContro handleDrag(source: readonly ObjectBrowserMemberItem[], dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken) { dataTransfer.set(OBJECT_BROWSER_MIMETYPE, new vscode.DataTransferItem(source.filter(item => item.resourceUri?.scheme === `member`) - .map(item => item.resourceUri) - .join(URI_LIST_SEPARATOR))); + .map(item => item.resourceUri) + .join(URI_LIST_SEPARATOR))); } } @@ -461,22 +533,6 @@ export function initializeObjectBrowser(context: vscode.ExtensionContext) { objectBrowser.refresh(); }), - vscode.commands.registerCommand(`code-for-ibmi.deleteFilter`, async (node: FilteredItem) => { - const config = getConfig(); - const filter = node.filter; - vscode.window.showInformationMessage(t(`objectBrowser.deleteFilter.infoMessage`, filter.name), t(`Yes`), t(`No`)).then(async (value) => { - if (value === t(`Yes`)) { - const index = config.objectFilters.findIndex(f => f.name === filter.name); - - if (index > -1) { - config.objectFilters.splice(index, 1); - await ConnectionConfiguration.update(config); - objectBrowser.refresh(); - } - } - }); - }), - vscode.commands.registerCommand(`code-for-ibmi.moveFilterUp`, (node: ObjectBrowserFilterItem) => objectBrowser.moveFilterInList(node, `UP`)), vscode.commands.registerCommand(`code-for-ibmi.moveFilterDown`, (node: ObjectBrowserFilterItem) => objectBrowser.moveFilterInList(node, `DOWN`)), vscode.commands.registerCommand(`code-for-ibmi.moveFilterToTop`, (node: ObjectBrowserFilterItem) => objectBrowser.moveFilterInList(node, `TOP`)), @@ -628,30 +684,6 @@ export function initializeObjectBrowser(context: vscode.ExtensionContext) { } } }), - vscode.commands.registerCommand(`code-for-ibmi.deleteMember`, async (node: ObjectBrowserMemberItem) => { - let result = await vscode.window.showWarningMessage(t(`objectBrowser.deleteMember.warningMessage`, node.path), t(`Yes`), t(`Cancel`)); - - if (result === t(`Yes`)) { - const connection = getConnection(); - const { library, file, name } = connection.parserMemberPath(node.path); - - const removeResult = await connection.runCommand({ - command: `RMVM FILE(${library}/${file}) MBR(${name})`, - noLibList: true - }); - - if (removeResult.code === 0) { - vscode.window.showInformationMessage(t(`objectBrowser.deleteMember.infoMessage`, node.path)); - - objectBrowser.refresh(node.parent); - - } else { - vscode.window.showErrorMessage(t(`objectBrowser.deleteMember.errorMessage`, removeResult.stderr)); - } - - //Not sure how to remove the item from the list. Must refresh - but that might be slow? - } - }), vscode.commands.registerCommand(`code-for-ibmi.updateMemberText`, async (node: ObjectBrowserMemberItem) => { const connection = getConnection(); const { library, file, name, basename } = connection.parserMemberPath(node.path); @@ -1043,30 +1075,6 @@ export function initializeObjectBrowser(context: vscode.ExtensionContext) { } while (newPath && !newPathOK) }), - vscode.commands.registerCommand(`code-for-ibmi.deleteObject`, async (node: ObjectBrowserObjectItem | ObjectBrowserSourcePhysicalFileItem) => { - let result = await vscode.window.showWarningMessage(t(`objectBrowser.deleteObject.warningMessage`, node.path, node.object.type.toUpperCase()), t(`Yes`), t(`Cancel`)); - - if (result === t(`Yes`)) { - const connection = getConnection(); - await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: t("objectBrowser.deleteObject.progress", node.path, node.object.type.toUpperCase()) } - , async (progress) => { - // TODO: Progress message about deleting! - const deleteResult = await connection.runCommand({ - command: `DLTOBJ OBJ(${node.path}) OBJTYPE(${node.object.type})`, - noLibList: true - }); - - if (deleteResult.code === 0) { - vscode.window.showInformationMessage(t(`objectBrowser.deleteObject.infoMessage`, node.path, node.object.type.toUpperCase())); - objectBrowser.refresh(node.parent); - } else { - vscode.window.showErrorMessage(t(`objectBrowser.deleteObject.errorMessage`, deleteResult.stderr)); - } - } - ); - } - }), - vscode.commands.registerCommand(`code-for-ibmi.renameObject`, async (node: ObjectBrowserObjectItem) => { let [, newObject] = node.path.split(`/`); let newObjectOK; @@ -1139,6 +1147,62 @@ export function initializeObjectBrowser(context: vscode.ExtensionContext) { }); } } while (newLibrary && !newLibraryOK) + }), + vscode.commands.registerCommand("code-for-ibmi.objectBrowser.delete", async (node?: ObjectBrowserItem, nodes?: ObjectBrowserItem[]) => { + const candidates: ObjectBrowserItem[] = []; + if (nodes) { + candidates.push(...nodes); + } + else if (node) { + candidates.push(node); + } + else { + candidates.push(...objectTreeViewer.selection.filter(i => i instanceof ObjectBrowserItem) as ObjectBrowserItem[]); + } + + const toBeDeleted = candidates.filter(item => !item.isProtected()); + if (toBeDeleted.length) { + const message = toBeDeleted.length === 1 ? t('objectBrowser.delete.confirm', toBeDeleted[0].toString()) : t('objectBrowser.delete.multiple.confirm', toBeDeleted.length); + const detail = toBeDeleted.length === 1 ? undefined : toBeDeleted.map(item => `- ${item.toString()}`).join("\n"); + if (await vscode.window.showWarningMessage(message, { modal: true, detail }, t(`Yes`))) { + const increment = 100 / toBeDeleted.length; + const toRefresh = new Set(); + let refreshBrowser = false; + await vscode.window.withProgress({ title: t("objectBrowser.delete.progress"), location: vscode.ProgressLocation.Notification }, async (task) => { + for (const item of toBeDeleted) { + task.report({ message: item.toString(), increment }); + await item.delete(); + + if (!item.parent) { + //No parent (a filter): the whole browser needs to be refreshed + refreshBrowser = true; + toRefresh.clear(); + } + + if (!refreshBrowser && item.parent) { + //Refresh the element's parent unless its own parent must be refreshed + let parent: BrowserItem | undefined = item.parent; + let found = false + while (!found && parent) { + found = toRefresh.has(parent); + parent = parent.parent + } + + if (!found) { + toRefresh.add(item.parent); + } + } + } + }); + + if (refreshBrowser) { + vscode.commands.executeCommand(`code-for-ibmi.refreshObjectBrowser`); + } + else { + toRefresh.forEach(item => item.refresh?.()); + } + } + } }) ); } @@ -1222,7 +1286,7 @@ async function doSearchInSourceFile(searchTerm: string, path: string, filter: Co } }, timeoutInternal); - let results = await Search.searchMembers(instance, pathParts[0], pathParts[1], `${filter?.member || `*`}.MBR`, searchTerm, filter ? isProtected(filter) : false); + let results = await Search.searchMembers(instance, pathParts[0], pathParts[1], `${filter?.member || `*`}.MBR`, searchTerm, filter?.protected || content.isProtectedPath(pathParts[0])); // Filter search result by member type filter. if (results.length > 0 && filter?.member) { @@ -1275,3 +1339,17 @@ async function listObjects(item: ObjectBrowserFilterItem, filter?: ConnectionCon return object.sourceFile ? new ObjectBrowserSourcePhysicalFileItem(item, object) : new ObjectBrowserObjectItem(item, object); }); } + +async function deleteObject(object: IBMiObject) { + const connection = getConnection(); + const deleteResult = await connection.runCommand({ + command: `DLTOBJ OBJ(${object.library}/${object.name}) OBJTYPE(${object.type})`, + noLibList: true + }); + + if (deleteResult.code !== 0) { + vscode.window.showErrorMessage(t(`objectBrowser.deleteObject.errorMessage`, deleteResult.stderr)); + } + + return deleteResult.code === 0; +} \ No newline at end of file