Skip to content

Commit

Permalink
Refactoring form controller logic to base class for reuse (#18842)
Browse files Browse the repository at this point in the history
* checkpoint

* checkpoint

* compiling

* Additional cleanup
  • Loading branch information
Benjin authored Mar 1, 2025
1 parent 9559efd commit 8c93b31
Showing 11 changed files with 334 additions and 206 deletions.
138 changes: 45 additions & 93 deletions src/connectionconfig/connectionDialogWebviewController.ts
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ import {
AddFirewallRuleDialogProps,
IConnectionDialogProfile,
TrustServerCertDialogProps,
ConnectionDialogFormItemSpec,
} from "../sharedInterfaces/connectionDialog";
import { ConnectionCompleteParams } from "../models/contracts/connection";
import {
@@ -51,7 +52,6 @@ import { AzureSubscription } from "@microsoft/vscode-azext-azureauth";
import { IConnectionInfo } from "vscode-mssql";
import MainController from "../controllers/mainController";
import { ObjectExplorerProvider } from "../objectExplorer/objectExplorerProvider";
import { ReactWebviewPanelController } from "../controllers/reactWebviewPanelController";
import { UserSurvey } from "../nps/userSurvey";
import VscodeWrapper from "../controllers/vscodeWrapper";
import {
@@ -69,13 +69,14 @@ import {
import { IAccount } from "../models/contracts/azure";
import {
generateConnectionComponents,
getActiveFormComponents,
getFormComponent,
groupAdvancedOptions,
} from "./formComponentHelpers";
import { FormWebviewController } from "../forms/formWebviewController";

export class ConnectionDialogWebviewController extends ReactWebviewPanelController<
export class ConnectionDialogWebviewController extends FormWebviewController<
IConnectionDialogProfile,
ConnectionDialogWebviewState,
ConnectionDialogFormItemSpec,
ConnectionDialogReducers
> {
//#region Properties
@@ -176,12 +177,13 @@ export class ConnectionDialogWebviewController extends ReactWebviewPanelControll
);
}

this.state.formComponents = await generateConnectionComponents(
this._mainController.connectionManager,
getAccounts(this._mainController.azureAccountService),
this.getAzureActionButtons(),
);

this.state.connectionComponents = {
components: await generateConnectionComponents(
this._mainController.connectionManager,
getAccounts(this._mainController.azureAccountService),
this.getAzureActionButtons(),
),
mainOptions: [...ConnectionDialogWebviewController.mainOptions],
topAdvancedOptions: [
"port",
@@ -195,7 +197,10 @@ export class ConnectionDialogWebviewController extends ReactWebviewPanelControll
};

this.state.connectionComponents.groupedAdvancedOptions =
groupAdvancedOptions(this.state.connectionComponents);
groupAdvancedOptions(
this.state.formComponents as any,
this.state.connectionComponents,
);

await this.updateItemVisibility();
this.updateState();
@@ -220,36 +225,6 @@ export class ConnectionDialogWebviewController extends ReactWebviewPanelControll
},
);

this.registerReducer("formAction", async (state, payload) => {
if (payload.event.isAction) {
const component = getFormComponent(
this.state,
payload.event.propertyName,
);
if (component && component.actionButtons) {
const actionButton = component.actionButtons.find(
(b) => b.id === payload.event.value,
);
if (actionButton?.callback) {
await actionButton.callback();
}
}
} else {
(this.state.connectionProfile[
payload.event.propertyName
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any) = payload.event.value;
await this.validateConnectionProfile(
this.state.connectionProfile,
payload.event.propertyName,
);
await this.handleAzureMFAEdits(payload.event.propertyName);
}
await this.updateItemVisibility();

return state;
});

this.registerReducer("loadConnection", async (state, payload) => {
sendActionEvent(
TelemetryViews.ConnectionDialog,
@@ -432,7 +407,13 @@ export class ConnectionDialogWebviewController extends ReactWebviewPanelControll

//#region Connection helpers

private async updateItemVisibility() {
override async afterSetFormProperty(
propertyName: keyof IConnectionDialogProfile,
): Promise<void> {
return await this.handleAzureMFAEdits(propertyName);
}

async updateItemVisibility() {
let hiddenProperties: (keyof IConnectionDialogProfile)[] = [];

if (
@@ -466,56 +447,23 @@ export class ConnectionDialogWebviewController extends ReactWebviewPanelControll
}
}

for (const component of Object.values(
this.state.connectionComponents.components,
)) {
for (const component of Object.values(this.state.formComponents)) {
component.hidden = hiddenProperties.includes(
component.propertyName,
);
}
}

private async validateConnectionProfile(
connectionProfile: IConnectionDialogProfile,
propertyName?: keyof IConnectionDialogProfile,
): Promise<string[]> {
const erroredInputs = [];
if (propertyName) {
const component = getFormComponent(this.state, propertyName);
if (component && component.validate) {
component.validation = component.validate(
this.state,
connectionProfile[propertyName],
);
if (!component.validation.isValid) {
erroredInputs.push(component.propertyName);
}
}
} else {
getActiveFormComponents(this.state)
.map((x) => this.state.connectionComponents.components[x])
.forEach((c) => {
if (c.hidden) {
c.validation = {
isValid: true,
validationMessage: "",
};
return;
} else {
if (c.validate) {
c.validation = c.validate(
this.state,
connectionProfile[c.propertyName],
);
if (!c.validation.isValid) {
erroredInputs.push(c.propertyName);
}
}
}
});
protected getActiveFormComponents(
state: ConnectionDialogWebviewState,
): (keyof IConnectionDialogProfile)[] {
if (
state.selectedInputMode === ConnectionInputMode.Parameters ||
state.selectedInputMode === ConnectionInputMode.AzureBrowse
) {
return state.connectionComponents.mainOptions;
}

return erroredInputs;
return ["connectionString", "profileName"];
}

/** Returns a copy of `connection` that's been cleaned up by clearing the properties that aren't being used
@@ -526,9 +474,7 @@ export class ConnectionDialogWebviewController extends ReactWebviewPanelControll
const cleanedConnection = structuredClone(connection);

// Clear values for inputs that are hidden due to form selections
for (const option of Object.values(
this.state.connectionComponents.components,
)) {
for (const option of Object.values(this.state.formComponents)) {
if (option.hidden) {
(cleanedConnection[
option.propertyName as keyof IConnectionDialogProfile
@@ -663,7 +609,7 @@ export class ConnectionDialogWebviewController extends ReactWebviewPanelControll
// clean the connection by clearing the options that aren't being used
const cleanedConnection = this.cleanConnection(connectionProfile);

return await this.validateConnectionProfile(cleanedConnection);
return await this.validateForm(cleanedConnection);
}

private async connectHelper(
@@ -963,7 +909,7 @@ export class ConnectionDialogWebviewController extends ReactWebviewPanelControll
callback: async () => {
const account =
await this._mainController.azureAccountService.addAccount();
const accountsComponent = getFormComponent(
const accountsComponent = this.getFormComponent(
this.state,
"accountId",
);
@@ -1047,8 +993,14 @@ export class ConnectionDialogWebviewController extends ReactWebviewPanelControll
) {
return;
}
const accountComponent = getFormComponent(this.state, "accountId");
const tenantComponent = getFormComponent(this.state, "tenantId");
const accountComponent = this.getFormComponent(
this.state,
"accountId",
);
const tenantComponent = this.getFormComponent(
this.state,
"tenantId",
);
let tenants: FormItemOptions[] = [];
switch (propertyName) {
case "accountId":
@@ -1321,8 +1273,8 @@ export class ConnectionDialogWebviewController extends ReactWebviewPanelControll

private clearFormError() {
this.state.formError = "";
for (const component of getActiveFormComponents(this.state).map(
(x) => this.state.connectionComponents.components[x],
for (const component of this.getActiveFormComponents(this.state).map(
(x) => this.state.formComponents[x],
)) {
component.validation = undefined;
}
35 changes: 10 additions & 25 deletions src/connectionconfig/formComponentHelpers.ts
Original file line number Diff line number Diff line change
@@ -100,6 +100,10 @@ export async function generateConnectionComponents(
}

export function groupAdvancedOptions(
components: Record<
keyof IConnectionDialogProfile,
ConnectionDialogFormItemSpec
>,
componentsInfo: ConnectionComponentsInfo,
): ConnectionComponentGroup[] {
const groupMap: Map<string, ConnectionComponentGroup> = new Map([
@@ -112,7 +116,7 @@ export function groupAdvancedOptions(
["context", undefined],
]);

const optionsToGroup = Object.values(componentsInfo.components).filter(
const optionsToGroup = Object.values(components).filter(
(c) =>
c.isAdvancedOption &&
!componentsInfo.mainOptions.includes(c.propertyName) &&
@@ -141,7 +145,11 @@ export function groupAdvancedOptions(

export function convertToFormComponent(
connOption: ConnectionOption,
): FormItemSpec<ConnectionDialogWebviewState, IConnectionDialogProfile> {
): FormItemSpec<
IConnectionDialogProfile,
ConnectionDialogWebviewState,
ConnectionDialogFormItemSpec
> {
switch (connOption.valueType) {
case "boolean":
return {
@@ -344,26 +352,3 @@ export async function completeFormComponents(
};
};
}

export function getActiveFormComponents(
state: ConnectionDialogWebviewState,
): (keyof IConnectionDialogProfile)[] {
if (
state.selectedInputMode === ConnectionInputMode.Parameters ||
state.selectedInputMode === ConnectionInputMode.AzureBrowse
) {
return state.connectionComponents.mainOptions;
}
return ["connectionString", "profileName"];
}

export function getFormComponent(
state: ConnectionDialogWebviewState,
propertyName: keyof IConnectionDialogProfile,
):
| FormItemSpec<ConnectionDialogWebviewState, IConnectionDialogProfile>
| undefined {
return getActiveFormComponents(state).includes(propertyName)
? state.connectionComponents.components[propertyName]
: undefined;
}
Loading
Oops, something went wrong.

0 comments on commit 8c93b31

Please sign in to comment.