Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the ultimate quick pick experience #1229

Merged
merged 55 commits into from
Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
80e57f6
It's full of stars
bwateratmsft Aug 30, 2022
df01530
Add a TODO comment
bwateratmsft Aug 30, 2022
493704b
Some explanatory comments
bwateratmsft Aug 31, 2022
39c3dd3
Check if isLeaf is null instead of undefined
alexweininger Sep 2, 2022
e7e66a2
Boxing / unboxing functionality (#1231)
alexweininger Sep 2, 2022
789b44a
Check `undefined` and `null`
bwateratmsft Sep 6, 2022
da6ccc3
Add find-by-ID experience and step
bwateratmsft Sep 6, 2022
bb38f55
Merge remote-tracking branch 'origin/api-v2' into bmw/quickPick_v8.30
alexweininger Sep 6, 2022
28c5e44
Changes to tree item picker (#1232)
alexweininger Sep 7, 2022
4a544a4
Commit rough progress
bwateratmsft Sep 9, 2022
77cd7d9
Rename Box to Wrapper
bwateratmsft Sep 9, 2022
09ec22a
Add compatibility steps (#1235)
alexweininger Sep 15, 2022
14f9334
Refactor and expose `isAzExtParentTreeItem` (#1236)
alexweininger Sep 16, 2022
d29d16d
Fix circular dependency (#1238)
alexweininger Sep 16, 2022
25018ea
Remove reliance on `quickPickOptions`, use `TreeItem`s directly (#1239)
alexweininger Sep 23, 2022
2c67a25
Add `PickFilter` interface and refactor steps (#1240)
alexweininger Oct 3, 2022
904f0a2
Fix go back behavior (#1241)
alexweininger Oct 6, 2022
a159f8c
Add compatibility pick subscription experience (#1245)
alexweininger Oct 13, 2022
086856a
Add `unwrapArgs` util (#1246)
alexweininger Oct 17, 2022
06b63ac
Add minimal v2 api types
alexweininger Oct 17, 2022
df3e93c
Add `getResourceGroupsApi` util (#1248)
alexweininger Oct 18, 2022
8121774
Fixup
alexweininger Oct 18, 2022
cfd9468
Fix `compatibilitySubscriptionExperience`
alexweininger Oct 19, 2022
7db798f
Rename `PickFilter` methods
alexweininger Nov 3, 2022
3227c42
Default `hideStepCount` to `true` in `RecursiveQuickPickStep`
alexweininger Nov 3, 2022
e23facc
Remove redundant `skipIfOne` property
alexweininger Nov 4, 2022
4d3502b
Add registerCommandWithTreeNodeUnwrapping util (#1262)
alexweininger Nov 7, 2022
580ffc7
Merge remote-tracking branch 'origin/api-v2' into bmw/quickPick_v8.30
alexweininger Nov 8, 2022
3999dda
Merge remote-tracking branch 'origin/api-v2' into bmw/quickPick_v8.30
alexweininger Nov 8, 2022
cc4bdc7
Move `isAzExtTreeItem`
alexweininger Nov 10, 2022
a9501cc
Expose a subset of `vscode.TreeDataProvider`.
alexweininger Nov 11, 2022
ee794bd
Fixup `isAncestorPick` jsdoc
alexweininger Nov 11, 2022
f448f8a
Use `Array.prototype.at`
alexweininger Nov 12, 2022
6164932
Fix double parens
alexweininger Nov 12, 2022
af98623
Make a copy of context
alexweininger Nov 15, 2022
6d0c31c
Move file
alexweininger Nov 15, 2022
d9367fb
import type
alexweininger Nov 15, 2022
23fe238
Organize compatibility experiences into namespace
alexweininger Nov 15, 2022
d31f406
Remove findById steps and experience
alexweininger Nov 29, 2022
07f0e5e
Add `isAzExtTreeItem` util (#1280)
alexweininger Nov 30, 2022
a084caf
Merge branch 'main' into api-v2
alexweininger Dec 5, 2022
22fe99a
Merge branch 'api-v2' into bmw/quickPick_v8.30
alexweininger Dec 6, 2022
adbb83b
Delete isAzExtParentTreeItem.ts
alexweininger Dec 6, 2022
5544e3b
Reset changes to AzExtTreeItem* classes
alexweininger Dec 6, 2022
bbebe06
Temporarily remove apiUtils
alexweininger Dec 6, 2022
2390d1e
Fixups
alexweininger Dec 6, 2022
6c36000
Remove pick resource group step
alexweininger Dec 6, 2022
0bbe730
Rename and move file
alexweininger Dec 6, 2022
92616e8
Rename to final + ancestor naming
alexweininger Dec 6, 2022
619e6da
Rename item to element
alexweininger Dec 6, 2022
d02d880
Add v2 hostapi typings (#1289)
alexweininger Dec 6, 2022
9d2f0dc
Merge branch 'api-v2' into bmw/quickPick_v8.30
alexweininger Dec 6, 2022
1d8ac62
Merge remote-tracking branch 'origin/main' into bmw/quickPick_v8.30
alexweininger Dec 6, 2022
feb7cf0
Remove unused QuickPickWithCreateStep for now
alexweininger Dec 6, 2022
7c150b8
Make some types internal
alexweininger Dec 6, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions utils/hostapi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface AzureHostExtensionApi {
/**
* The VSCode TreeView for the shared app resource view
*/
readonly appResourceTreeView: vscode.TreeView<AzExtTreeItem>;
readonly appResourceTreeView: vscode.TreeView<unknown>;

/**
* The `AzExtTreeDataProvider` for the shared workspace resource view
Expand All @@ -28,12 +28,12 @@ export interface AzureHostExtensionApi {
/**
* The VSCode TreeView for the shared workspace resource view
*/
readonly workspaceResourceTreeView: vscode.TreeView<AzExtTreeItem>;
readonly workspaceResourceTreeView: vscode.TreeView<unknown>;

/**
* Version of the API
*/
readonly apiVersion: string;
readonly apiVersion: '0.0.1';

/**
* Reveals an item in the shared app resource tree
Expand Down Expand Up @@ -76,7 +76,7 @@ export interface AzureHostExtensionApi {
/**
* @deprecated Use `appResourceTreeView` instead
*/
readonly treeView: vscode.TreeView<AzExtTreeItem>;
readonly treeView: vscode.TreeView<unknown>;

/**
* @deprecated Use `registerWorkspaceResourceProvider` instead
Expand Down
11 changes: 10 additions & 1 deletion utils/hostapi.v2.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import * as vscode from 'vscode';
import type { Environment } from '@azure/ms-rest-azure-env';
import type { Activity } from './hostapi';
import type { AzExtResourceType } from './index';
import type { AzExtResourceType, QuickPickWizardContext } from './index';
import type { AzureExtensionApi } from './api';

/**
Expand Down Expand Up @@ -325,3 +325,12 @@ export interface v2AzureResourcesApi extends AzureExtensionApi {
*/
registerWorkspaceResourceBranchDataProvider<TModel extends WorkspaceResourceModel>(type: WorkspaceResourceType, provider: WorkspaceResourceBranchDataProvider<TModel>): vscode.Disposable;
}

export declare interface PickSubscriptionWizardContext extends QuickPickWizardContext {
bwateratmsft marked this conversation as resolved.
Show resolved Hide resolved
subscription?: AzureSubscription;
}

export declare interface AzureResourceQuickPickWizardContext extends QuickPickWizardContext, PickSubscriptionWizardContext {
resource?: AzureResource;
resourceGroup?: string;
}
54 changes: 50 additions & 4 deletions utils/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import type { Environment } from '@azure/ms-rest-azure-env';
import { CancellationToken, CancellationTokenSource, Disposable, Event, ExtensionContext, FileChangeEvent, FileChangeType, FileStat, FileSystemProvider, FileType, InputBoxOptions, MarkdownString, MessageItem, MessageOptions, OpenDialogOptions, OutputChannel, Progress, QuickPickItem, QuickPickOptions, TextDocumentShowOptions, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, TreeView, Uri } from 'vscode';
import { CancellationToken, CancellationTokenSource, Disposable, Event, ExtensionContext, FileChangeEvent, FileChangeType, FileStat, FileSystemProvider, FileType, InputBoxOptions, MarkdownString, MessageItem, MessageOptions, OpenDialogOptions, OutputChannel, Progress, QuickPickItem, QuickPickOptions as VSCodeQuickPickOptions, TextDocumentShowOptions, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, TreeView, Uri } from 'vscode';
import { TargetPopulation } from 'vscode-tas-client';
import { AzureExtensionApi, AzureExtensionApiProvider } from './api';
import type { Activity, ActivityTreeItemOptions, AppResource, OnErrorActivityData, OnProgressActivityData, OnStartActivityData, OnSuccessActivityData } from './hostapi'; // This must remain `import type` or else a circular reference will result
import type { Activity, ActivityTreeItemOptions, AppResource, AzureHostExtensionApi, OnErrorActivityData, OnProgressActivityData, OnStartActivityData, OnSuccessActivityData } from './hostapi'; // This must remain `import type` or else a circular reference will result

export declare interface RunWithTemporaryDescriptionOptions {
description: string;
Expand Down Expand Up @@ -640,7 +640,9 @@ export declare class UserCancelledError extends Error {
constructor(stepName?: string);
}

export declare class NoResourceFoundError extends Error { }
export declare class NoResourceFoundError extends Error {
constructor(context?: ITreeItemPickerContext);
}

export type CommandCallback = (context: IActionContext, ...args: any[]) => any;

Expand Down Expand Up @@ -995,7 +997,7 @@ export interface IAzureQuickPickItem<T = undefined> extends QuickPickItem {
/**
* Provides additional options for QuickPicks used in Azure Extensions
*/
export interface IAzureQuickPickOptions extends QuickPickOptions, AzExtUserInputOptions {
export interface IAzureQuickPickOptions extends VSCodeQuickPickOptions, AzExtUserInputOptions {
nturinski marked this conversation as resolved.
Show resolved Hide resolved
/**
* An optional id to identify this QuickPick across sessions, used in persisting previous selections
* If not specified, a hash of the placeHolder will be used
Expand Down Expand Up @@ -1714,6 +1716,9 @@ export declare interface Wrapper {
unwrap<T>(): T;
}

// temporary
type ResourceGroupsItem = unknown;

/**
* Tests to see if something is a wrapper, by ensuring it is an object
* and has an "unwrap" function
Expand All @@ -1722,6 +1727,47 @@ export declare interface Wrapper {
*/
export declare function isWrapper(maybeWrapper: unknown): maybeWrapper is Wrapper;

export declare function appResourceExperience<TPick extends unknown>(context: IActionContext, tdp: TreeDataProvider<ResourceGroupsItem>, resourceTypes?: AzExtResourceType | AzExtResourceType[], childItemFilter?: ContextValueFilter): Promise<TPick>;
bwateratmsft marked this conversation as resolved.
Show resolved Hide resolved
export declare function contextValueExperience<TPick extends unknown>(context: IActionContext, tdp: TreeDataProvider<ResourceGroupsItem>, contextValueFilter: ContextValueFilter): Promise<TPick>;

interface CompatibilityPickResourceExperienceOptions {
resourceTypes?: AzExtResourceType | AzExtResourceType[];
childItemFilter?: ContextValueFilter
}

export declare namespace PickTreeItemWithCompatibility {
/**
* Provides compatibility for the legacy `pickAppResource` Resource Groups API
*/
export function resource<TPick extends AzExtTreeItem>(context: IActionContext, tdp: TreeDataProvider<ResourceGroupsItem>, options: CompatibilityPickResourceExperienceOptions): Promise<TPick>;
/**
* Returns `ISubscriptionContext` instead of `ApplicationSubscription` for compatibility.
*/
export function subscription(context: IActionContext, tdp: TreeDataProvider<ResourceGroupsItem>): Promise<ISubscriptionContext>;
}

export declare interface QuickPickWizardContext extends IActionContext {
pickedNodes: unknown[];
}

/**
* Describes filtering based on context value. Items that pass the filter will
* match at least one of the `include` filters, but none of the `exclude` filters.
*/
export declare interface ContextValueFilter {
/**
* This filter will include items that match *any* of the values in the array.
* When a string is used, exact value comparison is done.
*/
include: string | RegExp | (string | RegExp)[];
nturinski marked this conversation as resolved.
Show resolved Hide resolved

/**
* This filter will exclude items that match *any* of the values in the array.
* When a string is used, exact value comparison is done.
*/
exclude?: string | RegExp | (string | RegExp)[];
}

/**
* Get extension exports for the extension with the given id. Activates extension first if needed.
*
Expand Down
14 changes: 7 additions & 7 deletions utils/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@microsoft/vscode-azext-dev": "^0.1.4",
"@types/html-to-text": "^8.1.0",
"@types/mocha": "^7.0.2",
"@types/node": "^14.0.0",
"@types/node": "^16.0.0",
"@types/semver": "^7.3.9",
"@types/vscode": "1.64.0",
"@typescript-eslint/eslint-plugin": "^4.28.3",
Expand Down
3 changes: 3 additions & 0 deletions utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export * from './utils/contextUtils';
export * from './activityLog/activities/ExecuteActivity';
export * from './getAzExtResourceType';
export * from './AzExtResourceType';
export * from './treev2/quickPickWizard/experiences/appResourceExperience';
export * from './treev2/quickPickWizard/experiences/compatibility/PickTreeItemWithCompatibility';
export * from './treev2/quickPickWizard/experiences/contextValueExperience';
export * from './utils/apiUtils';
export * from './tree/isAzExtTreeItem';
// NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen
53 changes: 53 additions & 0 deletions utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { TreeItem } from 'vscode';
import * as types from '../../../index';
import { parseContextValue } from '../../utils/contextUtils';
import { PickFilter } from './common/PickFilter';
import { GenericQuickPickOptions, GenericQuickPickStep } from './GenericQuickPickStep';

export interface ContextValueFilterQuickPickOptions extends GenericQuickPickOptions {
contextValueFilter: types.ContextValueFilter;
}

export class ContextValueQuickPickStep<TContext extends types.QuickPickWizardContext, TOptions extends ContextValueFilterQuickPickOptions> extends GenericQuickPickStep<TContext, TOptions> {
nturinski marked this conversation as resolved.
Show resolved Hide resolved
protected readonly pickFilter: PickFilter = new ContextValuePickFilter(this.pickOptions);
}

class ContextValuePickFilter implements PickFilter {
constructor(private readonly pickOptions: ContextValueFilterQuickPickOptions) { }

isFinalPick(node: TreeItem): boolean {
const includeOption = this.pickOptions.contextValueFilter.include;
const excludeOption = this.pickOptions.contextValueFilter.exclude;

const includeArray: (string | RegExp)[] = Array.isArray(includeOption) ? includeOption : [includeOption];
nturinski marked this conversation as resolved.
Show resolved Hide resolved
const excludeArray: (string | RegExp)[] = excludeOption ?
(Array.isArray(excludeOption) ? excludeOption : [excludeOption]) :
[];

const nodeContextValues: string[] = parseContextValue(node.contextValue);

return includeArray.some(i => this.matchesSingleFilter(i, nodeContextValues)) &&
!excludeArray.some(e => this.matchesSingleFilter(e, nodeContextValues));
}

isAncestorPick(node: TreeItem): boolean {
// `TreeItemCollapsibleState.None` and `undefined` are both falsy, and indicate that a `TreeItem` cannot have children--and therefore, cannot be an indirect pick
return !node.collapsibleState;
nturinski marked this conversation as resolved.
Show resolved Hide resolved
}

private matchesSingleFilter(matcher: string | RegExp, nodeContextValues: string[]): boolean {
return nodeContextValues.some(c => {
if (matcher instanceof RegExp) {
return matcher.test(c);
}

// Context value matcher is a string, do full equality (same as old behavior)
return c === matcher;
})
}
}
99 changes: 99 additions & 0 deletions utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as types from '../../../index';
import * as vscode from 'vscode';
import { getLastNode } from './common/getLastNode';
import { AzureWizardPromptStep } from '../../wizard/AzureWizardPromptStep';
import { PickFilter } from './common/PickFilter';
import { localize } from '../../localize';

export interface GenericQuickPickOptions {
skipIfOne?: boolean;
}

export interface SkipIfOneQuickPickOptions extends GenericQuickPickOptions {
bwateratmsft marked this conversation as resolved.
Show resolved Hide resolved
skipIfOne?: true;
}

export abstract class GenericQuickPickStep<TContext extends types.QuickPickWizardContext, TOptions extends GenericQuickPickOptions> extends AzureWizardPromptStep<TContext> {
public readonly supportsDuplicateSteps = true;

protected readonly abstract pickFilter: PickFilter<vscode.TreeItem>;

public constructor(
protected readonly treeDataProvider: vscode.TreeDataProvider<unknown>,
protected readonly pickOptions: TOptions
) {
super();
}

public async prompt(wizardContext: TContext): Promise<void> {
const pick = await this.promptInternal(wizardContext);
wizardContext.pickedNodes.push(pick);
}

public undo(wizardContext: TContext): void {
wizardContext.pickedNodes.pop();
}

public shouldPrompt(_wizardContext: TContext): boolean {
return true;
}

protected async promptInternal(wizardContext: TContext): Promise<unknown> {
const picks = await this.getPicks(wizardContext);

if (picks.length === 1 && this.pickOptions.skipIfOne) {
return picks[0].data;
} else {
const selected = await wizardContext.ui.showQuickPick(picks, {
noPicksMessage: localize('noMatchingResources', 'No matching resources found.'),
/* TODO: options */
/* TODO: set id here so recently picked items appear at the top */
});

return selected.data;
}
}

protected async getPicks(wizardContext: TContext): Promise<types.IAzureQuickPickItem<unknown>[]> {
const lastPickedItem: unknown | undefined = getLastNode(wizardContext);

// TODO: if `lastPickedItem` is an `AzExtParentTreeItem`, should we clear its cache?
alexweininger marked this conversation as resolved.
Show resolved Hide resolved
const childElements = (await this.treeDataProvider.getChildren(lastPickedItem)) || [];
const childItems = await Promise.all(childElements.map(async (childElement: unknown) => await this.treeDataProvider.getTreeItem(childElement)));
const childPairs: [unknown, vscode.TreeItem][] = childElements.map((childElement: unknown, i: number) => [childElement, childItems[i]]);

const finalChoices = childPairs.filter(([, ti]) => this.pickFilter.isFinalPick(ti));
const ancestorChoices = childPairs.filter(([, ti]) => this.pickFilter.isAncestorPick(ti));

let promptChoices: [unknown, vscode.TreeItem][] = [];
if (finalChoices.length === 0) {
if (ancestorChoices.length === 0) {
// Don't throw and end the wizard, let user use back button instead
} else {
promptChoices = ancestorChoices;
}
} else {
promptChoices = finalChoices;
}

const picks: types.IAzureQuickPickItem<unknown>[] = [];
for (const choice of promptChoices) {
picks.push(await this.getQuickPickItem(...choice));
}

return picks;
}

private async getQuickPickItem(element: unknown, item: vscode.TreeItem): Promise<types.IAzureQuickPickItem<unknown>> {
return {
label: ((item.label as vscode.TreeItemLabel)?.label || item.label) as string,
description: item.description as string,
data: element,
};
}
}
34 changes: 34 additions & 0 deletions utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as types from '../../../index';
import { ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from './ContextValueQuickPickStep';
import { getLastNode } from './common/getLastNode';

export class RecursiveQuickPickStep<TContext extends types.QuickPickWizardContext> extends ContextValueQuickPickStep<TContext, ContextValueFilterQuickPickOptions> {
bwateratmsft marked this conversation as resolved.
Show resolved Hide resolved
hideStepCount: boolean = true;

public async getSubWizard(wizardContext: TContext): Promise<types.IWizardOptions<TContext> | undefined> {
const lastPickedItem = getLastNode(wizardContext);

if (!lastPickedItem) {
// Something went wrong, no node was chosen
throw new Error('No node was set after prompt step.');
}

if (this.pickFilter.isFinalPick(await this.treeDataProvider.getTreeItem(lastPickedItem))) {
// The last picked node matches the expected filter
// No need to continue prompting
return undefined;
} else {
// Need to keep going because the last picked node is not a match
return {
promptSteps: [
new RecursiveQuickPickStep(this.treeDataProvider, this.pickOptions)
],
};
}
}
}
Loading