diff --git a/CHANGELOG.md b/CHANGELOG.md index 528ca3d1..eece1083 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog Vlocity/Salesforce Integration for VSCode +## Version 0.16.17 - 2021-25-11 + - Fixed add to profiles does not work when selecting a single field + - Fixed issue with refresh and open didn't work for certain datapacks without matching keys + - Added multi select for exporting salesforce metadata + ## Version 0.16.16 - 2021-11-07 - Fixed #362; issue with tsconfig-paths-webpack-plugin causing relative imports in modules to be incorrectly resolved to local files if they could be resolved locally diff --git a/build/loaders/yaml.js b/build/loaders/yaml.js index f9ef7cb7..03e836f7 100644 --- a/build/loaders/yaml.js +++ b/build/loaders/yaml.js @@ -26,5 +26,5 @@ module.exports = function (source) { .replace(/\u2028/g, '\\u2028') .replace(/\u2029/g, '\\u2029'); - return `module.exports = ${value};`; + return `module.exports = Object.freeze(${value});`; }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8ac6f778..5b600271 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vlocode", - "version": "0.16.16", + "version": "0.16.17", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 45e1d744..7be247c0 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vlocode", "displayName": "Salesforce Vlocity Integration", "description": "Salesforce and Vlocity development and deployment extension for VSCode", - "version": "0.16.16", + "version": "0.16.17", "license": "MIT", "icon": "resources/icon.png", "author": { diff --git a/src/commands/exportDatapackCommand.ts b/src/commands/exportDatapackCommand.ts index 9ea17c0f..1884c7d8 100644 --- a/src/commands/exportDatapackCommand.ts +++ b/src/commands/exportDatapackCommand.ts @@ -82,13 +82,13 @@ export default class ExportDatapackCommand extends DatapackCommand { } protected async showDatapackTypeSelection() : Promise { - const datapackOptions = Object.values(exportQueryDefinitions).filter(queryDef => queryDef.query).map( - queryDef => ({ - label: queryDef.VlocityDataPackType, + const datapackOptions = Object.entries(exportQueryDefinitions) + .filter(([,queryDef]) => queryDef.query && !queryDef.requiredSetting) + .map(([key, queryDef]) => ({ + label: key, detail: queryDef.query.replace(constants.NAMESPACE_PLACEHOLDER_PATTERN, 'vlocity'), datapackType: queryDef.VlocityDataPackType - }) - ); + })); const datapackToExport = await vscode.window.showQuickPick(datapackOptions, { matchOnDetail: true, diff --git a/src/commands/salesforce/retrieveMetadataCommand.ts b/src/commands/salesforce/retrieveMetadataCommand.ts index 253f8ab6..c82f5a8a 100644 --- a/src/commands/salesforce/retrieveMetadataCommand.ts +++ b/src/commands/salesforce/retrieveMetadataCommand.ts @@ -25,7 +25,7 @@ export default class RetrieveMetadataCommand extends MetadataCommand { } // query available records - const components = await this.getExportableComponents(metadataType); + const components = await this.vlocode.withProgress(`Query ${metadataType.nameForMsgsPlural}...`, this.getExportableComponents(metadataType)); if (components.length == 0) { void vscode.window.showWarningMessage(`No exportable records for ${metadataType}`); return; @@ -37,14 +37,10 @@ export default class RetrieveMetadataCommand extends MetadataCommand { return; // selection cancelled; } - return this.retrieveMetadata([{ - fullname: this.getManifestName(metadataType, componentToExport), + return this.retrieveMetadata(componentToExport.map(item => ({ + fullname: item.fullName, componentType: metadataType.xmlName - }]); - } - - private getManifestName(metadataType: MetadataType, component: { fullName: string }) : string { - return component.fullName; + }))); } protected async getExportableObjectLikeTypes(nameFilter: RegExp) : Promise<{ fullName: string }[]> @@ -60,7 +56,7 @@ export default class RetrieveMetadataCommand extends MetadataCommand { })); } - protected async getExportableComponents(metadataType : MetadataType) : Promise<{ fullName: string }[]> { + protected async getExportableComponents(metadataType : MetadataType) : Promise { // query available records const connection = await this.salesforce.getJsForceConnection(); const components = await connection.metadata.list({ type: metadataType.xmlName }); @@ -147,20 +143,21 @@ export default class RetrieveMetadataCommand extends MetadataCommand { }); } - protected async showComponentSelection(records: T[]) : Promise { + protected async showComponentSelection(records: T[]) : Promise | undefined> { const objectOptions = records.map(record => ({ - label: record.label ?? record.fullName, - description: record.label ? record.fullName : undefined, + label: record.fullName, + description: `last modified: ${record.lastModifiedByName} (${record.lastModifiedDate})`, record })).sort((a, b) => a.label.localeCompare(b.label)); const objectSelection = await vscode.window.showQuickPick(objectOptions, { - placeHolder: 'Select metadata object to export' + placeHolder: 'Select metadata object to export', + canPickMany: true }); if (!objectSelection) { return; // selection cancelled; } - return objectSelection.record; + return objectSelection.map(item => item.record); } protected async showExportPathSelection() : Promise { diff --git a/src/extension.ts b/src/extension.ts index 37ce0013..84c79249 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -189,7 +189,6 @@ class Vlocode { private async deactivate() { // Log to debug as other output channels will be disposed this.service.dispose(); - console.debug('Vlocode extension deactivated'); } static activate(context: vscode.ExtensionContext) { @@ -206,5 +205,6 @@ export function activate(context: vscode.ExtensionContext) { } export function deactivate() { + console.log('deactivate'); return Vlocode.deactivate(); } \ No newline at end of file diff --git a/src/lib/vlocity/datapackInfoService.ts b/src/lib/vlocity/datapackInfoService.ts index ed39a3d7..71e3ffd4 100644 --- a/src/lib/vlocity/datapackInfoService.ts +++ b/src/lib/vlocity/datapackInfoService.ts @@ -82,7 +82,7 @@ export default class DatapackInfoService { const regex = new RegExp(`${removeNamespacePrefix(sobjectType)}`,'ig'); const datapackInfo = (await this.getDatapacks()).find(dataPack => dataPack.sobjectType && regex.test(removeNamespacePrefix(dataPack.sobjectType))); if (!datapackInfo) { - this.logger.warn(`No Datapack with SObject '${sobjectType}' configured in Salesforce (see VlocityDataPackConfiguration object)`); + this.logger.verbose(`No Datapack with SObject '${sobjectType}' configured in Salesforce (see VlocityDataPackConfiguration object)`); } return datapackInfo?.datapackType; } diff --git a/src/lib/vlocity/vlocityMatchingKeyService.ts b/src/lib/vlocity/vlocityMatchingKeyService.ts index 6aa0077f..faf68ef9 100644 --- a/src/lib/vlocity/vlocityMatchingKeyService.ts +++ b/src/lib/vlocity/vlocityMatchingKeyService.ts @@ -3,6 +3,7 @@ import { Logger , injectable } from '@vlocode/core'; import SalesforceService from '@lib/salesforce/salesforceService'; import { stringEquals , cache , removeNamespacePrefix } from '@vlocode/util'; +import * as ExportQueryDefinitions from 'exportQueryDefinitions.yaml'; import { VlocityNamespaceService } from './vlocityNamespaceService'; import DatapackInfoService from './datapackInfoService'; import { QueryDefinitions } from './types'; @@ -24,7 +25,6 @@ export default class VlocityMatchingKeyService { private readonly vlocityNamespace: VlocityNamespaceService, private readonly datapackInfo: DatapackInfoService, private readonly salesforce: SalesforceService) { - this.exportQueryDefinitions = require('exportQueryDefinitions.yaml')?.default ?? {}; } @cache(-1) @@ -33,7 +33,7 @@ export default class VlocityMatchingKeyService { } private get queryDefinitions() { - return this.exportQueryDefinitions; + return ExportQueryDefinitions; } /** @@ -61,11 +61,17 @@ export default class VlocityMatchingKeyService { sobject.fields.find(field => stringEquals(field.name, this.vlocityNamespace.updateNamespace(fieldName), true))?.type; // Append matching key fields - baseQuery += / where /gi.test(baseQuery) ? ' AND ' : ' WHERE '; - baseQuery += matchingKey.fields.filter(field => entry[field]) - .map(field => `${field} = ${this.formatValue(entry[field], getFieldType(field))}`).join(' and '); + if (matchingKey.fields.length) { + baseQuery += / where /gi.test(baseQuery) ? ' AND ' : ' WHERE '; + baseQuery += matchingKey.fields.filter(field => entry[field]) + .map(field => `${field} = ${this.formatValue(entry[field], getFieldType(field))}`).join(' and '); + } baseQuery += ' ORDER BY LastModifiedDate DESC LIMIT 1'; + if (!/ where /gi.test(baseQuery)) { + throw new Error(`The specified ${type} does not have a matching key`); + } + return baseQuery; } diff --git a/src/lib/vlocodeService.ts b/src/lib/vlocodeService.ts index 090ca5b4..2c42d623 100644 --- a/src/lib/vlocodeService.ts +++ b/src/lib/vlocodeService.ts @@ -66,6 +66,7 @@ export default class VlocodeService implements vscode.Disposable, JsForceConnect } public dispose() { + console.log('dispose service'); this.disposables.forEach(disposable => disposable.dispose()); this.disposables = []; if (this._datapackService) { diff --git a/src/lib/workspaceContextDetector.ts b/src/lib/workspaceContextDetector.ts index 595d7f04..f9787cb0 100644 --- a/src/lib/workspaceContextDetector.ts +++ b/src/lib/workspaceContextDetector.ts @@ -19,7 +19,7 @@ export interface FileFilterFunction { @injectable({ lifecycle: LifecyclePolicy.transient }) export class WorkspaceContextDetector implements vscode.Disposable { - private contextFiles = new Array(); + private contextFiles: { [file: string]: boolean } = {}; private workspaceFolderWatcher: vscode.Disposable; private workspaceFileWatcher: vscode.FileSystemWatcher; private scheduledContextUpdate?: NodeJS.Timeout; @@ -38,12 +38,13 @@ export class WorkspaceContextDetector implements vscode.Disposable { } public dispose() { - if (this.workspaceFolderWatcher) { - this.workspaceFolderWatcher.dispose(); - } - if (this.workspaceFileWatcher) { - this.workspaceFileWatcher.dispose(); + this.contextFiles = {}; + this.workspaceFolderWatcher?.dispose(); + this.workspaceFileWatcher?.dispose(); + if (this.scheduledContextUpdate) { + clearTimeout(this.scheduledContextUpdate); } + void vscode.commands.executeCommand('setContext', `${constants.CONTEXT_PREFIX}.${this.editorContextKey}`, null); } public async initialize() { @@ -52,7 +53,7 @@ export class WorkspaceContextDetector implements vscode.Disposable { this.remove(removeWorkspace.uri.fsPath); } for (const addedWorkspace of e.added) { - this.contextFiles.push(...await this.getApplicableFiles(addedWorkspace.uri.fsPath)); + this.add(await this.getApplicableFiles(addedWorkspace.uri.fsPath)); } this.scheduleContextUpdate(); }); @@ -60,28 +61,36 @@ export class WorkspaceContextDetector implements vscode.Disposable { this.workspaceFileWatcher = vscode.workspace.createFileSystemWatcher('**/*', false, true, false); this.workspaceFileWatcher.onDidCreate(async newFile => { const fsPath = newFile.fsPath; - const newFiles = new Array(); - if (await this.fs.isDirectory(fsPath)) { - newFiles.push(...await this.getApplicableFiles(fsPath)); - } else if (this.isApplicableFile(fsPath)) { - newFiles.push(fsPath); - } - - if (newFiles.length > 0) { - this.contextFiles.push(...newFiles); + const folderFiles = await this.getApplicableFiles(fsPath); + if (folderFiles.length) { + this.add(fsPath.split(/\\|\//).map((v,i,p) => [...p.slice(0, i), v].join(path.sep))); + this.add(folderFiles); + this.scheduleContextUpdate(); + } + } else if (this.isApplicableFile(fsPath) ) { + this.add(fsPath.split(/\\|\//).map((v,i,p) => [...p.slice(0, i), v].join(path.sep))); this.scheduleContextUpdate(); } }); - this.contextFiles.push(...await this.getApplicableFoldersInWorkspace()); + this.add(await this.getApplicableFoldersInWorkspace()); this.scheduleContextUpdate(); return this; } + private add(filePath: string | string[]) { + if (Array.isArray(filePath)) { + filePath.forEach(p => this.add(p)); + } else { + this.contextFiles[filePath] = true; + } + } + private remove(filePath: string) { - this.contextFiles = this.contextFiles.filter(file => file.startsWith(filePath)); + // eslint-disable-next-line @typescript-eslint/tslint/config + delete this.contextFiles[filePath]; } private scheduleContextUpdate() { @@ -94,8 +103,7 @@ export class WorkspaceContextDetector implements vscode.Disposable { private async updateContext() { this.scheduledContextUpdate = undefined; const timer = new Timer(); - const folders = this.contextFiles.reduce((map, fullPath) => Object.assign(map, { [fullPath]: true }), {}); - await vscode.commands.executeCommand('setContext', `${constants.CONTEXT_PREFIX}.${this.editorContextKey}`, folders); + await vscode.commands.executeCommand('setContext', `${constants.CONTEXT_PREFIX}.${this.editorContextKey}`, this.contextFiles); this.logger.verbose(`Updated context ${constants.CONTEXT_PREFIX}.${this.editorContextKey} [${timer.stop()}]`); } @@ -113,39 +121,25 @@ export class WorkspaceContextDetector implements vscode.Disposable { public async getApplicableFiles(folder: string) : Promise { const files = new Array(); - const dirEntries = await fs.readdir(folder, { withFileTypes: true }); - const hasApplicableFiles = dirEntries.some(entry => entry.isFile() && this.isApplicableFile(entry.name)); + const entries = await fs.readdir(folder, { withFileTypes: true }); - for (const entry of dirEntries) { + for (const entry of entries) { if (entry.name.startsWith('.') || entry.name == 'node_modules') { continue; } const fullPath = path.join(folder, entry.name); if (entry.isDirectory()) { files.push(...await this.getApplicableFiles(fullPath)); - } else if (hasApplicableFiles) { + } else if (this.isApplicableFile(entry.name)) { files.push(fullPath); } } - if (files.length > 0) { - // Include the parent folder when it has any files applicable + if (files.length) { + // Add folder when there are files in this folder files.push(folder); } return files; } - - public async isApplicableFolder(folder: vscode.Uri) { - try { - for (const [file, type] of await vscode.workspace.fs.readDirectory(folder)) { - if (type == vscode.FileType.File && this.isApplicableFile(file)) { - return true; - } - } - } catch(err) { - console.error(err); - } - return false; - } } \ No newline at end of file diff --git a/types/yaml-files.d.ts b/types/yaml-files.d.ts index 31de3742..49ad3f61 100644 --- a/types/yaml-files.d.ts +++ b/types/yaml-files.d.ts @@ -3,21 +3,22 @@ */ declare module 'exportQueryDefinitions.yaml' { interface DatapackQueryDefinition { - VlocityDataPackType: string - query: string - name: string - groupKey?: string - groupName?: string - groupDescription?: string - description?: string + VlocityDataPackType: string; + query: string; + name: string; + requiredSetting: string; + groupKey?: string; + groupName?: string; + groupDescription?: string; + description?: string; matchingKey?: { - fields: string[], - returnField?: string + fields: string[]; + returnField?: string; } salesforceUrl?: { - namespace?: string, - path: string - } | string + namespace?: string; + path: string; + } | string; } const exportQueryDefinitions : { [datapackType: string] : DatapackQueryDefinition