From 0e8c02d8868942a03215001935bdd2b045c84d84 Mon Sep 17 00:00:00 2001 From: Sam Gusick Date: Thu, 14 Aug 2025 23:30:12 -0400 Subject: [PATCH 01/18] bare bones functionality is working! --- src/connectionconfig/connectionconfig.ts | 217 +++++++++++++++--- .../objectExplorerDragAndDropController.ts | 28 ++- src/objectExplorer/objectExplorerService.ts | 8 +- .../common/searchableDropdown.component.tsx | 11 +- .../ConnectionDialog/connectionFormPage.tsx | 46 +++- src/sharedInterfaces/connectionDialog.ts | 4 +- 6 files changed, 265 insertions(+), 49 deletions(-) diff --git a/src/connectionconfig/connectionconfig.ts b/src/connectionconfig/connectionconfig.ts index 096ca457c2..9b6a1a9983 100644 --- a/src/connectionconfig/connectionconfig.ts +++ b/src/connectionconfig/connectionconfig.ts @@ -39,13 +39,68 @@ export class ConnectionConfig implements IConnectionConfig { void this.initialize(); } + public getUserConnectionsGroup(): IConnectionGroup | undefined { + const rootGroup = this.getRootGroup(); + if (!rootGroup) return undefined; + const groups = this.getGroupsFromSettings(); + return groups.find((g) => g.name === "User Connections" && g.parentId === rootGroup.id); + } + + public getWorkspaceConnectionsGroup(): IConnectionGroup | undefined { + const rootGroup = this.getRootGroup(); + if (!rootGroup) return undefined; + const groups = this.getGroupsFromSettings(); + return groups.find( + (g) => g.name === "Workspace Connections" && g.parentId === rootGroup.id, + ); + } + + public getUserConnectionsGroupId(): string | undefined { + const group = this.getUserConnectionsGroup(); + return group?.id; + } + + public getWorkspaceConnectionsGroupId(): string | undefined { + const group = this.getWorkspaceConnectionsGroup(); + return group?.id; + } + private async initialize(): Promise { + // Ensure workspace arrays exist + await this.ensureWorkspaceArraysInitialized(); await this.assignConnectionGroupMissingIds(); await this.assignConnectionMissingIds(); this.initialized.resolve(); } + private async ensureWorkspaceArraysInitialized(): Promise { + const workspaceGroups = this.getGroupsFromSettings(ConfigurationTarget.Workspace); + const workspaceConnections = this.getConnectionsFromSettings(ConfigurationTarget.Workspace); + let changed = false; + if (!workspaceGroups || workspaceGroups.length === 0) { + await this._vscodeWrapper.setConfiguration( + Constants.extensionName, + Constants.connectionGroupsArrayName, + [], + ConfigurationTarget.Workspace, + ); + changed = true; + } + if (!workspaceConnections || workspaceConnections.length === 0) { + await this._vscodeWrapper.setConfiguration( + Constants.extensionName, + Constants.connectionsArrayName, + [], + ConfigurationTarget.Workspace, + ); + changed = true; + } + if (changed) { + this._logger.logDebug("Initialized workspace arrays for connections and groups."); + } + } + //#region Connection Profiles /** @@ -139,18 +194,26 @@ export class ConnectionConfig implements IConnectionConfig { return profiles.find((profile) => profile.id === id); } - public async addConnection(profile: IConnectionProfile): Promise { + public async addConnection( + profile: IConnectionProfile, + target: ConfigTarget = ConfigurationTarget.Global, + ): Promise { this.populateMissingConnectionIds(profile); - let profiles = await this.getConnections(false /* getWorkspaceConnections */); + // If the group is Workspace Connections, always use workspace settings + const workspaceGroupId = this.getWorkspaceConnectionsGroupId(); + if (profile.groupId === workspaceGroupId) { + target = ConfigurationTarget.Workspace; + } + + let profiles = this.getConnectionsFromSettings(target); // Remove the profile if already set profiles = profiles.filter((value) => !Utils.isSameProfile(value, profile)); profiles.push(profile); - return await this.writeConnectionsToSettings(profiles); + return await this.writeConnectionsToSettings(profiles, target); } - /** * Remove an existing connection from the connection config if it exists. * @returns true if the connection was removed, false if the connection wasn't found. @@ -166,13 +229,25 @@ export class ConnectionConfig implements IConnectionConfig { } public async updateConnection(updatedProfile: IConnectionProfile): Promise { - const profiles = await this.getConnections(false /* getWorkspaceConnections */); + return this.updateConnectionWithTarget(updatedProfile, ConfigurationTarget.Global); + } + + public async updateConnectionWithTarget( + updatedProfile: IConnectionProfile, + target: ConfigTarget, + ): Promise { + // If the group is Workspace Connections, always use workspace settings + const workspaceGroupId = this.getWorkspaceConnectionsGroupId(); + if (updatedProfile.groupId === workspaceGroupId) { + target = ConfigurationTarget.Workspace; + } + const profiles = this.getConnectionsFromSettings(target); const index = profiles.findIndex((p) => p.id === updatedProfile.id); if (index === -1) { throw new Error(`Connection with ID ${updatedProfile.id} not found`); } profiles[index] = updatedProfile; - await this.writeConnectionsToSettings(profiles); + await this.writeConnectionsToSettings(profiles, target); } //#endregion @@ -219,13 +294,20 @@ export class ConnectionConfig implements IConnectionConfig { group.id = Utils.generateGuid(); } + // If this is Workspace Connections or a child, use workspace settings + const workspaceGroupId = this.getWorkspaceConnectionsGroupId(); + let target: ConfigTarget = ConfigurationTarget.Global; + if (group.parentId === workspaceGroupId || group.id === workspaceGroupId) { + target = ConfigurationTarget.Workspace; + } + if (!group.parentId) { - group.parentId = this.getRootGroup().id; + group.parentId = this.getUserConnectionsGroupId(); } - const groups = this.getGroupsFromSettings(); + const groups = this.getGroupsFromSettings(target); groups.push(group); - return this.writeConnectionGroupsToSettings(groups); + return this.writeConnectionGroupsToSettingsWithTarget(groups, target); } /** @@ -283,14 +365,15 @@ export class ConnectionConfig implements IConnectionConfig { // Remove all groups that were marked for removal remainingGroups = groups.filter((g) => !groupsToRemove.has(g.id)); } else { - // Move immediate child connections to root + // Move immediate child connections and groups to User Connections group + const userGroupId = this.getUserConnectionsGroupId(); remainingConnections = connections.map((conn) => { if (conn.groupId === id) { this._logger.verbose( - `Moving connection '${conn.id}' to root group because its immediate parent group '${id}' was removed`, + `Moving connection '${conn.id}' to User Connections group because its immediate parent group '${id}' was removed`, ); connectionModified = true; - return { ...conn, groupId: rootGroup.id }; + return { ...conn, groupId: userGroupId }; } return conn; }); @@ -298,13 +381,13 @@ export class ConnectionConfig implements IConnectionConfig { // First remove the target group remainingGroups = groups.filter((g) => g.id !== id); - // Then reparent immediate children to root + // Then reparent immediate children to User Connections group remainingGroups = remainingGroups.map((g) => { if (g.parentId === id) { this._logger.verbose( - `Moving group '${g.id}' to root group because its immediate parent group '${id}' was removed`, + `Moving group '${g.id}' to User Connections group because its immediate parent group '${id}' was removed`, ); - return { ...g, parentId: rootGroup.id }; + return { ...g, parentId: userGroupId }; } return g; }); @@ -324,7 +407,19 @@ export class ConnectionConfig implements IConnectionConfig { } public async updateGroup(updatedGroup: IConnectionGroup): Promise { - const groups = this.getGroupsFromSettings(); + return this.updateGroupWithTarget(updatedGroup, ConfigurationTarget.Global); + } + + public async updateGroupWithTarget( + updatedGroup: IConnectionGroup, + target: ConfigTarget, + ): Promise { + // If this is Workspace Connections or a child, use workspace settings + const workspaceGroupId = this.getWorkspaceConnectionsGroupId(); + if (updatedGroup.parentId === workspaceGroupId || updatedGroup.id === workspaceGroupId) { + target = ConfigurationTarget.Workspace; + } + const groups = this.getGroupsFromSettings(target); const index = groups.findIndex((g) => g.id === updatedGroup.id); if (index === -1) { throw Error(`Connection group with ID ${updatedGroup.id} not found when updating`); @@ -332,7 +427,7 @@ export class ConnectionConfig implements IConnectionConfig { groups[index] = updatedGroup; } - return await this.writeConnectionGroupsToSettings(groups); + return await this.writeConnectionGroupsToSettingsWithTarget(groups, target); } //#endregion @@ -378,9 +473,9 @@ export class ConnectionConfig implements IConnectionConfig { // ensure each profile is in a group if (profile.groupId === undefined) { - const rootGroup = this.getRootGroup(); - if (rootGroup) { - profile.groupId = rootGroup.id; + const userGroupId = this.getUserConnectionsGroupId(); + if (userGroupId) { + profile.groupId = userGroupId; modified = true; } } @@ -401,39 +496,71 @@ export class ConnectionConfig implements IConnectionConfig { private async assignConnectionGroupMissingIds(): Promise { let madeChanges = false; const groups: IConnectionGroup[] = this.getGroupsFromSettings(); + let connections: IConnectionProfile[] = this.getConnectionsFromSettings(); // ensure ROOT group exists let rootGroup = await this.getRootGroup(); - if (!rootGroup) { rootGroup = { name: ConnectionConfig.RootGroupName, id: Utils.generateGuid(), }; - this._logger.logDebug(`Adding missing ROOT group to connection groups`); madeChanges = true; groups.push(rootGroup); } - // Clean up connection groups - for (const group of groups) { - if (group.id === rootGroup.id) { - continue; - } + // Check for User Connections and Workspace Connections under ROOT + let userConnectionsGroup = groups.find( + (g) => g.name === "User Connections" && g.parentId === rootGroup.id, + ); + let workspaceConnectionsGroup = groups.find( + (g) => g.name === "Workspace Connections" && g.parentId === rootGroup.id, + ); - // ensure each group has an ID - if (!group.id) { - group.id = Utils.generateGuid(); + if (!userConnectionsGroup) { + userConnectionsGroup = { + name: "User Connections", + id: Utils.generateGuid(), + parentId: rootGroup.id, + }; + groups.push(userConnectionsGroup); + madeChanges = true; + this._logger.logDebug(`Created 'User Connections' group under ROOT`); + } + if (!workspaceConnectionsGroup) { + workspaceConnectionsGroup = { + name: "Workspace Connections", + id: Utils.generateGuid(), + parentId: rootGroup.id, + }; + groups.push(workspaceConnectionsGroup); + madeChanges = true; + this._logger.logDebug(`Created 'Workspace Connections' group under ROOT`); + } + + // Reparent all groups directly under ROOT (except the two new groups) to User Connections + for (const group of groups) { + if ( + group.parentId === rootGroup.id && + group.id !== userConnectionsGroup.id && + group.id !== workspaceConnectionsGroup.id + ) { + group.parentId = userConnectionsGroup.id; madeChanges = true; - this._logger.logDebug(`Adding missing ID to connection group '${group.name}'`); + this._logger.logDebug(`Reparented group '${group.name}' to 'User Connections'`); } + } - // ensure each group is in a group - if (!group.parentId) { - group.parentId = rootGroup.id; + // Reparent all connections directly under ROOT to User Connections + for (const conn of connections) { + // If connection is under ROOT or has no group, move to User Connections + if (!conn.groupId || conn.groupId === rootGroup.id) { + conn.groupId = userConnectionsGroup.id; madeChanges = true; - this._logger.logDebug(`Adding missing parentId to connection '${group.name}'`); + this._logger.logDebug( + `Reparented connection '${getConnectionDisplayName(conn)}' to 'User Connections'`, + ); } } @@ -442,8 +569,8 @@ export class ConnectionConfig implements IConnectionConfig { this._logger.logDebug( `Updates made to connection groups. Writing all ${groups.length} group(s) to settings.`, ); - await this.writeConnectionGroupsToSettings(groups); + await this.writeConnectionsToSettings(connections); } } @@ -504,20 +631,34 @@ export class ConnectionConfig implements IConnectionConfig { * Replace existing profiles in the user settings with a new set of profiles. * @param profiles the set of profiles to insert into the settings file. */ - private async writeConnectionsToSettings(profiles: IConnectionProfile[]): Promise { - // Save the file + private async writeConnectionsToSettings( + profiles: IConnectionProfile[], + target: ConfigTarget = ConfigurationTarget.Global, + ): Promise { await this._vscodeWrapper.setConfiguration( Constants.extensionName, Constants.connectionsArrayName, profiles, + target, ); } private async writeConnectionGroupsToSettings(connGroups: IConnectionGroup[]): Promise { + return this.writeConnectionGroupsToSettingsWithTarget( + connGroups, + ConfigurationTarget.Global, + ); + } + + private async writeConnectionGroupsToSettingsWithTarget( + connGroups: IConnectionGroup[], + target: ConfigTarget, + ): Promise { await this._vscodeWrapper.setConfiguration( Constants.extensionName, Constants.connectionGroupsArrayName, connGroups, + target, ); } diff --git a/src/objectExplorer/objectExplorerDragAndDropController.ts b/src/objectExplorer/objectExplorerDragAndDropController.ts index ea3fb3593c..ce2d6047e2 100644 --- a/src/objectExplorer/objectExplorerDragAndDropController.ts +++ b/src/objectExplorer/objectExplorerDragAndDropController.ts @@ -105,12 +105,25 @@ export class ObjectExplorerDragAndDropController `Dragged ${dragData.type} '${dragData.name}' (ID: ${dragData.id}) onto group '${targetInfo.label}' (ID: ${targetInfo.id})`, ); + const workspaceGroupId = + this.connectionStore.connectionConfig.getWorkspaceConnectionsGroupId(); if (dragData.type === "connection") { const conn = await this.connectionStore.connectionConfig.getConnectionById( dragData.id, ); conn.groupId = targetInfo.id; - await this.connectionStore.connectionConfig.updateConnection(conn); + // If moving to Workspace Connections, store in workspace settings + if (targetInfo.id === workspaceGroupId) { + await this.connectionStore.connectionConfig.updateConnectionWithTarget( + conn, + vscode.ConfigurationTarget.Workspace, + ); + } else { + await this.connectionStore.connectionConfig.updateConnectionWithTarget( + conn, + vscode.ConfigurationTarget.Global, + ); + } } else { const group = this.connectionStore.connectionConfig.getGroupById( dragData.id, @@ -122,7 +135,18 @@ export class ObjectExplorerDragAndDropController } group.parentId = targetInfo.id; - await this.connectionStore.connectionConfig.updateGroup(group); + // If moving to Workspace Connections, store in workspace settings + if (targetInfo.id === workspaceGroupId) { + await this.connectionStore.connectionConfig.updateGroupWithTarget( + group, + vscode.ConfigurationTarget.Workspace, + ); + } else { + await this.connectionStore.connectionConfig.updateGroupWithTarget( + group, + vscode.ConfigurationTarget.Global, + ); + } } sendActionEvent(TelemetryViews.ObjectExplorer, TelemetryActions.DragAndDrop, { diff --git a/src/objectExplorer/objectExplorerService.ts b/src/objectExplorer/objectExplorerService.ts index 9a7a15f984..15bef533ce 100644 --- a/src/objectExplorer/objectExplorerService.ts +++ b/src/objectExplorer/objectExplorerService.ts @@ -90,8 +90,12 @@ export class ObjectExplorerService { return []; } - for (const child of this._connectionGroupNodes.get(rootId)?.children || []) { - result.push(child); + // Only show 'User Connections' and 'Workspace Connections' as children of ROOT + const rootChildren = this._connectionGroupNodes.get(rootId)?.children || []; + for (const child of rootChildren) { + if (child.label === "User Connections" || child.label === "Workspace Connections") { + result.push(child); + } } return result; diff --git a/src/reactviews/common/searchableDropdown.component.tsx b/src/reactviews/common/searchableDropdown.component.tsx index 8dc95e5873..82b6231b6c 100644 --- a/src/reactviews/common/searchableDropdown.component.tsx +++ b/src/reactviews/common/searchableDropdown.component.tsx @@ -138,9 +138,7 @@ const searchOptions = (text: string, items: SearchableDropdownOptions[]) => { export const SearchableDropdown = (props: SearchableDropdownProps) => { const [searchText, setSearchText] = useState(""); const [selectedOption, setSelectedOption] = useState( - props.selectedOption ?? { - value: "", - }, + props.selectedOption ?? props.options[0] ?? { value: "", text: props.placeholder ?? "" }, ); const id = props.id ?? useId(); @@ -296,11 +294,14 @@ export const SearchableDropdown = (props: SearchableDropdownProps) => { }, [buttonRef.current]); useEffect(() => { - setSelectedOption(props.selectedOption ?? props.options[0]); + setSelectedOption( + props.selectedOption ?? + props.options[0] ?? { value: "", text: props.placeholder ?? "" }, + ); setSelectedOptionIndex( props.options.findIndex((opt) => opt.value === props.selectedOption?.value), ); - }, [props.selectedOption]); + }, [props.selectedOption, props.options, props.placeholder]); return ( { return undefined; } + // Helper to flatten group hierarchy for dropdown + function getGroupOptions(): SearchableDropdownOptions[] { + if (!context?.state?.connectionGroups) return []; + // Recursively build hierarchical options, skipping ROOT + function buildOptions( + groups: IConnectionGroup[], + parentId?: string, + prefix: string = "", + ): SearchableDropdownOptions[] { + return groups + .filter((g) => g.parentId === parentId && g.name !== "ROOT") + .flatMap((g) => { + const label = prefix ? `${prefix} / ${g.name}` : g.name; + const children = buildOptions(groups, g.id, label); + return [{ key: g.id, text: label, value: g.id }, ...children]; + }); + } + + return buildOptions(context.state.connectionGroups); + } + + // Selected group state + const [selectedGroup, setSelectedGroup] = useState(getGroupOptions()[0]?.value ?? ""); + return (
+ {/* Connection Group Dropdown */} +
+ + o.value === selectedGroup)} + onSelect={(option: SearchableDropdownOptions) => setSelectedGroup(option.value)} + placeholder="Select a group" + /> +
+ {/* Existing connection form fields */} {context.state.connectionComponents.mainOptions.map((inputName, idx) => { const component = context.state.formComponents[inputName as keyof IConnectionDialogProfile]; if (component?.hidden !== false) { return undefined; } - return ( Date: Fri, 15 Aug 2025 09:54:06 -0400 Subject: [PATCH 02/18] workspace connection groups now show --- .../connectionGroupWebviewController.ts | 86 +++++++++++-------- src/objectExplorer/objectExplorerService.ts | 63 +++++--------- .../connectionGroup.component.tsx | 15 +++- src/sharedInterfaces/connectionGroup.ts | 3 + 4 files changed, 90 insertions(+), 77 deletions(-) diff --git a/src/controllers/connectionGroupWebviewController.ts b/src/controllers/connectionGroupWebviewController.ts index 3d21a07369..e297ce9b73 100644 --- a/src/controllers/connectionGroupWebviewController.ts +++ b/src/controllers/connectionGroupWebviewController.ts @@ -77,44 +77,58 @@ export class ConnectionGroupWebviewController extends ReactWebviewPanelControlle return state; }); - this.registerReducer("saveConnectionGroup", async (state, payload) => { - try { - if (this.connectionGroupToEdit) { - this.logger.verbose("Updating existing connection group", payload); - await this.connectionConfig.updateGroup({ - ...this.connectionGroupToEdit, - name: payload.name, - description: payload.description, - color: payload.color, - }); - } else { - this.logger.verbose("Creating new connection group", payload); - await this.connectionConfig.addGroup(createConnectionGroupFromSpec(payload)); - } + this.registerReducer( + "saveConnectionGroup", + async (state, payload: ConnectionGroupSpec & { scope: "user" | "workspace" }) => { + try { + if (this.connectionGroupToEdit) { + this.logger.verbose("Updating existing connection group", payload); + // Only update name, description, color; parentId and scope are not editable for existing groups + await this.connectionConfig.updateGroup({ + ...this.connectionGroupToEdit, + name: payload.name, + description: payload.description, + color: payload.color, + }); + } else { + this.logger.verbose("Creating new connection group", payload); + // Set parentId based on scope + let parentId: string | undefined; + if (payload.scope === "workspace") { + parentId = this.connectionConfig.getWorkspaceConnectionsGroupId(); + } else { + parentId = this.connectionConfig.getUserConnectionsGroupId(); + } + const groupSpec = { ...payload, parentId }; + await this.connectionConfig.addGroup( + createConnectionGroupFromSpec(groupSpec), + ); + } - sendActionEvent( - TelemetryViews.ConnectionGroup, - TelemetryActions.SaveConnectionGroup, - { newOrEdit: this.connectionGroupToEdit ? "edit" : "new" }, - ); + sendActionEvent( + TelemetryViews.ConnectionGroup, + TelemetryActions.SaveConnectionGroup, + { newOrEdit: this.connectionGroupToEdit ? "edit" : "new" }, + ); - this.dialogResult.resolve(true); - await this.panel.dispose(); - } catch (err) { - state.message = getErrorMessage(err); - sendErrorEvent( - TelemetryViews.ConnectionGroup, - TelemetryActions.SaveConnectionGroup, - err, - true, // includeErrorMessage - undefined, // errorCode - undefined, // errorType - { newOrEdit: this.connectionGroupToEdit ? "edit" : "new" }, - ); - } + this.dialogResult.resolve(true); + await this.panel.dispose(); + } catch (err) { + state.message = getErrorMessage(err); + sendErrorEvent( + TelemetryViews.ConnectionGroup, + TelemetryActions.SaveConnectionGroup, + err, + true, // includeErrorMessage + undefined, // errorCode + undefined, // errorType + { newOrEdit: this.connectionGroupToEdit ? "edit" : "new" }, + ); + } - return state; - }); + return state; + }, + ); } } @@ -124,6 +138,8 @@ export function createConnectionGroupFromSpec(spec: ConnectionGroupState): IConn description: spec.description, color: spec.color, id: Utils.generateGuid(), + parentId: + "parentId" in spec && spec.parentId !== undefined ? String(spec.parentId) : undefined, }; } diff --git a/src/objectExplorer/objectExplorerService.ts b/src/objectExplorer/objectExplorerService.ts index 15bef533ce..b04f48f7c9 100644 --- a/src/objectExplorer/objectExplorerService.ts +++ b/src/objectExplorer/objectExplorerService.ts @@ -82,7 +82,6 @@ export class ObjectExplorerService { const result = []; const rootId = this._connectionManager.connectionStore.rootGroupId; - if (!this._connectionGroupNodes.has(rootId)) { this._logger.verbose( "Root server group is not defined. Cannot get root nodes for Object Explorer.", @@ -90,13 +89,12 @@ export class ObjectExplorerService { return []; } - // Only show 'User Connections' and 'Workspace Connections' as children of ROOT + // Always show both User Connections and Workspace Connections as children of ROOT const rootChildren = this._connectionGroupNodes.get(rootId)?.children || []; - for (const child of rootChildren) { - if (child.label === "User Connections" || child.label === "Workspace Connections") { - result.push(child); - } - } + let userGroup = rootChildren.find((child) => child.label === "User Connections"); + let workspaceGroup = rootChildren.find((child) => child.label === "Workspace Connections"); + if (userGroup) result.push(userGroup); + if (workspaceGroup) result.push(workspaceGroup); return result; } @@ -372,16 +370,22 @@ export class ObjectExplorerService { ); const rootId = this._connectionManager.connectionStore.rootGroupId; - const serverGroups = - await this._connectionManager.connectionStore.readAllConnectionGroups(); + // Read user and workspace groups separately + const userGroups = + this._connectionManager.connectionStore.connectionConfig.getGroupsFromSettings(); + // Import ConfigurationTarget from the correct module + // Use VscodeWrapper.ConfigurationTarget.Workspace + const { ConfigurationTarget } = require("../controllers/vscodeWrapper"); + const workspaceGroups = + this._connectionManager.connectionStore.connectionConfig.getGroupsFromSettings( + ConfigurationTarget.Workspace, + ); + // Merge root, user, and workspace groups + const allGroups = [...userGroups, ...workspaceGroups]; let savedConnections = await this._connectionManager.connectionStore.readAllConnections(); // if there are no saved connections, show the add connection node - if ( - savedConnections.length === 0 && - serverGroups.length === 1 && - serverGroups[0].id === rootId - ) { + if (savedConnections.length === 0 && allGroups.length === 1 && allGroups[0].id === rootId) { this._logger.verbose( "No saved connections or groups found. Showing add connection node.", ); @@ -394,45 +398,27 @@ export class ObjectExplorerService { const newConnectionGroupNodes = new Map(); const newConnectionNodes = new Map(); - // Add all group nodes from settings first - // Read the user setting for collapsed/expanded state - const config = vscode.workspace.getConfiguration(Constants.extensionName); - const collapseGroups = config.get( - Constants.cmdObjectExplorerCollapseOrExpandByDefault, - false, - ); - - for (const group of serverGroups) { - // Pass the desired collapsible state to the ConnectionGroupNode constructor - const initialState = collapseGroups - ? vscode.TreeItemCollapsibleState.Collapsed - : vscode.TreeItemCollapsibleState.Expanded; - const groupNode = new ConnectionGroupNode(group, initialState); - + // Add all group nodes from merged settings + for (const group of allGroups) { + const groupNode = new ConnectionGroupNode(group); if (this._connectionGroupNodes.has(group.id)) { groupNode.id = this._connectionGroupNodes.get(group.id).id; } - newConnectionGroupNodes.set(group.id, groupNode); } // Populate group hierarchy - add each group as a child to its parent - for (const group of serverGroups) { + for (const group of allGroups) { // Skip the root group as it has no parent if (group.id === rootId) { continue; } - if (group.parentId && newConnectionGroupNodes.has(group.parentId)) { const parentNode = newConnectionGroupNodes.get(group.parentId); const childNode = newConnectionGroupNodes.get(group.id); - if (parentNode && childNode) { parentNode.addChild(childNode); - if (parentNode.id !== rootId) { - // set the parent node for the child group unless the parent is the root group - // parent property is used to childNode.parentNode = parentNode; } } else { @@ -451,9 +437,7 @@ export class ObjectExplorerService { for (const connection of savedConnections) { if (connection.groupId && newConnectionGroupNodes.has(connection.groupId)) { const groupNode = newConnectionGroupNodes.get(connection.groupId); - let connectionNode: ConnectionNode; - if (this._connectionNodes.has(connection.id)) { connectionNode = this._connectionNodes.get(connection.id); connectionNode.updateConnectionProfile(connection); @@ -464,9 +448,7 @@ export class ObjectExplorerService { groupNode.id === rootId ? undefined : groupNode, ); } - connectionNode.parentNode = groupNode.id === rootId ? undefined : groupNode; - newConnectionNodes.set(connection.id, connectionNode); groupNode.addChild(connectionNode); } else { @@ -480,7 +462,6 @@ export class ObjectExplorerService { this._connectionNodes = newConnectionNodes; const result = [...this._rootTreeNodeArray]; - getConnectionActivity.end(ActivityStatus.Succeeded, undefined, { nodeCount: result.length, }); diff --git a/src/reactviews/pages/ConnectionGroup/connectionGroup.component.tsx b/src/reactviews/pages/ConnectionGroup/connectionGroup.component.tsx index b146442225..fb9d5f2795 100644 --- a/src/reactviews/pages/ConnectionGroup/connectionGroup.component.tsx +++ b/src/reactviews/pages/ConnectionGroup/connectionGroup.component.tsx @@ -99,6 +99,7 @@ export const ConnectionGroupDialog = ({ const [color, setColor] = useState(intialHsvColor); const [pickerColor, setPickerColor] = useState(intialHsvColor); const [popoverOpen, setPopoverOpen] = useState(false); + const [scope, setScope] = useState(state.scope || "user"); const handleChange: ColorPickerProps["onColorChange"] = (_, data) => { setColor({ ...data.color, a: 1 }); @@ -117,6 +118,7 @@ export const ConnectionGroupDialog = ({ color: new TinyColor(color).toHexString(false /* allow3Char */).toUpperCase() || undefined, + scope, }); } } @@ -146,6 +148,17 @@ export const ConnectionGroupDialog = ({
)}{" "} + + + - {" "} + diff --git a/src/sharedInterfaces/connectionGroup.ts b/src/sharedInterfaces/connectionGroup.ts index 9e42ef0f5c..101efa6241 100644 --- a/src/sharedInterfaces/connectionGroup.ts +++ b/src/sharedInterfaces/connectionGroup.ts @@ -22,6 +22,7 @@ export interface IConnectionGroup { parentId?: string; color?: string; description?: string; + scope?: "user" | "workspace"; } /** @@ -41,6 +42,7 @@ export interface ConnectionGroupState { description?: string; color?: string; message?: string; + scope?: "user" | "workspace"; } /** @@ -68,4 +70,5 @@ export interface ConnectionGroupSpec { name: string; description?: string; color?: string; + scope: "user" | "workspace"; } From 2d0ffe2d65233b4146dc70ac3eb31dd019a2440d Mon Sep 17 00:00:00 2001 From: Sam Gusick Date: Mon, 18 Aug 2025 18:00:32 -0400 Subject: [PATCH 03/18] Deletion of workspace connections implemented --- src/connectionconfig/connectionconfig.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/connectionconfig/connectionconfig.ts b/src/connectionconfig/connectionconfig.ts index 9b6a1a9983..c3b38135c2 100644 --- a/src/connectionconfig/connectionconfig.ts +++ b/src/connectionconfig/connectionconfig.ts @@ -219,11 +219,17 @@ export class ConnectionConfig implements IConnectionConfig { * @returns true if the connection was removed, false if the connection wasn't found. */ public async removeConnection(profile: IConnectionProfile): Promise { - let profiles = await this.getConnections(false /* getWorkspaceConnections */); + // Determine if this is a workspace connection + const workspaceGroupId = this.getWorkspaceConnectionsGroupId(); + let target = ConfigurationTarget.Global; + if (profile.groupId === workspaceGroupId) { + target = ConfigurationTarget.Workspace; + } + let profiles = this.getConnectionsFromSettings(target); const found = this.removeConnectionHelper(profile, profiles); if (found) { - await this.writeConnectionsToSettings(profiles); + await this.writeConnectionsToSettings(profiles, target); } return found; } From e33a6c35de8590367d5968f93292ea1154002b25 Mon Sep 17 00:00:00 2001 From: Sam Gusick Date: Wed, 20 Aug 2025 10:15:04 -0400 Subject: [PATCH 04/18] More experimenting --- .../connectionDialogWebviewController.ts | 4 + src/connectionconfig/connectionconfig.ts | 94 ++++++++++++------- src/models/connectionStore.ts | 15 ++- src/objectExplorer/objectExplorerService.ts | 50 +++++----- src/views/connectionUI.ts | 20 ++-- 5 files changed, 116 insertions(+), 67 deletions(-) diff --git a/src/connectionconfig/connectionDialogWebviewController.ts b/src/connectionconfig/connectionDialogWebviewController.ts index c6a0cc3cce..3451d24768 100644 --- a/src/connectionconfig/connectionDialogWebviewController.ts +++ b/src/connectionconfig/connectionDialogWebviewController.ts @@ -1076,6 +1076,10 @@ export class ConnectionDialogWebviewController extends FormWebviewController< // eslint-disable-next-line @typescript-eslint/no-explicit-any cleanedConnection as any, ); + // Refresh the Object Explorer tree to include new connections/groups + if (self._objectExplorerProvider?.objectExplorerService?.refreshTree) { + await self._objectExplorerProvider.objectExplorerService.refreshTree(); + } const node = await self._mainController.createObjectExplorerSession(cleanedConnection); await self.updateLoadedConnections(state); diff --git a/src/connectionconfig/connectionconfig.ts b/src/connectionconfig/connectionconfig.ts index c3b38135c2..4ec8bc2aa0 100644 --- a/src/connectionconfig/connectionconfig.ts +++ b/src/connectionconfig/connectionconfig.ts @@ -9,6 +9,8 @@ import * as Utils from "../models/utils"; import { IConnectionGroup, IConnectionProfile } from "../models/interfaces"; import { IConnectionConfig } from "./iconnectionconfig"; import VscodeWrapper, { ConfigurationTarget } from "../controllers/vscodeWrapper"; + +export { ConfigurationTarget }; import { ConnectionProfile } from "../models/connectionProfile"; import { getConnectionDisplayName } from "../models/connectionInfo"; import { Deferred } from "../protocol"; @@ -291,8 +293,11 @@ export class ConnectionConfig implements IConnectionConfig { * @returns The connection group with the specified ID, or `undefined` if not found. */ public getGroupById(id: string): IConnectionGroup | undefined { - const connGroups = this.getGroupsFromSettings(ConfigurationTarget.Global); - return connGroups.find((g) => g.id === id); + // Search both user and workspace groups for the given ID + const userGroups = this.getGroupsFromSettings(ConfigurationTarget.Global); + const workspaceGroups = this.getGroupsFromSettings(ConfigurationTarget.Workspace); + const allGroups = [...userGroups, ...workspaceGroups]; + return allGroups.find((g) => g.id === id); } public addGroup(group: IConnectionGroup): Promise { @@ -501,82 +506,101 @@ export class ConnectionConfig implements IConnectionConfig { private async assignConnectionGroupMissingIds(): Promise { let madeChanges = false; - const groups: IConnectionGroup[] = this.getGroupsFromSettings(); - let connections: IConnectionProfile[] = this.getConnectionsFromSettings(); + // User groups and connections + const userGroups: IConnectionGroup[] = this.getGroupsFromSettings( + ConfigurationTarget.Global, + ); + let userConnections: IConnectionProfile[] = this.getConnectionsFromSettings( + ConfigurationTarget.Global, + ); + // Workspace groups and connections + const workspaceGroups: IConnectionGroup[] = this.getGroupsFromSettings( + ConfigurationTarget.Workspace, + ); + let workspaceConnections: IConnectionProfile[] = this.getConnectionsFromSettings( + ConfigurationTarget.Workspace, + ); - // ensure ROOT group exists - let rootGroup = await this.getRootGroup(); + // ensure ROOT group exists in user settings + let rootGroup = userGroups.find((g) => g.name === ConnectionConfig.RootGroupName); if (!rootGroup) { rootGroup = { name: ConnectionConfig.RootGroupName, id: Utils.generateGuid(), }; - this._logger.logDebug(`Adding missing ROOT group to connection groups`); + userGroups.push(rootGroup); madeChanges = true; - groups.push(rootGroup); + this._logger.logDebug(`Adding missing ROOT group to user connection groups`); } - // Check for User Connections and Workspace Connections under ROOT - let userConnectionsGroup = groups.find( + // Ensure User Connections group exists in user settings + let userConnectionsGroup = userGroups.find( (g) => g.name === "User Connections" && g.parentId === rootGroup.id, ); - let workspaceConnectionsGroup = groups.find( - (g) => g.name === "Workspace Connections" && g.parentId === rootGroup.id, - ); - if (!userConnectionsGroup) { userConnectionsGroup = { name: "User Connections", id: Utils.generateGuid(), parentId: rootGroup.id, }; - groups.push(userConnectionsGroup); + userGroups.push(userConnectionsGroup); madeChanges = true; this._logger.logDebug(`Created 'User Connections' group under ROOT`); } + + // Ensure Workspace Connections group exists in workspace settings, parented to ROOT (user) + let workspaceConnectionsGroup = workspaceGroups.find( + (g) => g.name === "Workspace Connections" && g.parentId === rootGroup.id, + ); if (!workspaceConnectionsGroup) { workspaceConnectionsGroup = { name: "Workspace Connections", id: Utils.generateGuid(), parentId: rootGroup.id, }; - groups.push(workspaceConnectionsGroup); + workspaceGroups.push(workspaceConnectionsGroup); madeChanges = true; - this._logger.logDebug(`Created 'Workspace Connections' group under ROOT`); + this._logger.logDebug(`Created 'Workspace Connections' group under ROOT (user)`); } - // Reparent all groups directly under ROOT (except the two new groups) to User Connections - for (const group of groups) { - if ( - group.parentId === rootGroup.id && - group.id !== userConnectionsGroup.id && - group.id !== workspaceConnectionsGroup.id - ) { - group.parentId = userConnectionsGroup.id; + // Reparent all workspace groups directly under ROOT to Workspace Connections group + for (const group of workspaceGroups) { + if (group.parentId === rootGroup.id && group.id !== workspaceConnectionsGroup.id) { + group.parentId = workspaceConnectionsGroup.id; madeChanges = true; - this._logger.logDebug(`Reparented group '${group.name}' to 'User Connections'`); + this._logger.logDebug( + `Reparented workspace group '${group.name}' to 'Workspace Connections'`, + ); } } - // Reparent all connections directly under ROOT to User Connections - for (const conn of connections) { - // If connection is under ROOT or has no group, move to User Connections + // Reparent all workspace connections directly under ROOT to Workspace Connections group + for (const conn of workspaceConnections) { if (!conn.groupId || conn.groupId === rootGroup.id) { - conn.groupId = userConnectionsGroup.id; + conn.groupId = workspaceConnectionsGroup.id; madeChanges = true; this._logger.logDebug( - `Reparented connection '${getConnectionDisplayName(conn)}' to 'User Connections'`, + `Reparented workspace connection '${getConnectionDisplayName(conn)}' to 'Workspace Connections'`, ); } } - // Save the changes to settings + // Save changes to settings if (madeChanges) { + this._logger.logDebug(`Writing updated user groups and connections to user settings.`); + await this.writeConnectionGroupsToSettings(userGroups); + await this.writeConnectionsToSettings(userConnections); this._logger.logDebug( - `Updates made to connection groups. Writing all ${groups.length} group(s) to settings.`, + `Writing updated workspace groups and connections to workspace settings.`, + ); + await this.writeConnectionGroupsToSettingsWithTarget( + workspaceGroups, + ConfigurationTarget.Workspace, + ); + await this.writeConnectionsToSettings( + workspaceConnections, + ConfigurationTarget.Workspace, ); - await this.writeConnectionGroupsToSettings(groups); - await this.writeConnectionsToSettings(connections); } } diff --git a/src/models/connectionStore.ts b/src/models/connectionStore.ts index 95e6d7361e..49ffadb8a7 100644 --- a/src/models/connectionStore.ts +++ b/src/models/connectionStore.ts @@ -20,7 +20,7 @@ import { IConnectionGroup, } from "../models/interfaces"; import { ICredentialStore } from "../credentialstore/icredentialstore"; -import { ConnectionConfig } from "../connectionconfig/connectionconfig"; +import { ConnectionConfig, ConfigurationTarget } from "../connectionconfig/connectionconfig"; import VscodeWrapper from "../controllers/vscodeWrapper"; import { IConnectionInfo } from "vscode-mssql"; import { Logger } from "./logger"; @@ -369,6 +369,17 @@ export class ConnectionStore { ): Promise { await this._connectionConfig.populateMissingConnectionIds(profile); + // Determine the correct target for saving based on groupId + let target = ConfigurationTarget.Global; + // Get all workspace group IDs + const workspaceGroups = this._connectionConfig.getGroupsFromSettings( + ConfigurationTarget.Workspace, + ); + const workspaceGroupIds = new Set(workspaceGroups.map((g) => g.id)); + if (workspaceGroupIds.has(profile.groupId)) { + target = ConfigurationTarget.Workspace; + } + // Add the profile to the saved list, taking care to clear out the password field if necessary let savedProfile: IConnectionProfile; if (profile.authenticationType === Utils.authTypeToString(AuthenticationTypes.AzureMFA)) { @@ -383,7 +394,7 @@ export class ConnectionStore { } } - await this._connectionConfig.addConnection(savedProfile); + await this._connectionConfig.addConnection(savedProfile, target); if (await this.saveProfilePasswordIfNeeded(profile)) { ConnInfo.fixupConnectionCredentials(profile); diff --git a/src/objectExplorer/objectExplorerService.ts b/src/objectExplorer/objectExplorerService.ts index b04f48f7c9..6258cb98b5 100644 --- a/src/objectExplorer/objectExplorerService.ts +++ b/src/objectExplorer/objectExplorerService.ts @@ -99,6 +99,20 @@ export class ObjectExplorerService { return result; } + /** + * Public method to refresh the Object Explorer tree and internal maps, merging user and workspace connections/groups. + * Call this after adding/removing connections/groups to ensure the tree is up to date. + */ + public async refreshTree(): Promise { + await this.getRootNodes(); + // Optionally, trigger a UI refresh if needed + if (this._refreshCallback && this._rootTreeNodeArray.length > 0) { + for (const node of this._rootTreeNodeArray) { + this._refreshCallback(node); + } + } + } + /** * Map of pending session creations */ @@ -380,7 +394,7 @@ export class ObjectExplorerService { this._connectionManager.connectionStore.connectionConfig.getGroupsFromSettings( ConfigurationTarget.Workspace, ); - // Merge root, user, and workspace groups + // Merge user and workspace groups before building hierarchy const allGroups = [...userGroups, ...workspaceGroups]; let savedConnections = await this._connectionManager.connectionStore.readAllConnections(); @@ -395,10 +409,8 @@ export class ObjectExplorerService { return this.getAddConnectionNodes(); } + // Build group nodes from merged settings const newConnectionGroupNodes = new Map(); - const newConnectionNodes = new Map(); - - // Add all group nodes from merged settings for (const group of allGroups) { const groupNode = new ConnectionGroupNode(group); if (this._connectionGroupNodes.has(group.id)) { @@ -407,12 +419,9 @@ export class ObjectExplorerService { newConnectionGroupNodes.set(group.id, groupNode); } - // Populate group hierarchy - add each group as a child to its parent + // Build hierarchy: add each group as a child to its parent for (const group of allGroups) { - // Skip the root group as it has no parent - if (group.id === rootId) { - continue; - } + if (group.id === rootId) continue; if (group.parentId && newConnectionGroupNodes.has(group.parentId)) { const parentNode = newConnectionGroupNodes.get(group.parentId); const childNode = newConnectionGroupNodes.get(group.id); @@ -421,19 +430,12 @@ export class ObjectExplorerService { if (parentNode.id !== rootId) { childNode.parentNode = parentNode; } - } else { - this._logger.error( - `Child group '${group.name}' with ID '${group.id}' does not have a valid parent group (${group.parentId}).`, - ); } - } else { - this._logger.error( - `Group '${group.name}' with ID '${group.id}' does not have a valid parent group ID. This should have been corrected when reading server groups from settings.`, - ); } } // Add connections as children of their respective groups + const newConnectionNodes = new Map(); for (const connection of savedConnections) { if (connection.groupId && newConnectionGroupNodes.has(connection.groupId)) { const groupNode = newConnectionGroupNodes.get(connection.groupId); @@ -451,21 +453,21 @@ export class ObjectExplorerService { connectionNode.parentNode = groupNode.id === rootId ? undefined : groupNode; newConnectionNodes.set(connection.id, connectionNode); groupNode.addChild(connectionNode); - } else { - this._logger.error( - `Connection '${getConnectionDisplayName(connection)}' with ID '${connection.id}' does not have a valid group ID. This should have been corrected when reading connections from settings.`, - ); } } + // Set the new maps before refreshing UI this._connectionGroupNodes = newConnectionGroupNodes; this._connectionNodes = newConnectionNodes; - const result = [...this._rootTreeNodeArray]; + // For the ROOT node, include as children any group whose parentId matches rootId + const rootChildren = Array.from(newConnectionGroupNodes.values()).filter( + (groupNode) => groupNode.connectionGroup.parentId === rootId, + ); getConnectionActivity.end(ActivityStatus.Succeeded, undefined, { - nodeCount: result.length, + nodeCount: rootChildren.length, }); - return result; + return rootChildren; } /** diff --git a/src/views/connectionUI.ts b/src/views/connectionUI.ts index 1b6fcba0b2..c2455c957b 100644 --- a/src/views/connectionUI.ts +++ b/src/views/connectionUI.ts @@ -20,6 +20,7 @@ import { Timer } from "../models/utils"; import { INameValueChoice, IPrompter, IQuestion, QuestionTypes } from "../prompts/question"; import { CREATE_NEW_GROUP_ID, IConnectionGroup } from "../sharedInterfaces/connectionGroup"; import { FormItemOptions } from "../sharedInterfaces/form"; +import { ConfigurationTarget } from "../connectionconfig/connectionconfig"; /** * The different tasks for managing connection profiles. @@ -438,19 +439,26 @@ export class ConnectionUI { */ public async getConnectionGroupOptions(): Promise { const rootId = this._connectionManager.connectionStore.rootGroupId; - let connectionGroups = - await this._connectionManager.connectionStore.readAllConnectionGroups(); - connectionGroups = connectionGroups.filter((g) => g.id !== rootId); + // Fetch user and workspace groups separately + const userGroups = await this._connectionManager.connectionStore.connectionConfig.getGroups( + ConfigurationTarget.Global, + ); + const workspaceGroups = + await this._connectionManager.connectionStore.connectionConfig.getGroups( + ConfigurationTarget.Workspace, + ); + // Merge and filter out the root group + let allGroups = [...userGroups, ...workspaceGroups].filter((g) => g.id !== rootId); // Count occurrences of group names to handle naming conflicts const nameOccurrences = new Map(); - for (const group of connectionGroups) { + for (const group of allGroups) { const count = nameOccurrences.get(group.name) || 0; nameOccurrences.set(group.name, count + 1); } // Create a map of group IDs to their full paths - const groupById = new Map(connectionGroups.map((g) => [g.id, g])); + const groupById = new Map(allGroups.map((g) => [g.id, g])); // Helper function to get parent path const getParentPath = (group: IConnectionGroup): string => { @@ -464,7 +472,7 @@ export class ConnectionUI { return `${getParentPath(parent)} > ${group.name}`; }; - const result = connectionGroups + const result = allGroups .map((g) => { // If there are naming conflicts, use the full path const displayName = nameOccurrences.get(g.name) > 1 ? getParentPath(g) : g.name; From c6f49c93ec634dae4a5b98a0d42980b8e8700ab9 Mon Sep 17 00:00:00 2001 From: Sam Gusick Date: Wed, 27 Aug 2025 17:09:36 -0400 Subject: [PATCH 05/18] ensure both connections and connection groups are merged correctly --- src/connectionconfig/connectionconfig.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/connectionconfig/connectionconfig.ts b/src/connectionconfig/connectionconfig.ts index 4ec8bc2aa0..1020a6f979 100644 --- a/src/connectionconfig/connectionconfig.ts +++ b/src/connectionconfig/connectionconfig.ts @@ -174,7 +174,11 @@ export class ConnectionConfig implements IConnectionConfig { } // filter out any connection with a group that isn't defined - const groupIds = new Set((await this.getGroups()).map((g) => g.id)); + // Merge user and workspace groups for group existence check + const userGroups = this.getGroupsFromSettings(ConfigurationTarget.Global); + const workspaceGroups = this.getGroupsFromSettings(ConfigurationTarget.Workspace); + const allGroups = [...userGroups, ...workspaceGroups]; + const groupIds = new Set(allGroups.map((g) => g.id)); profiles = profiles.filter((p) => { if (!groupIds.has(p.groupId)) { this._logger.warn( From 301eeaddab2f958da0178e2b4e93d7ee2f73ac89 Mon Sep 17 00:00:00 2001 From: Sam Gusick Date: Thu, 28 Aug 2025 14:25:28 -0400 Subject: [PATCH 06/18] workspace connection groups can now be created successfully --- src/connectionconfig/connectionconfig.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/connectionconfig/connectionconfig.ts b/src/connectionconfig/connectionconfig.ts index 1020a6f979..74d47f795d 100644 --- a/src/connectionconfig/connectionconfig.ts +++ b/src/connectionconfig/connectionconfig.ts @@ -22,6 +22,14 @@ export type ConfigTarget = ConfigurationTarget.Global | ConfigurationTarget.Work * Implements connection profile file storage. */ export class ConnectionConfig implements IConnectionConfig { + /** + * Get all connection groups from both user and workspace settings. + */ + public getAllConnectionGroups(): IConnectionGroup[] { + const userGroups = this.getGroupsFromSettings(ConfigurationTarget.Global); + const workspaceGroups = this.getGroupsFromSettings(ConfigurationTarget.Workspace); + return [...userGroups, ...workspaceGroups]; + } protected _logger: Logger; public initialized: Deferred = new Deferred(); @@ -50,8 +58,9 @@ export class ConnectionConfig implements IConnectionConfig { public getWorkspaceConnectionsGroup(): IConnectionGroup | undefined { const rootGroup = this.getRootGroup(); + console.log("ROOT GROUP FOUND: " + rootGroup.id); if (!rootGroup) return undefined; - const groups = this.getGroupsFromSettings(); + const groups = this.getAllConnectionGroups(); return groups.find( (g) => g.name === "Workspace Connections" && g.parentId === rootGroup.id, ); @@ -317,7 +326,13 @@ export class ConnectionConfig implements IConnectionConfig { } if (!group.parentId) { - group.parentId = this.getUserConnectionsGroupId(); + // If target is workspace, parent should be Workspace Connections group + if (target === ConfigurationTarget.Workspace) { + group.parentId = this.getWorkspaceConnectionsGroupId(); + console.log("workspace id:" + group.parentId); + } else { + group.parentId = this.getUserConnectionsGroupId(); + } } const groups = this.getGroupsFromSettings(target); From 2b44a9ecdc96a062dc28128da2a8e439f0de1388 Mon Sep 17 00:00:00 2001 From: Sam Gusick Date: Thu, 28 Aug 2025 16:56:29 -0400 Subject: [PATCH 07/18] created a new scope property and fixed issues with drag and drop --- src/connectionconfig/connectionconfig.ts | 36 +++++++++++-- src/models/interfaces.ts | 2 + .../objectExplorerDragAndDropController.ts | 53 +++++++++---------- src/views/connectionUI.ts | 3 ++ 4 files changed, 64 insertions(+), 30 deletions(-) diff --git a/src/connectionconfig/connectionconfig.ts b/src/connectionconfig/connectionconfig.ts index 74d47f795d..44eb21eaa2 100644 --- a/src/connectionconfig/connectionconfig.ts +++ b/src/connectionconfig/connectionconfig.ts @@ -58,7 +58,6 @@ export class ConnectionConfig implements IConnectionConfig { public getWorkspaceConnectionsGroup(): IConnectionGroup | undefined { const rootGroup = this.getRootGroup(); - console.log("ROOT GROUP FOUND: " + rootGroup.id); if (!rootGroup) return undefined; const groups = this.getAllConnectionGroups(); return groups.find( @@ -329,7 +328,6 @@ export class ConnectionConfig implements IConnectionConfig { // If target is workspace, parent should be Workspace Connections group if (target === ConfigurationTarget.Workspace) { group.parentId = this.getWorkspaceConnectionsGroupId(); - console.log("workspace id:" + group.parentId); } else { group.parentId = this.getUserConnectionsGroupId(); } @@ -670,10 +668,32 @@ export class ConnectionConfig implements IConnectionConfig { public getGroupsFromSettings( configLocation: ConfigTarget = ConfigurationTarget.Global, ): IConnectionGroup[] { - return this.getArrayFromSettings( + const groups = this.getArrayFromSettings( Constants.connectionGroupsArrayName, configLocation, ); + // Ensure scope is set for legacy groups + const expectedScope = + configLocation === ConfigurationTarget.Workspace ? "workspace" : "user"; + let changed = false; + for (const group of groups) { + if (!group.scope) { + group.scope = expectedScope; + changed = true; + } + } + // If any legacy group was updated, write back + if (changed) { + if (configLocation === ConfigurationTarget.Workspace) { + void this.writeConnectionGroupsToSettingsWithTarget( + groups, + ConfigurationTarget.Workspace, + ); + } else { + void this.writeConnectionGroupsToSettings(groups); + } + } + return groups; } /** @@ -684,6 +704,11 @@ export class ConnectionConfig implements IConnectionConfig { profiles: IConnectionProfile[], target: ConfigTarget = ConfigurationTarget.Global, ): Promise { + // Ensure scope is set before writing + const expectedScope = target === ConfigurationTarget.Workspace ? "workspace" : "user"; + for (const conn of profiles) { + conn.scope = conn.scope || expectedScope; + } await this._vscodeWrapper.setConfiguration( Constants.extensionName, Constants.connectionsArrayName, @@ -703,6 +728,11 @@ export class ConnectionConfig implements IConnectionConfig { connGroups: IConnectionGroup[], target: ConfigTarget, ): Promise { + // Ensure scope is set before writing + const expectedScope = target === ConfigurationTarget.Workspace ? "workspace" : "user"; + for (const group of connGroups) { + group.scope = group.scope || expectedScope; + } await this._vscodeWrapper.setConfiguration( Constants.extensionName, Constants.connectionGroupsArrayName, diff --git a/src/models/interfaces.ts b/src/models/interfaces.ts index 4010f0ff71..c9d657cead 100644 --- a/src/models/interfaces.ts +++ b/src/models/interfaces.ts @@ -68,6 +68,7 @@ export interface IConnectionProfile extends vscodeMssql.IConnectionInfo { accountStore: AccountStore; isValidProfile(): boolean; isAzureActiveDirectory(): boolean; + scope?: "user" | "workspace"; } export interface IConnectionGroup { @@ -76,6 +77,7 @@ export interface IConnectionGroup { parentId?: string; color?: string; description?: string; + scope?: "user" | "workspace"; } export enum CredentialsQuickPickItemType { diff --git a/src/objectExplorer/objectExplorerDragAndDropController.ts b/src/objectExplorer/objectExplorerDragAndDropController.ts index ce2d6047e2..189e275daa 100644 --- a/src/objectExplorer/objectExplorerDragAndDropController.ts +++ b/src/objectExplorer/objectExplorerDragAndDropController.ts @@ -105,48 +105,47 @@ export class ObjectExplorerDragAndDropController `Dragged ${dragData.type} '${dragData.name}' (ID: ${dragData.id}) onto group '${targetInfo.label}' (ID: ${targetInfo.id})`, ); - const workspaceGroupId = - this.connectionStore.connectionConfig.getWorkspaceConnectionsGroupId(); if (dragData.type === "connection") { const conn = await this.connectionStore.connectionConfig.getConnectionById( dragData.id, ); conn.groupId = targetInfo.id; - // If moving to Workspace Connections, store in workspace settings - if (targetInfo.id === workspaceGroupId) { - await this.connectionStore.connectionConfig.updateConnectionWithTarget( - conn, - vscode.ConfigurationTarget.Workspace, - ); - } else { - await this.connectionStore.connectionConfig.updateConnectionWithTarget( - conn, - vscode.ConfigurationTarget.Global, - ); - } + // Set scope based on target group + const targetGroup = this.connectionStore.connectionConfig.getGroupById( + targetInfo.id, + ); + conn.scope = targetGroup?.scope || "user"; + const configTarget = + conn.scope === "workspace" + ? vscode.ConfigurationTarget.Workspace + : vscode.ConfigurationTarget.Global; + await this.connectionStore.connectionConfig.updateConnectionWithTarget( + conn, + configTarget, + ); } else { const group = this.connectionStore.connectionConfig.getGroupById( dragData.id, ); - if (group.id === targetInfo.id) { this._logger.verbose("Cannot move group into itself; skipping."); return; } group.parentId = targetInfo.id; - // If moving to Workspace Connections, store in workspace settings - if (targetInfo.id === workspaceGroupId) { - await this.connectionStore.connectionConfig.updateGroupWithTarget( - group, - vscode.ConfigurationTarget.Workspace, - ); - } else { - await this.connectionStore.connectionConfig.updateGroupWithTarget( - group, - vscode.ConfigurationTarget.Global, - ); - } + // Set scope based on target group + const targetGroup = this.connectionStore.connectionConfig.getGroupById( + targetInfo.id, + ); + group.scope = targetGroup?.scope || "user"; + const configTarget = + group.scope === "workspace" + ? vscode.ConfigurationTarget.Workspace + : vscode.ConfigurationTarget.Global; + await this.connectionStore.connectionConfig.updateGroupWithTarget( + group, + configTarget, + ); } sendActionEvent(TelemetryViews.ObjectExplorer, TelemetryActions.DragAndDrop, { diff --git a/src/views/connectionUI.ts b/src/views/connectionUI.ts index c2455c957b..848066e3a5 100644 --- a/src/views/connectionUI.ts +++ b/src/views/connectionUI.ts @@ -501,6 +501,9 @@ export class ConnectionUI { * Save a connection profile using the connection store */ public async saveProfile(profile: IConnectionProfile): Promise { + // Set scope based on group + const group = this._connectionStore.connectionConfig.getGroupById(profile.groupId); + profile.scope = group?.scope || "user"; return await this._connectionStore.saveProfile(profile); } From 4943111ecfeb9a862b01515780c7719507477da3 Mon Sep 17 00:00:00 2001 From: Sam Gusick Date: Thu, 28 Aug 2025 17:51:41 -0400 Subject: [PATCH 08/18] new conditions to prevent newly disallowed drag and drop behavior --- .../objectExplorerDragAndDropController.ts | 57 +++++++++++++------ 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/src/objectExplorer/objectExplorerDragAndDropController.ts b/src/objectExplorer/objectExplorerDragAndDropController.ts index 189e275daa..9a6eaafc51 100644 --- a/src/objectExplorer/objectExplorerDragAndDropController.ts +++ b/src/objectExplorer/objectExplorerDragAndDropController.ts @@ -48,6 +48,15 @@ export class ObjectExplorerDragAndDropController ): void { const item = source[0]; // Handle only the first item for simplicity + // Prevent dragging User Connections and Workspace Connections groups + if ( + item instanceof ConnectionGroupNode && + (item.label === "User Connections" || item.label === "Workspace Connections") + ) { + // Do not set drag data, effectively disabling drag + return; + } + if (item instanceof ConnectionNode || item instanceof ConnectionGroupNode) { const dragData: ObjectExplorerDragMetadata = { name: item.label.toString(), @@ -83,23 +92,39 @@ export class ObjectExplorerDragAndDropController return; } + // Prevent dropping User Connections or Workspace Connections groups anywhere + const userGroupId = this.connectionStore.connectionConfig.getUserConnectionsGroupId(); + const workspaceGroupId = + this.connectionStore.connectionConfig.getWorkspaceConnectionsGroupId(); + if ( + dragData.type === "connectionGroup" && + (dragData.id === userGroupId || dragData.id === workspaceGroupId) + ) { + // Do nothing, prevent drop + return; + } + + // Prevent dropping onto User Connections or Workspace Connections groups + if ( + target instanceof ConnectionGroupNode && + (target.label === "User Connections" || target.label === "Workspace Connections") + ) { + // Do nothing, prevent drop + return; + } + + // Prevent drag-and-drop if target is root + if (target === undefined) { + return; + } + try { if (dragData.isConnectionOrGroup && dragData.type && dragData.id) { - if (target instanceof ConnectionGroupNode || target === undefined) { - let targetInfo: { label: string; id: string }; - - // If the target is undefined, we're dropping onto the root of the Object Explorer - if (target === undefined) { - targetInfo = { - label: "ROOT", - id: this.connectionStore.rootGroupId, - }; - } else { - targetInfo = { - label: target.label.toString(), - id: target.id, - }; - } + if (target instanceof ConnectionGroupNode) { + let targetInfo: { label: string; id: string } = { + label: target.label.toString(), + id: target.id, + }; this._logger.verbose( `Dragged ${dragData.type} '${dragData.name}' (ID: ${dragData.id}) onto group '${targetInfo.label}' (ID: ${targetInfo.id})`, @@ -150,7 +175,7 @@ export class ObjectExplorerDragAndDropController sendActionEvent(TelemetryViews.ObjectExplorer, TelemetryActions.DragAndDrop, { dragType: dragData.type, - dropTarget: target ? "connectionGroup" : "ROOT", + dropTarget: "connectionGroup", }); } } From f53be97968fcfe070b01cac6afc169ef566ef287 Mon Sep 17 00:00:00 2001 From: Sam Gusick Date: Thu, 28 Aug 2025 18:07:27 -0400 Subject: [PATCH 09/18] localized and fixed more drag and drop issues --- .../objectExplorerDragAndDropController.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/objectExplorer/objectExplorerDragAndDropController.ts b/src/objectExplorer/objectExplorerDragAndDropController.ts index 9a6eaafc51..6e33c8e7ec 100644 --- a/src/objectExplorer/objectExplorerDragAndDropController.ts +++ b/src/objectExplorer/objectExplorerDragAndDropController.ts @@ -104,14 +104,7 @@ export class ObjectExplorerDragAndDropController return; } - // Prevent dropping onto User Connections or Workspace Connections groups - if ( - target instanceof ConnectionGroupNode && - (target.label === "User Connections" || target.label === "Workspace Connections") - ) { - // Do nothing, prevent drop - return; - } + // Allow dropping child items into User Connections or Workspace Connections groups // Prevent drag-and-drop if target is root if (target === undefined) { From 57bc38554e3ccd6dd0214890beb3223b268ce231 Mon Sep 17 00:00:00 2001 From: Sam Gusick Date: Thu, 28 Aug 2025 18:31:44 -0400 Subject: [PATCH 10/18] delete for connections/connection groups seems to work correctly now --- src/connectionconfig/connectionconfig.ts | 93 +++++++++++++++++++----- 1 file changed, 76 insertions(+), 17 deletions(-) diff --git a/src/connectionconfig/connectionconfig.ts b/src/connectionconfig/connectionconfig.ts index 44eb21eaa2..a2705b636b 100644 --- a/src/connectionconfig/connectionconfig.ts +++ b/src/connectionconfig/connectionconfig.ts @@ -234,9 +234,8 @@ export class ConnectionConfig implements IConnectionConfig { */ public async removeConnection(profile: IConnectionProfile): Promise { // Determine if this is a workspace connection - const workspaceGroupId = this.getWorkspaceConnectionsGroupId(); let target = ConfigurationTarget.Global; - if (profile.groupId === workspaceGroupId) { + if (profile.scope === "workspace") { target = ConfigurationTarget.Workspace; } let profiles = this.getConnectionsFromSettings(target); @@ -349,8 +348,11 @@ export class ConnectionConfig implements IConnectionConfig { id: string, contentAction: "delete" | "move" = "delete", ): Promise { - const connections = this.getConnectionsFromSettings(); - const groups = this.getGroupsFromSettings(); + // Get all connections and groups from both user and workspace + const userConnections = this.getConnectionsFromSettings(ConfigurationTarget.Global); + const workspaceConnections = this.getConnectionsFromSettings(ConfigurationTarget.Workspace); + const allConnections = [...userConnections, ...workspaceConnections]; + const groups = this.getAllConnectionGroups(); const rootGroup = this.getRootGroup(); if (!rootGroup) { @@ -371,18 +373,34 @@ export class ConnectionConfig implements IConnectionConfig { }; let connectionModified = false; - let remainingConnections: IConnectionProfile[]; - let remainingGroups: IConnectionGroup[]; + let remainingUserConnections: IConnectionProfile[] = userConnections.slice(); + let remainingWorkspaceConnections: IConnectionProfile[] = workspaceConnections.slice(); + let remainingUserGroups: IConnectionGroup[] = this.getGroupsFromSettings( + ConfigurationTarget.Global, + ).slice(); + let remainingWorkspaceGroups: IConnectionGroup[] = this.getGroupsFromSettings( + ConfigurationTarget.Workspace, + ).slice(); if (contentAction === "delete") { // Get all nested subgroups to remove const groupsToRemove = getAllSubgroupIds(id); // Remove all connections in the groups being removed - remainingConnections = connections.filter((conn) => { + remainingUserConnections = remainingUserConnections.filter((conn) => { if (groupsToRemove.has(conn.groupId)) { this._logger.verbose( - `Removing connection '${conn.id}' because its group '${conn.groupId}' was removed`, + `Removing user connection '${conn.id}' because its group '${conn.groupId}' was removed`, + ); + connectionModified = true; + return false; + } + return true; + }); + remainingWorkspaceConnections = remainingWorkspaceConnections.filter((conn) => { + if (groupsToRemove.has(conn.groupId)) { + this._logger.verbose( + `Removing workspace connection '${conn.id}' because its group '${conn.groupId}' was removed`, ); connectionModified = true; return false; @@ -391,14 +409,27 @@ export class ConnectionConfig implements IConnectionConfig { }); // Remove all groups that were marked for removal - remainingGroups = groups.filter((g) => !groupsToRemove.has(g.id)); + remainingUserGroups = remainingUserGroups.filter((g) => !groupsToRemove.has(g.id)); + remainingWorkspaceGroups = remainingWorkspaceGroups.filter( + (g) => !groupsToRemove.has(g.id), + ); } else { // Move immediate child connections and groups to User Connections group const userGroupId = this.getUserConnectionsGroupId(); - remainingConnections = connections.map((conn) => { + remainingUserConnections = remainingUserConnections.map((conn) => { + if (conn.groupId === id) { + this._logger.verbose( + `Moving user connection '${conn.id}' to User Connections group because its immediate parent group '${id}' was removed`, + ); + connectionModified = true; + return { ...conn, groupId: userGroupId }; + } + return conn; + }); + remainingWorkspaceConnections = remainingWorkspaceConnections.map((conn) => { if (conn.groupId === id) { this._logger.verbose( - `Moving connection '${conn.id}' to User Connections group because its immediate parent group '${id}' was removed`, + `Moving workspace connection '${conn.id}' to User Connections group because its immediate parent group '${id}' was removed`, ); connectionModified = true; return { ...conn, groupId: userGroupId }; @@ -407,13 +438,23 @@ export class ConnectionConfig implements IConnectionConfig { }); // First remove the target group - remainingGroups = groups.filter((g) => g.id !== id); + remainingUserGroups = remainingUserGroups.filter((g) => g.id !== id); + remainingWorkspaceGroups = remainingWorkspaceGroups.filter((g) => g.id !== id); // Then reparent immediate children to User Connections group - remainingGroups = remainingGroups.map((g) => { + remainingUserGroups = remainingUserGroups.map((g) => { if (g.parentId === id) { this._logger.verbose( - `Moving group '${g.id}' to User Connections group because its immediate parent group '${id}' was removed`, + `Moving user group '${g.id}' to User Connections group because its immediate parent group '${id}' was removed`, + ); + return { ...g, parentId: userGroupId }; + } + return g; + }); + remainingWorkspaceGroups = remainingWorkspaceGroups.map((g) => { + if (g.parentId === id) { + this._logger.verbose( + `Moving workspace group '${g.id}' to User Connections group because its immediate parent group '${id}' was removed`, ); return { ...g, parentId: userGroupId }; } @@ -421,16 +462,34 @@ export class ConnectionConfig implements IConnectionConfig { }); } - if (remainingGroups.length === groups.length) { + // If no group was removed, return false + const originalUserGroups = this.getGroupsFromSettings(ConfigurationTarget.Global); + const originalWorkspaceGroups = this.getGroupsFromSettings(ConfigurationTarget.Workspace); + if ( + remainingUserGroups.length === originalUserGroups.length && + remainingWorkspaceGroups.length === originalWorkspaceGroups.length + ) { this._logger.error(`Connection group with ID '${id}' not found when removing.`); return false; } + // Write updated connections and groups to correct settings if (connectionModified) { - await this.writeConnectionsToSettings(remainingConnections); + await this.writeConnectionsToSettings( + remainingUserConnections, + ConfigurationTarget.Global, + ); + await this.writeConnectionsToSettings( + remainingWorkspaceConnections, + ConfigurationTarget.Workspace, + ); } - await this.writeConnectionGroupsToSettings(remainingGroups); + await this.writeConnectionGroupsToSettings(remainingUserGroups); + await this.writeConnectionGroupsToSettingsWithTarget( + remainingWorkspaceGroups, + ConfigurationTarget.Workspace, + ); return true; } From d50a958f46110ec33aaef5c2834a0a08e7d04e60 Mon Sep 17 00:00:00 2001 From: Sam Gusick Date: Wed, 3 Sep 2025 17:28:31 -0400 Subject: [PATCH 11/18] attempt to flatten hierarchy and comment out unused methods/variables for now --- src/connectionconfig/connectionconfig.ts | 1 - .../pages/ConnectionDialog/connectionFormPage.tsx | 14 +++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/connectionconfig/connectionconfig.ts b/src/connectionconfig/connectionconfig.ts index a2705b636b..34fdfc93ae 100644 --- a/src/connectionconfig/connectionconfig.ts +++ b/src/connectionconfig/connectionconfig.ts @@ -351,7 +351,6 @@ export class ConnectionConfig implements IConnectionConfig { // Get all connections and groups from both user and workspace const userConnections = this.getConnectionsFromSettings(ConfigurationTarget.Global); const workspaceConnections = this.getConnectionsFromSettings(ConfigurationTarget.Workspace); - const allConnections = [...userConnections, ...workspaceConnections]; const groups = this.getAllConnectionGroups(); const rootGroup = this.getRootGroup(); diff --git a/src/reactviews/pages/ConnectionDialog/connectionFormPage.tsx b/src/reactviews/pages/ConnectionDialog/connectionFormPage.tsx index 486a5080c7..2c1c95066d 100644 --- a/src/reactviews/pages/ConnectionDialog/connectionFormPage.tsx +++ b/src/reactviews/pages/ConnectionDialog/connectionFormPage.tsx @@ -31,9 +31,12 @@ export const ConnectionFormPage = () => { return undefined; } - // Helper to flatten group hierarchy for dropdown + // Helper to flatten group hierarchy for dropdown, excluding ROOT group function getGroupOptions(): SearchableDropdownOptions[] { if (!context?.state?.connectionGroups) return []; + // Find the root group id (assuming name is "ROOT") + const rootGroup = context.state.connectionGroups.find((g) => g.name === "ROOT"); + const rootGroupId = rootGroup?.id; // Recursively build hierarchical options, skipping ROOT function buildOptions( groups: IConnectionGroup[], @@ -41,7 +44,7 @@ export const ConnectionFormPage = () => { prefix: string = "", ): SearchableDropdownOptions[] { return groups - .filter((g) => g.parentId === parentId && g.name !== "ROOT") + .filter((g) => g.parentId === parentId && g.id !== rootGroupId && g.name !== "ROOT") .flatMap((g) => { const label = prefix ? `${prefix} / ${g.name}` : g.name; const children = buildOptions(groups, g.id, label); @@ -49,7 +52,8 @@ export const ConnectionFormPage = () => { }); } - return buildOptions(context.state.connectionGroups); + // Start from rootGroupId if available, otherwise undefined + return buildOptions(context.state.connectionGroups, rootGroupId ?? undefined); } // Selected group state @@ -58,7 +62,7 @@ export const ConnectionFormPage = () => { return (
{/* Connection Group Dropdown */} -
+ {/*
+
*/} {/* Existing connection form fields */} {context.state.connectionComponents.mainOptions.map((inputName, idx) => { const component = From fd882d9a4353bc04e194eae6658d8cc4ffbdd802 Mon Sep 17 00:00:00 2001 From: Sam Gusick Date: Wed, 3 Sep 2025 17:29:23 -0400 Subject: [PATCH 12/18] remove unused variables --- .../ConnectionDialog/connectionFormPage.tsx | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/src/reactviews/pages/ConnectionDialog/connectionFormPage.tsx b/src/reactviews/pages/ConnectionDialog/connectionFormPage.tsx index 2c1c95066d..48f1709c75 100644 --- a/src/reactviews/pages/ConnectionDialog/connectionFormPage.tsx +++ b/src/reactviews/pages/ConnectionDialog/connectionFormPage.tsx @@ -13,11 +13,8 @@ import { ConnectionDialogWebviewState, IConnectionDialogProfile, } from "../../../sharedInterfaces/connectionDialog"; -import { - SearchableDropdown, - SearchableDropdownOptions, -} from "../../common/searchableDropdown.component"; -import { IConnectionGroup } from "../../../sharedInterfaces/connectionGroup"; +// import { SearchableDropdownOptions } from "../../common/searchableDropdown.component"; +// import { IConnectionGroup } from "../../../sharedInterfaces/connectionGroup"; import { ConnectButton } from "./components/connectButton.component"; import { locConstants } from "../../common/locConstants"; import { AdvancedOptionsDrawer } from "./components/advancedOptionsDrawer.component"; @@ -32,32 +29,32 @@ export const ConnectionFormPage = () => { } // Helper to flatten group hierarchy for dropdown, excluding ROOT group - function getGroupOptions(): SearchableDropdownOptions[] { - if (!context?.state?.connectionGroups) return []; - // Find the root group id (assuming name is "ROOT") - const rootGroup = context.state.connectionGroups.find((g) => g.name === "ROOT"); - const rootGroupId = rootGroup?.id; - // Recursively build hierarchical options, skipping ROOT - function buildOptions( - groups: IConnectionGroup[], - parentId?: string, - prefix: string = "", - ): SearchableDropdownOptions[] { - return groups - .filter((g) => g.parentId === parentId && g.id !== rootGroupId && g.name !== "ROOT") - .flatMap((g) => { - const label = prefix ? `${prefix} / ${g.name}` : g.name; - const children = buildOptions(groups, g.id, label); - return [{ key: g.id, text: label, value: g.id }, ...children]; - }); - } + // function getGroupOptions(): SearchableDropdownOptions[] { + // if (!context?.state?.connectionGroups) return []; + // // Find the root group id (assuming name is "ROOT") + // const rootGroup = context.state.connectionGroups.find((g) => g.name === "ROOT"); + // const rootGroupId = rootGroup?.id; + // // Recursively build hierarchical options, skipping ROOT + // function buildOptions( + // groups: IConnectionGroup[], + // parentId?: string, + // prefix: string = "", + // ): SearchableDropdownOptions[] { + // return groups + // .filter((g) => g.parentId === parentId && g.id !== rootGroupId && g.name !== "ROOT") + // .flatMap((g) => { + // const label = prefix ? `${prefix} / ${g.name}` : g.name; + // const children = buildOptions(groups, g.id, label); + // return [{ key: g.id, text: label, value: g.id }, ...children]; + // }); + // } - // Start from rootGroupId if available, otherwise undefined - return buildOptions(context.state.connectionGroups, rootGroupId ?? undefined); - } + // // Start from rootGroupId if available, otherwise undefined + // return buildOptions(context.state.connectionGroups, rootGroupId ?? undefined); + // } // Selected group state - const [selectedGroup, setSelectedGroup] = useState(getGroupOptions()[0]?.value ?? ""); + // const [selectedGroup, setSelectedGroup] = useState(getGroupOptions()[0]?.value ?? ""); return (
From 974c95488b7f0751017b7c3d70ebdfde78f489f9 Mon Sep 17 00:00:00 2001 From: Sam Gusick Date: Mon, 15 Sep 2025 16:04:26 -0400 Subject: [PATCH 13/18] Additions to "Add Connection Group" dialog are now in line with the UI setup --- .../connectionGroup.component.tsx | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/reactviews/pages/ConnectionGroup/connectionGroup.component.tsx b/src/reactviews/pages/ConnectionGroup/connectionGroup.component.tsx index fb9d5f2795..e53d02f088 100644 --- a/src/reactviews/pages/ConnectionGroup/connectionGroup.component.tsx +++ b/src/reactviews/pages/ConnectionGroup/connectionGroup.component.tsx @@ -20,6 +20,12 @@ import { Popover, PopoverTrigger, PopoverSurface, + InputOnChangeData, + TextareaOnChangeData, + Dropdown, + Option, + OptionOnSelectData, + SelectionEvents, } from "@fluentui/react-components"; import { ColorArea, @@ -101,7 +107,8 @@ export const ConnectionGroupDialog = ({ const [popoverOpen, setPopoverOpen] = useState(false); const [scope, setScope] = useState(state.scope || "user"); - const handleChange: ColorPickerProps["onColorChange"] = (_, data) => { + // Explicitly remove undefined from the possible type so parameters are contextually typed + const handleChange: NonNullable = (_event, data) => { setColor({ ...data.color, a: 1 }); }; @@ -148,16 +155,16 @@ export const ConnectionGroupDialog = ({
)}{" "} - - + + setScope(data.optionValue as "user" | "workspace")}> + + + { + onChange={( + _e: React.ChangeEvent, + data: InputOnChangeData, + ) => { setGroupName(data.value); }} required @@ -177,7 +187,10 @@ export const ConnectionGroupDialog = ({ label={Loc.connectionGroups.description}>