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 4 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
2 changes: 1 addition & 1 deletion utils/src/tree/AzExtParentTreeItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export abstract class AzExtParentTreeItem extends AzExtTreeItem implements types
// Just in case implementers of `loadMoreChildrenImpl` re-use the same child node, we want to clear those caches as well
for (const child of this._cachedChildren) {
if (isAzExtParentTreeItem(child)) {
(<AzExtParentTreeItem>child).clearCache();
child.clearCache();
}
}
this._cachedChildren = [];
Expand Down
4 changes: 2 additions & 2 deletions utils/src/tree/AzExtTreeDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export class AzExtTreeDataProvider implements IAzExtTreeDataProviderInternal, ty
}

if (isAzExtParentTreeItem(treeItem)) {
(<AzExtParentTreeItem>treeItem).clearCache();
treeItem.clearCache();
}

this.refreshUIOnly(treeItem);
Expand Down Expand Up @@ -179,7 +179,7 @@ export class AzExtTreeDataProvider implements IAzExtTreeDataProviderInternal, ty

while (!treeItem.matchesContextValue(expectedContextValues)) {
if (isAzExtParentTreeItem(treeItem)) {
const pickedItems: AzExtTreeItem | AzExtTreeItem[] = await (<AzExtParentTreeItem>treeItem).pickChildTreeItem(expectedContextValues, context);
const pickedItems: AzExtTreeItem | AzExtTreeItem[] = await treeItem.pickChildTreeItem(expectedContextValues, context);
if (Array.isArray(pickedItems)) {
// canPickMany is only supported at the last stage of the picker, so automatically return if this is an array
return <T[]><unknown>pickedItems;
Expand Down
7 changes: 4 additions & 3 deletions utils/src/tree/InternalInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@

import { EventEmitter } from 'vscode';
import * as types from '../../index';
import { AzExtParentTreeItem } from './AzExtParentTreeItem';
import { AzExtTreeItem } from './AzExtTreeItem';
import { CollapsibleStateTracker } from './CollapsibleStateTracker';

// Interfaces for methods on the tree that aren't exposed outside of this package
// We can't reference the classes directly because it would result in circular dependencies

export interface IAzExtParentTreeItemInternal extends types.AzExtParentTreeItem, AzExtTreeItem {
export interface IAzExtParentTreeItemInternal extends AzExtParentTreeItem {
_isAzExtParentTreeItem: boolean;
parent: IAzExtParentTreeItemInternal | undefined;
treeDataProvider: IAzExtTreeDataProviderInternal;
Expand All @@ -28,6 +29,6 @@ export interface IAzExtTreeDataProviderInternal extends types.AzExtTreeDataProvi
/**
* Using instanceof AzExtParentTreeItem causes issues whenever packages are linked for dev testing. Instead, check _isAzExtParentTreeItem
*/
export function isAzExtParentTreeItem(item: {}): boolean {
return !!(<IAzExtParentTreeItemInternal>item)._isAzExtParentTreeItem;
export function isAzExtParentTreeItem(maybeParent: unknown): maybeParent is IAzExtParentTreeItemInternal {
return !!(maybeParent as IAzExtParentTreeItemInternal)._isAzExtParentTreeItem;
}
89 changes: 89 additions & 0 deletions utils/src/treev2/quickPickWizard/ContextValueQuickPickStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*---------------------------------------------------------------------------------------------
* 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 { GenericQuickPickOptions, GenericQuickPickStep } from './GenericQuickPickStep';
import { isAzExtParentTreeItem } from '../../tree/InternalInterfaces';
import { QuickPickWizardContext } from './QuickPickWizardContext';

/**
* 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 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)[];

/**
* 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)[];
}

interface ContextValueFilterableTreeNodeV2 {
readonly quickPickOptions: {
readonly contextValues: Array<string>;
readonly isLeaf: boolean;
}
}

export type ContextValueFilterableTreeNode = ContextValueFilterableTreeNodeV2 | types.AzExtTreeItem;

export interface ContextValueFilterQuickPickOptions extends GenericQuickPickOptions {
contextValueFilter: ContextValueFilter;
}

export class ContextValueQuickPickStep<TNode extends ContextValueFilterableTreeNode, TContext extends QuickPickWizardContext<TNode>, TOptions extends ContextValueFilterQuickPickOptions> extends GenericQuickPickStep<TNode, TContext, TOptions> {
protected override isDirectPick(node: TNode): boolean {
const includeOption = this.pickOptions.contextValueFilter.include;
const excludeOption = this.pickOptions.contextValueFilter.exclude;

const includeArray: (string | RegExp)[] = Array.isArray(includeOption) ? includeOption : [includeOption];
const excludeArray: (string | RegExp)[] = excludeOption ?
(Array.isArray(excludeOption) ? excludeOption : [excludeOption]) :
[];

const nodeContextValues: string[] = isV2TreeNode(node) ?
node.quickPickOptions.contextValues :
[node.contextValue];

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

protected override isIndirectPick(node: TNode): boolean {
if (isV2TreeNode(node)) {
return node.quickPickOptions.isLeaf === false;
} else if (isAzExtParentTreeItem(node)) {
return true;
}

return false;
}

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;
})
}
}

function isV2TreeNode(maybeNode: unknown): maybeNode is ContextValueFilterableTreeNodeV2 {
if (typeof maybeNode === 'object') {
return Array.isArray((maybeNode as ContextValueFilterableTreeNodeV2).quickPickOptions?.contextValues) &&
(maybeNode as ContextValueFilterableTreeNodeV2).quickPickOptions?.isLeaf !== null;
}

return false;
}
112 changes: 112 additions & 0 deletions utils/src/treev2/quickPickWizard/GenericQuickPickStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*---------------------------------------------------------------------------------------------
* 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, QuickPickWizardContext } from './QuickPickWizardContext';
import { AzureWizardPromptStep } from '../../wizard/AzureWizardPromptStep';
import { NoResourceFoundError } from '../../errors';
import { parseError } from '../../parseError';

export interface GenericQuickPickOptions {
skipIfOne?: boolean;
}

export interface SkipIfOneQuickPickOptions extends GenericQuickPickOptions {
skipIfOne?: true;
}

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

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

public async prompt(wizardContext: TContext): Promise<void> {
try {
const pick = await this.promptInternal(wizardContext);
wizardContext.pickedNodes.push(pick);
} catch (err) {
const error = parseError(err);
if (error.errorType === 'GoBackError') {
// Instead of wiping out a property value, which is the default wizard behavior for `GoBackError`, pop the most recent
// value off from the provenance of the picks
wizardContext.pickedNodes.pop();
}

// And rethrow
throw err;
}
}

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

protected async promptInternal(wizardContext: TContext): Promise<TNode> {
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, { /* TODO: options */ });
return selected.data;
}
}

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

// TODO: if `lastPickedItem` is an `AzExtParentTreeItem`, should we clear its cache?
const children = (await this.treeDataProvider.getChildren(lastPickedItem)) || [];

const directChoices = children.filter(c => this.isDirectPick(c));
const indirectChoices = children.filter(c => this.isIndirectPick(c));

let promptChoices: TNode[];
if (directChoices.length === 0) {
if (indirectChoices.length === 0) {
throw new NoResourceFoundError();
} else {
promptChoices = indirectChoices;
}
} else {
promptChoices = directChoices;
}

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

return picks;
}

/**
* Filters for nodes that match the final target.
* @param node The node to apply the filter to
*/
protected abstract isDirectPick(node: TNode): boolean;

/**
* Filters for nodes that could have a descendant matching the final target.
* @param node The node to apply the filter to
*/
protected abstract isIndirectPick(node: TNode): boolean;

private async getQuickPickItem(resource: TNode): Promise<types.IAzureQuickPickItem<TNode>> {
const treeItem = await Promise.resolve(this.treeDataProvider.getTreeItem(resource));

return {
label: ((treeItem.label as vscode.TreeItemLabel)?.label || treeItem.label) as string,
description: treeItem.description as string,
data: resource,
};
}
}
42 changes: 42 additions & 0 deletions utils/src/treev2/quickPickWizard/QuickPickWithCreateStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* 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 { getLastNode, QuickPickWizardContext } from './QuickPickWizardContext';
import { ContextValueFilterableTreeNode, ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from './ContextValueQuickPickStep';
import { localize } from '../../localize';

type CreateCallback = <TNode extends ContextValueFilterableTreeNode>() => TNode | Promise<TNode>;
interface CreateQuickPickOptions extends ContextValueFilterQuickPickOptions {
skipIfOne?: never; // Not allowed in CreateQuickPickStep
createLabel?: string;
createCallback: CreateCallback;
}

export class CreateQuickPickStep<TNode extends ContextValueFilterableTreeNode, TContext extends QuickPickWizardContext<TNode>> extends ContextValueQuickPickStep<TNode, TContext, CreateQuickPickOptions> {
public override async prompt(wizardContext: TContext): Promise<void> {
await super.prompt(wizardContext);

const lastNode = getLastNode(wizardContext) as (TNode | CreateCallback);
if (typeof lastNode === 'function') {
// If the last node is a function, pop it off the list and execute it
const callback = wizardContext.pickedNodes.pop() as unknown as CreateCallback;
wizardContext.pickedNodes.push(await callback());
}
}

protected override async getPicks(wizardContext: TContext): Promise<types.IAzureQuickPickItem<TNode>[]> {
const picks: types.IAzureQuickPickItem<TNode | CreateCallback>[] = await super.getPicks(wizardContext);
picks.push(this.getCreatePick());
return picks as types.IAzureQuickPickItem<TNode>[];
}

private getCreatePick(): types.IAzureQuickPickItem<CreateCallback> {
return {
label: this.pickOptions.createLabel || localize('createQuickPickLabel', '$(add) Create...'),
data: this.pickOptions.createCallback,
};
}
}
18 changes: 18 additions & 0 deletions utils/src/treev2/quickPickWizard/QuickPickWizardContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* 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';

export interface QuickPickWizardContext<TNode extends unknown> extends types.IActionContext {
pickedNodes: TNode[];
}

export function getLastNode<TNode extends unknown>(context: QuickPickWizardContext<TNode>): TNode | undefined {
if (context.pickedNodes.length) {
return context.pickedNodes[context.pickedNodes.length - 1];
}

return undefined;
}
33 changes: 33 additions & 0 deletions utils/src/treev2/quickPickWizard/RecursiveQuickPickStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* 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 { ContextValueFilterableTreeNode, ContextValueFilterQuickPickOptions, ContextValueQuickPickStep } from './ContextValueQuickPickStep';
import { getLastNode, QuickPickWizardContext } from './QuickPickWizardContext';

export class RecursiveQuickPickStep<TNode extends ContextValueFilterableTreeNode, TContext extends QuickPickWizardContext<TNode>> extends ContextValueQuickPickStep<TNode, TContext, ContextValueFilterQuickPickOptions> {
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 (super.isDirectPick(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 {
hideStepCount: true,
promptSteps: [
new RecursiveQuickPickStep(this.treeDataProvider, this.pickOptions)
],
};
}
}
}
Loading