From 3931a16c6484a80dbd0c33d765f3367c682722e3 Mon Sep 17 00:00:00 2001 From: Mike Nguyen Date: Thu, 15 Aug 2024 10:19:00 -0400 Subject: [PATCH 1/4] init: first commit --- .../itwin/measure-tools/src/ui-2.0/MeasureToolsUiProvider.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/itwin/measure-tools/src/ui-2.0/MeasureToolsUiProvider.tsx b/packages/itwin/measure-tools/src/ui-2.0/MeasureToolsUiProvider.tsx index 117d49cb9..1d50648b2 100644 --- a/packages/itwin/measure-tools/src/ui-2.0/MeasureToolsUiProvider.tsx +++ b/packages/itwin/measure-tools/src/ui-2.0/MeasureToolsUiProvider.tsx @@ -32,6 +32,7 @@ export interface MeasureToolsUiProviderOptions { enableSheetMeasurement?: boolean; } + export class MeasureToolsUiItemsProvider implements UiItemsProvider { public readonly id = "MeasureToolsUiItemsProvider"; private _props?: MeasureToolsUiProviderOptions; From 9dbe7a2f07731300984ec124ddb170c7c630a8b5 Mon Sep 17 00:00:00 2001 From: Mike Nguyen Date: Tue, 27 Aug 2024 13:03:56 -0400 Subject: [PATCH 2/4] feat: add tracking for distance, clear measure, widget, selection --- packages/itwin/measure-tools/README.md | 69 +++++++++++++++++++ .../measure-tools/src/api/MeasurementTool.ts | 19 ++++- .../src/tools/ClearMeasurementsTool.ts | 6 +- .../src/tools/MeasureDistanceTool.ts | 12 +++- .../src/tools/MeasureToolDefinitions.ts | 9 +-- .../src/ui-2.0/MeasureToolsUiProvider.tsx | 8 ++- .../src/ui-2.0/MeasurementPropertyWidget.tsx | 14 +++- 7 files changed, 121 insertions(+), 16 deletions(-) diff --git a/packages/itwin/measure-tools/README.md b/packages/itwin/measure-tools/README.md index 689c00424..8e84e2ce4 100644 --- a/packages/itwin/measure-tools/README.md +++ b/packages/itwin/measure-tools/README.md @@ -99,3 +99,72 @@ An application can further customize UI event behavior by registering override h A concrete example of this customization is an application that has measurements organized into multiple groups. One group may be "frozen" due to some application state (state that the measure-tools library may be unaware of) and should not be cleared by the clear measurements tool. So the application would register a custom UI event handler that would cause those measurements to be ignored when the clear measurement tool is invoked. + +### Usage Tracking + +This package allows consumers to track the usage of specific features. + +This can be achieved by passing `onFeatureUsed` function to `MeasureToolsUiProvider`. The function is invoked with feature message as the feature is being used. The message is under this format: + +`feature-{feature being used}-event-{specific event related to the feature}` + +List of tracked features and their respective events: + +**Measure Distance**: + +- `"feature-measure-distance-event-trigger"` - when measure distance feature starts being used + +- `"feature-measure-distance-event-cancel"` - when measure distance feature stops being used + +- `"feature-measure-distance-event-start-point"` - when start point of the distance is registered + +- `"feature-measure-distance-event-end-point"` - when end point of the distance is registered + +**Clear Measurements**: + +- `"feature-clear-measurements-event-trigger"` - when clear measurements is triggered and clear all measurements on the screen + +**Property Widget**: + +- `"feature-property-widget-event-no-data"` - when the property widget is empty / not showed on the widget panel + +- `"feature-property-widget-event-update"` - when the property widget is updated with newest measurements results + +**Selection Set**: + +- `"feature-selection-set-event-change"` - when the selection set is updated with newly selected element(s) in the model + +### Example for Usage Tracking + +In this case, we create a sample [Itwin Viewer](https://www.npmjs.com/package/@itwin/web-viewer-react) and configure `MeasureToolsUiProvider` + +```ts +import { MeasureToolsUiItemsProvider } from "@itwin/measure-tools-react"; + +const App: React.FC = () => { + // Viewer Setup here... + return ( +
+ { + console.log(`MeasureTools [${feature}] used`); + }, + }), + ]} + /> +
+ ); +}; + +export default App; +``` + diff --git a/packages/itwin/measure-tools/src/api/MeasurementTool.ts b/packages/itwin/measure-tools/src/api/MeasurementTool.ts index 95396c4bd..95bddf004 100644 --- a/packages/itwin/measure-tools/src/api/MeasurementTool.ts +++ b/packages/itwin/measure-tools/src/api/MeasurementTool.ts @@ -110,10 +110,16 @@ export class SelectionHolder { /** Useful base class for tools */ export abstract class PrimitiveToolBase extends PrimitiveTool { + private _onFeatureUsed?: (feature: string) => void; + protected get feature(): Feature | undefined { return undefined; } + protected get onFeatureUsed(): ((feature: string) => void) | undefined { + return this._onFeatureUsed; + } + public override async onPostInstall(): Promise { await super.onPostInstall(); @@ -177,6 +183,15 @@ export abstract class PrimitiveToolBase extends PrimitiveTool { ); } + protected handleFeature(feature: string) { + this.onFeatureUsed?.(feature); + } + + constructor(onFeatureUsed?: (feature: string) => void) { + super(); + this._onFeatureUsed = onFeatureUsed; + } + // For most of our cases we expect to draw our decorations when suspended also public override decorateSuspended(context: DecorateContext): void { this.decorate(context); @@ -216,8 +231,8 @@ export abstract class MeasurementToolBase< return true; } - constructor() { - super(); + constructor(onFeatureUsed?: (feature: string) => void) { + super(onFeatureUsed); this._toolModel = this.createToolModel(); this._toolModel.synchMeasurementsWithSelectionSet = true; // Sync by default diff --git a/packages/itwin/measure-tools/src/tools/ClearMeasurementsTool.ts b/packages/itwin/measure-tools/src/tools/ClearMeasurementsTool.ts index 5b114ddd5..a8c3054e0 100644 --- a/packages/itwin/measure-tools/src/tools/ClearMeasurementsTool.ts +++ b/packages/itwin/measure-tools/src/tools/ClearMeasurementsTool.ts @@ -36,8 +36,8 @@ export class ClearMeasurementsTool extends PrimitiveToolBase { return MeasureToolsFeatures.Tools_ClearMeasurements; } - constructor() { - super(); + constructor(onFeatureUsed?: (feature: string) => void) { + super(onFeatureUsed); } public override requireWriteableTarget(): boolean { @@ -53,7 +53,7 @@ export class ClearMeasurementsTool extends PrimitiveToolBase { public override async onPostInstall(): Promise { await super.onPostInstall(); - + this.handleFeature("feature-clear-measurements-event-trigger"); // NOTE: If we were laying out measurements in a tool, by virtue of how tools run, those measurements will have persisted by the time // we install this clear tool diff --git a/packages/itwin/measure-tools/src/tools/MeasureDistanceTool.ts b/packages/itwin/measure-tools/src/tools/MeasureDistanceTool.ts index 9b8c2730a..7d053c4f0 100644 --- a/packages/itwin/measure-tools/src/tools/MeasureDistanceTool.ts +++ b/packages/itwin/measure-tools/src/tools/MeasureDistanceTool.ts @@ -57,19 +57,25 @@ MeasureDistanceToolModel return MeasureToolsFeatures.Tools_MeasureDistance; } - constructor(enableSheetMeasurements = false) { - super(); + constructor(enableSheetMeasurements = false, onFeatureUsed?: (feature: string) => void) { + super(onFeatureUsed); this._enableSheetMeasurements = enableSheetMeasurements; } public override async onPostInstall(): Promise { await super.onPostInstall(); + this.handleFeature("feature-measure-distance-event-trigger"); if (this._enableSheetMeasurements) { this._drawingTypeCache = new DrawingDataCache(); await this._drawingTypeCache.updateDrawingTypeCache(this.iModel); } } + public override async onCleanup(): Promise { + await super.onCleanup(); + this.handleFeature("feature-measure-distance-event-cancel"); + } + public async onRestartTool(): Promise { const tool = new MeasureDistanceTool(this._enableSheetMeasurements); if (await tool.run()) return; @@ -94,6 +100,7 @@ MeasureDistanceToolModel this.toolModel.currentState ) { this.toolModel.setMeasurementViewport(viewType); + this.handleFeature("feature-measure-distance-event-start-point"); this.toolModel.setStartPoint(viewType, ev.point); await this.sheetMeasurementsDataButtonDown(ev); this._sendHintsToAccuDraw(ev); @@ -101,6 +108,7 @@ MeasureDistanceToolModel } else if ( MeasureDistanceToolModel.State.SetEndPoint === this.toolModel.currentState ) { + this.handleFeature("feature-measure-distance-event-end-point"); this.toolModel.setEndPoint(viewType, ev.point, false); await this.onReinitialize(); } diff --git a/packages/itwin/measure-tools/src/tools/MeasureToolDefinitions.ts b/packages/itwin/measure-tools/src/tools/MeasureToolDefinitions.ts index 6f02b9fa9..f0c9b5300 100644 --- a/packages/itwin/measure-tools/src/tools/MeasureToolDefinitions.ts +++ b/packages/itwin/measure-tools/src/tools/MeasureToolDefinitions.ts @@ -35,14 +35,14 @@ export class MeasureToolDefinitions { }); } - public static getMeasureDistanceToolCommand(enableSheetMeasurements: boolean) { + public static getMeasureDistanceToolCommand(enableSheetMeasurements: boolean, onFeatureUsed?: (feature: string) => void) { return new ToolItemDef({ toolId: MeasureDistanceTool.toolId, iconSpec: MeasureDistanceTool.iconSpec, label: () => MeasureDistanceTool.flyover, tooltip: () => MeasureDistanceTool.description, execute: () => { - const tool = new MeasureDistanceTool(enableSheetMeasurements); + const tool = new MeasureDistanceTool(enableSheetMeasurements, onFeatureUsed); void tool.run(); }, }); @@ -104,7 +104,7 @@ export class MeasureToolDefinitions { }); } - public static get clearMeasurementsToolCommand() { + public static getClearMeasurementsToolCommand(onFeatureUsed?: (feature: string) => void) { return new ToolItemDef({ toolId: ClearMeasurementsTool.toolId, iconSpec: ClearMeasurementsTool.iconSpec, @@ -112,7 +112,8 @@ export class MeasureToolDefinitions { label: () => ClearMeasurementsTool.flyover, tooltip: () => ClearMeasurementsTool.description, execute: () => { - void IModelApp.tools.run(ClearMeasurementsTool.toolId); + const tool = new ClearMeasurementsTool(onFeatureUsed); + void tool.run(); }, }); } diff --git a/packages/itwin/measure-tools/src/ui-2.0/MeasureToolsUiProvider.tsx b/packages/itwin/measure-tools/src/ui-2.0/MeasureToolsUiProvider.tsx index 1d50648b2..6fec587a6 100644 --- a/packages/itwin/measure-tools/src/ui-2.0/MeasureToolsUiProvider.tsx +++ b/packages/itwin/measure-tools/src/ui-2.0/MeasureToolsUiProvider.tsx @@ -30,6 +30,8 @@ export interface MeasureToolsUiProviderOptions { }; // If we check for sheet to 3d transformation when measuring in sheets enableSheetMeasurement?: boolean; + // Callback that is invoked when a tracked feature is used. + onFeatureUsed?: (feature: string) => void; } @@ -51,7 +53,7 @@ export class MeasureToolsUiItemsProvider implements UiItemsProvider { const featureFlags = MeasureTools.featureFlags; const tools: ToolItemDef[] = []; if (!featureFlags?.hideDistanceTool) { - tools.push(MeasureToolDefinitions.getMeasureDistanceToolCommand(this._props?.enableSheetMeasurement ?? false)); + tools.push(MeasureToolDefinitions.getMeasureDistanceToolCommand(this._props?.enableSheetMeasurement ?? false, this._props?.onFeatureUsed)); } if (!featureFlags?.hideAreaTool) { tools.push(MeasureToolDefinitions.getMeasureAreaToolCommand(this._props?.enableSheetMeasurement ?? false)); @@ -94,7 +96,7 @@ export class MeasureToolsUiItemsProvider implements UiItemsProvider { return [ ToolbarHelper.createToolbarItemFromItemDef( 100, - MeasureToolDefinitions.clearMeasurementsToolCommand, + MeasureToolDefinitions.getClearMeasurementsToolCommand(this._props?.onFeatureUsed), { isHidden: new ConditionalBooleanValue( () => isSheetViewActive() || !MeasurementUIEvents.isClearMeasurementButtonVisible, @@ -131,7 +133,7 @@ export class MeasureToolsUiItemsProvider implements UiItemsProvider { widgets.push({ id: MeasurementPropertyWidgetId, label: MeasureTools.localization.getLocalizedString("MeasureTools:Generic.measurements"), - content: , + content: , defaultState: WidgetState.Hidden, icon: "icon-measure", }); diff --git a/packages/itwin/measure-tools/src/ui-2.0/MeasurementPropertyWidget.tsx b/packages/itwin/measure-tools/src/ui-2.0/MeasurementPropertyWidget.tsx index a4da50169..95a4376ca 100644 --- a/packages/itwin/measure-tools/src/ui-2.0/MeasurementPropertyWidget.tsx +++ b/packages/itwin/measure-tools/src/ui-2.0/MeasurementPropertyWidget.tsx @@ -22,6 +22,14 @@ import { SvgCopy } from "@itwin/itwinui-icons-react"; import { IconButton } from "@itwin/itwinui-react"; import type { PrimitiveValue } from "@itwin/appui-abstract"; +/** + * Props for `MeasureTools` + * @public + */ +export interface MeasureToolsProps { + onFeatureUsed?: (feature: string) => void; +} + export function useSpecificWidgetDef(id: string) { const frontstageDef = useActiveFrontstageDef(); return frontstageDef?.findWidgetDef(id); @@ -30,7 +38,7 @@ export function useSpecificWidgetDef(id: string) { export const MeasurementPropertyWidgetId = "measure-tools-property-widget"; // eslint-disable-next-line @typescript-eslint/naming-convention -export const MeasurementPropertyWidget = () => { +export const MeasurementPropertyWidget = (props: MeasureToolsProps) => { const activeIModelConnection = useActiveIModelConnection(); const [dataProvider] = React.useState(new SimplePropertyDataProvider()); const [lastSelectedCount, setLastSelectedCount] = React.useState(MeasurementSelectionSet.global.measurements.length); @@ -112,10 +120,11 @@ export const MeasurementPropertyWidget = () => { // addProperty will raise onDataChanged. If we have no data, raise it ourselves. if (!data.length) { + props.onFeatureUsed?.("feature-property-widget-event-no-data"); dataProvider.onDataChanged.raiseEvent(); return; } - + props.onFeatureUsed?.("feature-property-widget-event-update"); // Reverse the order. Last selected measurement should display up top. data = data.reverse(); transientIds = transientIds.reverse(); @@ -130,6 +139,7 @@ export const MeasurementPropertyWidget = () => { }; const onSelectionChanged = async (args: MeasurementSelectionSetEvent | Measurement[]) => { + props.onFeatureUsed?.("feature-selection-set-event-change"); // Only collapse if we are adding/removing more than one at once let collapseAll: boolean; if (Array.isArray(args)) { From 75b0afcec4750b2c5c727e0546dbbebb320d6c3a Mon Sep 17 00:00:00 2001 From: Mike Nguyen Date: Tue, 27 Aug 2024 14:26:56 -0400 Subject: [PATCH 3/4] fix: onfeatureused not required --- .vscode/settings.json | 116 +++++++++--------- .../src/ui-2.0/MeasureToolsUiProvider.tsx | 7 +- 2 files changed, 63 insertions(+), 60 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index beb3c139d..bd18703e2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,61 +1,61 @@ -{ - "editor.insertSpaces": true, - "editor.tabSize": 2, - "editor.trimAutoWhitespace": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": [ - "source.fixAll" - ], +// { +// "editor.insertSpaces": true, +// "editor.tabSize": 2, +// "editor.trimAutoWhitespace": true, +// "editor.defaultFormatter": "esbenp.prettier-vscode", +// "editor.formatOnSave": false, +// "editor.codeActionsOnSave": [ +// "source.fixAll" +// ], - "files.associations": { - ".nycrc": "json", - }, - "files.exclude": { - "**/coverage": true, - "**/node_modules": true, - "**/lib": true, - "**/build": true - }, - "files.insertFinalNewline": true, - "files.trimFinalNewlines": true, - "files.trimTrailingWhitespace": true, +// "files.associations": { +// ".nycrc": "json", +// }, +// "files.exclude": { +// "**/coverage": true, +// "**/node_modules": true, +// "**/lib": true, +// "**/build": true +// }, +// "files.insertFinalNewline": true, +// "files.trimFinalNewlines": true, +// "files.trimTrailingWhitespace": true, - "[markdown]": { - "editor.tabSize": 4, - }, +// "[markdown]": { +// "editor.tabSize": 4, +// }, - "importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.count": 150, - "importSorter.importStringConfiguration.quoteMark": "double", - "importSorter.importStringConfiguration.tabSize": 2, - "importSorter.importStringConfiguration.trailingComma": "multiLine", - "importSorter.generalConfiguration.sortOnBeforeSave": true, - "importSorter.sortConfiguration.customOrderingRules.defaultNumberOfEmptyLinesAfterGroup": 0, - "importSorter.sortConfiguration.removeUnusedImports": true, - "importSorter.sortConfiguration.customOrderingRules.rules": [ - { - "type": "importMember", - "regex": "^$", - "orderLevel": 5, - "disableSort": true - }, - { - "regex": "^[^.@]", - "orderLevel": 10 - }, - { - "regex": "^[@]", - "orderLevel": 15 - }, - { - "regex": "^[.]", - "orderLevel": 30 - } - ], - "eslint.workingDirectories": [ - { - "mode": "auto" - } - ], - "editor.formatOnSaveMode": "modifications" -} +// "importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.count": 150, +// "importSorter.importStringConfiguration.quoteMark": "double", +// "importSorter.importStringConfiguration.tabSize": 2, +// "importSorter.importStringConfiguration.trailingComma": "multiLine", +// "importSorter.generalConfiguration.sortOnBeforeSave": true, +// "importSorter.sortConfiguration.customOrderingRules.defaultNumberOfEmptyLinesAfterGroup": 0, +// "importSorter.sortConfiguration.removeUnusedImports": true, +// "importSorter.sortConfiguration.customOrderingRules.rules": [ +// { +// "type": "importMember", +// "regex": "^$", +// "orderLevel": 5, +// "disableSort": true +// }, +// { +// "regex": "^[^.@]", +// "orderLevel": 10 +// }, +// { +// "regex": "^[@]", +// "orderLevel": 15 +// }, +// { +// "regex": "^[.]", +// "orderLevel": 30 +// } +// ], +// "eslint.workingDirectories": [ +// { +// "mode": "auto" +// } +// ], +// "editor.formatOnSaveMode": "modifications" +// } diff --git a/packages/itwin/measure-tools/src/ui-2.0/MeasureToolsUiProvider.tsx b/packages/itwin/measure-tools/src/ui-2.0/MeasureToolsUiProvider.tsx index 423acd07a..9b9771cfc 100644 --- a/packages/itwin/measure-tools/src/ui-2.0/MeasureToolsUiProvider.tsx +++ b/packages/itwin/measure-tools/src/ui-2.0/MeasureToolsUiProvider.tsx @@ -39,7 +39,9 @@ export interface MeasureToolsUiProviderOptions { export class MeasureToolsUiItemsProvider implements UiItemsProvider { public readonly id = "MeasureToolsUiItemsProvider"; - private _props: RecursiveRequired; + private _props: Omit, 'onFeatureUsed'> & { + onFeatureUsed?: (feature: string) => void; + }; constructor(props?: MeasureToolsUiProviderOptions) { this._props = { @@ -51,6 +53,7 @@ export class MeasureToolsUiItemsProvider implements UiItemsProvider { }, enableSheetMeasurement: props?.enableSheetMeasurement ?? false, stageUsageList: props?.stageUsageList ?? [StageUsage.General], + onFeatureUsed: props?.onFeatureUsed, }; } @@ -64,7 +67,7 @@ export class MeasureToolsUiItemsProvider implements UiItemsProvider { const featureFlags = MeasureTools.featureFlags; const tools: ToolItemDef[] = []; if (!featureFlags?.hideDistanceTool) { - tools.push(MeasureToolDefinitions.getMeasureDistanceToolCommand(this._props?.enableSheetMeasurement, this._props?.onFeatureUsed)); + tools.push(MeasureToolDefinitions.getMeasureDistanceToolCommand(this._props.enableSheetMeasurement, this._props?.onFeatureUsed)); } if (!featureFlags?.hideAreaTool) { tools.push(MeasureToolDefinitions.getMeasureAreaToolCommand(this._props.enableSheetMeasurement)); From c86f9cf7274f05a3666f3dc60981e3bde8b20c06 Mon Sep 17 00:00:00 2001 From: Mike Nguyen Date: Tue, 27 Aug 2024 14:29:09 -0400 Subject: [PATCH 4/4] fix: add back setting --- .vscode/settings.json | 116 +++++++++++++++++++++--------------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index bd18703e2..beb3c139d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,61 +1,61 @@ -// { -// "editor.insertSpaces": true, -// "editor.tabSize": 2, -// "editor.trimAutoWhitespace": true, -// "editor.defaultFormatter": "esbenp.prettier-vscode", -// "editor.formatOnSave": false, -// "editor.codeActionsOnSave": [ -// "source.fixAll" -// ], +{ + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.trimAutoWhitespace": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": [ + "source.fixAll" + ], -// "files.associations": { -// ".nycrc": "json", -// }, -// "files.exclude": { -// "**/coverage": true, -// "**/node_modules": true, -// "**/lib": true, -// "**/build": true -// }, -// "files.insertFinalNewline": true, -// "files.trimFinalNewlines": true, -// "files.trimTrailingWhitespace": true, + "files.associations": { + ".nycrc": "json", + }, + "files.exclude": { + "**/coverage": true, + "**/node_modules": true, + "**/lib": true, + "**/build": true + }, + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, -// "[markdown]": { -// "editor.tabSize": 4, -// }, + "[markdown]": { + "editor.tabSize": 4, + }, -// "importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.count": 150, -// "importSorter.importStringConfiguration.quoteMark": "double", -// "importSorter.importStringConfiguration.tabSize": 2, -// "importSorter.importStringConfiguration.trailingComma": "multiLine", -// "importSorter.generalConfiguration.sortOnBeforeSave": true, -// "importSorter.sortConfiguration.customOrderingRules.defaultNumberOfEmptyLinesAfterGroup": 0, -// "importSorter.sortConfiguration.removeUnusedImports": true, -// "importSorter.sortConfiguration.customOrderingRules.rules": [ -// { -// "type": "importMember", -// "regex": "^$", -// "orderLevel": 5, -// "disableSort": true -// }, -// { -// "regex": "^[^.@]", -// "orderLevel": 10 -// }, -// { -// "regex": "^[@]", -// "orderLevel": 15 -// }, -// { -// "regex": "^[.]", -// "orderLevel": 30 -// } -// ], -// "eslint.workingDirectories": [ -// { -// "mode": "auto" -// } -// ], -// "editor.formatOnSaveMode": "modifications" -// } + "importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.count": 150, + "importSorter.importStringConfiguration.quoteMark": "double", + "importSorter.importStringConfiguration.tabSize": 2, + "importSorter.importStringConfiguration.trailingComma": "multiLine", + "importSorter.generalConfiguration.sortOnBeforeSave": true, + "importSorter.sortConfiguration.customOrderingRules.defaultNumberOfEmptyLinesAfterGroup": 0, + "importSorter.sortConfiguration.removeUnusedImports": true, + "importSorter.sortConfiguration.customOrderingRules.rules": [ + { + "type": "importMember", + "regex": "^$", + "orderLevel": 5, + "disableSort": true + }, + { + "regex": "^[^.@]", + "orderLevel": 10 + }, + { + "regex": "^[@]", + "orderLevel": 15 + }, + { + "regex": "^[.]", + "orderLevel": 30 + } + ], + "eslint.workingDirectories": [ + { + "mode": "auto" + } + ], + "editor.formatOnSaveMode": "modifications" +}