diff --git a/package.json b/package.json index 8552577d..27348bc3 100644 --- a/package.json +++ b/package.json @@ -236,8 +236,12 @@ "when": "resourceScheme == objectscript && vscode-objectscript.connectActive" }, { - "command": "vscode-objectscript.explorer.project.exportProjectContents", - "when": "false" + "command": "vscode-objectscript.export", + "when": "workspaceFolderCount != 0" + }, + { + "command": "vscode-objectscript.exportProjectContents", + "when": "workspaceFolderCount != 0" }, { "command": "vscode-objectscript.explorer.project.compileProjectContents", @@ -446,11 +450,6 @@ "when": "view == ObjectScriptProjectsExplorer && viewItem == dataNode:projectNode", "group": "5_objectscript_prj@4" }, - { - "command": "vscode-objectscript.explorer.project.exportProjectContents", - "when": "view == ObjectScriptProjectsExplorer && viewItem == dataNode:projectNode", - "group": "5_objectscript_prj@5" - }, { "command": "vscode-objectscript.explorer.project.compileProjectContents", "when": "view == ObjectScriptProjectsExplorer && viewItem == dataNode:projectNode", @@ -837,7 +836,7 @@ { "category": "ObjectScript", "command": "vscode-objectscript.export", - "title": "Export Code from Server" + "title": "Export Code from Server..." }, { "category": "ObjectScript", @@ -1068,8 +1067,8 @@ }, { "category": "ObjectScript", - "command": "vscode-objectscript.explorer.project.exportProjectContents", - "title": "Export Project Contents" + "command": "vscode-objectscript.exportProjectContents", + "title": "Export Project Contents from Server..." }, { "category": "ObjectScript", diff --git a/src/commands/export.ts b/src/commands/export.ts index c500e97e..4fbb69a9 100644 --- a/src/commands/export.ts +++ b/src/commands/export.ts @@ -165,93 +165,70 @@ export async function exportList(files: string[], workspaceFolder: string, names } export async function exportAll(): Promise { - let workspaceFolder: string; - const workspaceList = vscode.workspace.workspaceFolders - .filter((folder) => !schemas.includes(folder.uri.scheme) && config("conn", folder.name).active) - .map((el) => el.name); - if (workspaceList.length > 1) { - const selection = await vscode.window.showQuickPick(workspaceList, { - title: "Pick the workspace folder to export files to.", - }); - if (selection === undefined) { + try { + const wsFolder = await getWsFolder("Pick a workspace folder to export files to.", true, false, true, true); + if (!wsFolder) { + if (wsFolder === undefined) { + // Strict equality needed because undefined == null + vscode.window.showErrorMessage( + "'Export Code from Server...' command requires a workspace folder with an active server connection.", + "Dismiss" + ); + } return; } - workspaceFolder = selection; - } else if (workspaceList.length === 1) { - workspaceFolder = workspaceList.pop(); - } else { - vscode.window.showInformationMessage( - "There are no folders in the current workspace that code can be exported to.", - "Dismiss" - ); - return; - } - if (!config("conn", workspaceFolder).active) { - return; - } - const api = new AtelierAPI(workspaceFolder); - const { category, generated, filter, exactFilter, mapped } = config("export", workspaceFolder); - // Replicate the behavior of getDocNames() but use StudioOpenDialog for better performance - let filterStr = ""; - switch (category) { - case "CLS": - filterStr = "Type = 4"; - break; - case "CSP": - filterStr = "Type %INLIST $LISTFROMSTRING('5,6')"; - break; - case "OTH": - filterStr = "Type NOT %INLIST $LISTFROMSTRING('0,1,2,3,4,5,6,11,12')"; - break; - case "RTN": - filterStr = "Type %INLIST $LISTFROMSTRING('0,1,2,3,11,12')"; - break; - } - if (filter !== "" || exactFilter !== "") { - if (exactFilter !== "") { - if (filterStr !== "") { - filterStr += " AND "; - } - filterStr += `Name LIKE '${exactFilter}'`; - } else { - if (filterStr !== "") { - filterStr += " AND "; - } - filterStr += `Name LIKE '%${filter}%'`; + const api = new AtelierAPI(wsFolder.uri); + const { category, generated, filter, exactFilter, mapped } = config("export", wsFolder.name); + const filters: string[] = []; + switch (category) { + case "CLS": + filters.push("Type = 4"); + break; + case "CSP": + filters.push("Type %INLIST $LISTFROMSTRING('5,6')"); + break; + case "OTH": + filters.push("Type NOT %INLIST $LISTFROMSTRING('0,1,2,3,4,5,6,11,12')"); + break; + case "RTN": + filters.push("Type %INLIST $LISTFROMSTRING('0,1,2,3,11,12')"); + break; } - } - return api - .actionQuery("SELECT Name FROM %Library.RoutineMgr_StudioOpenDialog(?,?,?,?,?,?,?,?,?,?)", [ - "*", - "1", - "1", - api.config.ns.toLowerCase() === "%sys" ? "1" : "0", - "1", - "0", - generated ? "1" : "0", - filterStr, - "0", - mapped ? "1" : "0", - ]) - .then(async (data) => { - let files: vscode.QuickPickItem[] = data.result.content.map((file) => { - return { label: file.Name, picked: true }; - }); - files = await vscode.window.showQuickPick(files, { - canPickMany: true, - ignoreFocusOut: true, - prompt: "Uncheck a file to exclude it. Press 'Escape' to cancel export.", - title: "Files to Export", - }); - if (files === undefined) { - return; - } - return exportList( - files.map((file) => file.label), - workspaceFolder, - api.config.ns + /** Verify that a filter is non-empty and won't allow SQL injection */ + const filterIsValid = (f) => typeof f == "string" && /^(?:[^']|'')+$/.test(f); + if (filterIsValid(exactFilter)) { + filters.push(`Name LIKE '${exactFilter}'`); + } else if (filterIsValid(filter)) { + filters.push(`Name LIKE '%${filter}%'`); + } + let files: vscode.QuickPickItem[] = await api + .actionQuery("SELECT Name FROM %Library.RoutineMgr_StudioOpenDialog('*',1,1,?,1,0,?,?,0,?)", [ + api.ns == "%SYS" ? "1" : "0", + generated ? "1" : "0", + filters.join(" AND "), + mapped ? "1" : "0", + ]) + .then((data) => + data.result.content.map((file) => { + return { label: file.Name, picked: true }; + }) ); + if (!files?.length) return; + files = await vscode.window.showQuickPick(files, { + canPickMany: true, + ignoreFocusOut: true, + prompt: "Uncheck a file to exclude it. Press 'Escape' to cancel export.", + title: "Files to Export", }); + if (!files?.length) return; + await exportList( + files.map((file) => file.label), + wsFolder.name, + api.ns + ); + } catch (error) { + handleError(error, "Error executing 'Export Code from Server...' command."); + } } export async function exportExplorerItems(nodes: NodeBase[]): Promise { diff --git a/src/commands/project.ts b/src/commands/project.ts index 85a4e5d2..cc565fb6 100644 --- a/src/commands/project.ts +++ b/src/commands/project.ts @@ -1,9 +1,8 @@ import * as vscode from "vscode"; import { AtelierAPI } from "../api"; -import { config, filesystemSchemas, projectsExplorerProvider, schemas } from "../extension"; -import { compareConns } from "../providers/DocumentContentProvider"; +import { config, filesystemSchemas, projectsExplorerProvider } from "../extension"; import { isfsDocumentName } from "../providers/FileSystemProvider/FileSystemProvider"; -import { compileErrorMsg, getWsServerConnection, handleError, notIsfs, notNull } from "../utils"; +import { compileErrorMsg, getWsFolder, getWsServerConnection, handleError, notNull } from "../utils"; import { exportList } from "./export"; import { OtherStudioAction, StudioActions } from "./studio"; import { NodeBase, ProjectNode, ProjectRootNode, RoutineNode, CSPFileNode, ClassNode } from "../explorer/nodes"; @@ -14,8 +13,7 @@ export interface ProjectItem { Type: string; } -export async function pickProject(api: AtelierAPI): Promise { - const ns = api.config.ns.toUpperCase(); +export async function pickProject(api: AtelierAPI, allowCreate = true): Promise { const projects: vscode.QuickPickItem[] = await api .actionQuery("SELECT Name, Description FROM %Studio.Project", []) .then((data) => @@ -23,12 +21,19 @@ export async function pickProject(api: AtelierAPI): Promise return { label: prj.Name, detail: prj.Description }; }) ); - if (projects.length === 0) { - const create = await vscode.window.showQuickPick(["Yes", "No"], { - title: `Namespace ${ns} on server '${api.serverId}' contains no projects. Create one?`, - }); - if (create == "Yes") { - return createProject(undefined, api); + if (projects.length == 0) { + if (allowCreate) { + const create = await vscode.window.showQuickPick(["Yes", "No"], { + title: `Namespace ${api.ns} on server '${api.serverId}' contains no projects. Create one?`, + }); + if (create == "Yes") { + return createProject(undefined, api); + } + } else { + vscode.window.showInformationMessage( + `Namespace ${api.ns} on server '${api.serverId}' contains no projects.`, + "Dismiss" + ); } return; } @@ -36,9 +41,9 @@ export async function pickProject(api: AtelierAPI): Promise let result: string; let resolveOnHide = true; const quickPick = vscode.window.createQuickPick(); - quickPick.title = `Select a project in namespace ${ns} on server '${api.serverId}', or click '+' to add one.`; + quickPick.title = `Select a project in namespace ${api.ns} on server '${api.serverId}'${allowCreate ? ", or click '+' to add one" : ""}.`; quickPick.items = projects; - quickPick.buttons = [{ iconPath: new vscode.ThemeIcon("add"), tooltip: "Create new project" }]; + quickPick.buttons = allowCreate ? [{ iconPath: new vscode.ThemeIcon("add"), tooltip: "Create new project" }] : []; async function addAndResolve() { resolveOnHide = false; @@ -879,64 +884,38 @@ export async function modifyProject( } } -export async function exportProjectContents(node: ProjectNode | undefined): Promise { - let workspaceFolder: string; - const api = new AtelierAPI(node.workspaceFolderUri); - api.setNamespace(node.namespace); - const project = node.label; - if (notIsfs(node.workspaceFolderUri)) { - workspaceFolder = node.workspaceFolder; - } else { - const conn = config("conn", node.workspaceFolder); - const workspaceList = vscode.workspace.workspaceFolders - .filter((folder) => { - if (schemas.includes(folder.uri.scheme)) { - return false; - } - const wFolderConn = config("conn", folder.name); - if (!compareConns(conn, wFolderConn)) { - return false; - } - if (!wFolderConn.active) { - return false; - } - if (wFolderConn.ns.toLowerCase() != node.namespace.toLowerCase()) { - return false; - } - return true; - }) - .map((el) => el.name); - if (workspaceList.length > 1) { - const selection = await vscode.window.showQuickPick(workspaceList, { - title: "Pick the workspace folder to export files to.", - }); - if (selection === undefined) { - return; +export async function exportProjectContents(): Promise { + try { + const wsFolder = await getWsFolder("Pick a workspace folder to export files to.", true, false, true, true); + if (!wsFolder) { + if (wsFolder === undefined) { + // Strict equality needed because undefined == null + vscode.window.showErrorMessage( + "'Export Project Contents from Server...' command requires a workspace folder with an active server connection.", + "Dismiss" + ); } - workspaceFolder = selection; - } else if (workspaceList.length === 1) { - workspaceFolder = workspaceList.pop(); - } else { - vscode.window.showInformationMessage( - "There are no folders in the current workspace that code can be exported to.", - "Dismiss" - ); return; } + const api = new AtelierAPI(wsFolder.uri); + const project = await pickProject(api, false); + if (!project) return; + await exportList( + await api + .actionQuery( + "SELECT CASE WHEN sod.Name %STARTSWITH '/' THEN SUBSTR(sod.Name,2) ELSE sod.Name END Name " + + "FROM %Library.RoutineMgr_StudioOpenDialog('*',1,1,1,1,0,1) AS sod JOIN %Studio.Project_ProjectItemsList(?) AS pil " + + "ON sod.Name = pil.Name OR (pil.Type = 'CLS' AND pil.Name||'.cls' = sod.Name) " + + "OR (pil.Type = 'CSP' AND pil.Name = SUBSTR(sod.Name,2)) OR (pil.Type = 'DIR' AND sod.Name %STARTSWITH '/'||pil.Name||'/')", + [project] + ) + .then((data) => data.result.content.map((e) => e.Name)), + wsFolder.name, + api.ns + ); + } catch (error) { + handleError(error, "Error executing 'Export Project Contents from Server...' command."); } - if (workspaceFolder === undefined) { - return; - } - const exportFiles: string[] = await api - .actionQuery( - "SELECT CASE WHEN sod.Name %STARTSWITH '/' THEN SUBSTR(sod.Name,2) ELSE sod.Name END Name " + - "FROM %Library.RoutineMgr_StudioOpenDialog('*',1,1,1,1,0,1) AS sod JOIN %Studio.Project_ProjectItemsList(?) AS pil " + - "ON sod.Name = pil.Name OR (pil.Type = 'CLS' AND pil.Name||'.cls' = sod.Name) " + - "OR (pil.Type = 'CSP' AND pil.Name = SUBSTR(sod.Name,2)) OR (pil.Type = 'DIR' AND sod.Name %STARTSWITH '/'||pil.Name||'/')", - [project] - ) - .then((data) => data.result.content.map((e) => e.Name)); - return exportList(exportFiles, workspaceFolder, node.namespace); } export async function compileProjectContents(node: ProjectNode): Promise { diff --git a/src/extension.ts b/src/extension.ts index 26e39c81..677022af 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1507,9 +1507,9 @@ export async function activate(context: vscode.ExtensionContext): Promise { sendCommandTelemetryEvent("deleteProject"); deleteProject(node); }), - vscode.commands.registerCommand("vscode-objectscript.explorer.project.exportProjectContents", (node) => { - sendCommandTelemetryEvent("explorer.project.exportProjectContents"); - exportProjectContents(node); + vscode.commands.registerCommand("vscode-objectscript.exportProjectContents", () => { + sendCommandTelemetryEvent("exportProjectContents"); + exportProjectContents(); }), vscode.commands.registerCommand("vscode-objectscript.explorer.project.compileProjectContents", (node) => { sendCommandTelemetryEvent("explorer.project.compileProjectContents");