diff --git a/src/components/fuzzySearch.ts b/src/components/fuzzySearch.ts index f5ace0c..f6609a3 100644 --- a/src/components/fuzzySearch.ts +++ b/src/components/fuzzySearch.ts @@ -3,15 +3,23 @@ import { WebviewApi } from "vscode-webview"; export enum SearchType { variables = 'variables', - features = 'features' + features = 'features', + projects = 'projects', } + +type LocalStorageKey = `${SearchType}${string}` | SearchType + //View Provider -export const getCustomDropdown = (optionElements: string, selectedValue: string) => { +export const getCustomDropdown = (optionElements: string, selectedValue: string, folderIndex?: number, dataType?: string) => { + const folderAttribute = folderIndex !== undefined ? `data-folder="${folderIndex}"` : '' + const dataTypeAttribute = dataType ? `data-type="${dataType}"` : '' + const inputId = folderIndex !== undefined ? `id="dropdown-input-${folderIndex}"` : '' + const optionsListId = folderIndex !== undefined ? `id="dropdown-optionsList-${folderIndex}"` : '' return `
- + - ` @@ -28,11 +36,13 @@ export const getCustomDropdown = (optionElements: string, selectedValue: string) */ export function handleFuzzySearchDropdown( vscode: WebviewApi, - searchType: SearchType, + localStorageKey: LocalStorageKey, fuseOptions: Fuse.IFuseOptions = { keys: ['label', 'value'], threshold: 0.5, - } + }, + inputSelector: string = '.dropdown-input', + optionsListSelector: string = '.dropdown-options' ) { window.addEventListener('message', (event) => { const message = event.data @@ -42,17 +52,10 @@ export function handleFuzzySearchDropdown( } }) - const localStorageData = localStorage.getItem(searchType) - - if (!localStorageData) { - return - } - - const input = document.querySelector('.dropdown-input') as HTMLInputElement - const optionsList = document.querySelector('.dropdown-options') as HTMLDivElement - - const searchData: { label: string, value?: string }[] = JSON.parse(localStorageData) - const fuse = new Fuse([...searchData], fuseOptions) + const input = document.querySelector(inputSelector) as HTMLInputElement + const optionsList = document.querySelector(optionsListSelector) as HTMLDivElement + const folderIndex = optionsList?.getAttribute('data-folder') + const selectedValueKey = `selectedValue${folderIndex || ''}` const createAndAppendOption = (displayValue: string, dataAttributeValue?: string) => { const option = document.createElement('div') @@ -63,15 +66,57 @@ export function handleFuzzySearchDropdown( // Event listeners to handle dropdown behavior document.addEventListener('click', () => { + if (!optionsList?.classList.contains('visible')) { + return + } + const selectedValue = localStorage.getItem(selectedValueKey) + if (selectedValue) { + input.value = selectedValue + } optionsList?.classList.remove('visible') }) + optionsList?.addEventListener('click', (event) => { + const selectedOption = event.target as HTMLElement + if (selectedOption.tagName === 'DIV') { + const selectedValue = selectedOption.getAttribute('data-value') + const selectedType = optionsList.getAttribute('data-type') + if (selectedValue) { + input.value = selectedOption.innerText + vscode.postMessage({ + type: selectedType || 'key', + value: selectedValue, + ...(optionsList.dataset.folder && { folderIndex: optionsList.dataset.folder }) + }) + optionsList.classList.remove('visible') + } + } + }) + input?.addEventListener('click', (event) => { + const { value } = event.target as HTMLInputElement + localStorage.setItem(selectedValueKey, value) event.stopPropagation() optionsList?.classList.toggle('visible') }) + + let localStorageData: string | null = localStorage.getItem(localStorageKey) + const refreshLocalStorageData = () => { + localStorageData = localStorage.getItem(localStorageKey) + } input?.addEventListener('input', () => { + if (!localStorageData) { + vscode.postMessage({ + type: 'setFuseData', + ...(folderIndex && { folderIndex: folderIndex }) + }) + refreshLocalStorageData() + return + } + + const searchData: { label: string, value?: string }[] = JSON.parse(localStorageData) + const fuse = new Fuse([...searchData], fuseOptions) const inputValue = input?.value.toLowerCase().trim() optionsList.innerHTML = '' @@ -95,18 +140,4 @@ export function handleFuzzySearchDropdown( } }) - optionsList.addEventListener('click', (event) => { - const selectedOption = event.target as HTMLElement - if (selectedOption.tagName === 'DIV') { - const selectedValue = selectedOption.getAttribute('data-value') - if (selectedValue) { - vscode.postMessage({ - type: 'key', - value: selectedValue, - }) - optionsList.classList.remove('visible') - } - } - }) - } \ No newline at end of file diff --git a/src/views/home/HomeViewProvider.ts b/src/views/home/HomeViewProvider.ts index 801fdae..d943cfe 100644 --- a/src/views/home/HomeViewProvider.ts +++ b/src/views/home/HomeViewProvider.ts @@ -1,24 +1,27 @@ import * as vscode from 'vscode' import { getNonce } from '../../utils/getNonce' -import { BaseCLIController, OrganizationsCLIController, ProjectsCLIController } from '../../cli' +import { BaseCLIController, OrganizationsCLIController, Project, ProjectsCLIController } from '../../cli' import path from 'path' import { COMMAND_LOGOUT } from '../../commands/logout' import { KEYS, StateManager } from '../../StateManager' import { updateRepoConfig } from '../../utils/updateRepoConfigProject' import { executeRefreshAllCommand } from '../../commands' import { loginAndRefresh } from '../../utils/loginAndRefresh' +import { SearchType, getCustomDropdown } from '../../components/fuzzySearch' type HomeViewMessage = | { type: 'project' | 'organization', value: string, folderIndex: number } | { type: 'config', folderIndex: number } | { type: 'login', folderIndex: number } | { type: 'logout' } + | { type: 'setFuseData', folderIndex: number } export class HomeViewProvider implements vscode.WebviewViewProvider { _view?: vscode.WebviewView _doc?: vscode.TextDocument isRefreshing: boolean = false webviewIsDisposed: boolean = false + projectOptions: Record = [] constructor(private readonly _extensionUri: vscode.Uri) {} @@ -46,6 +49,11 @@ export class HomeViewProvider implements vscode.WebviewViewProvider { const folder = vscode.workspace.workspaceFolders?.[data.folderIndex] if (!folder) { return } + if (data.type === 'setFuseData') { + this.setFuseSearchData(this.projectOptions[folder.index], folder) + return + } + if (data.type === 'login') { await loginAndRefresh([folder]) webviewView.webview.html = await this._getHtmlForWebview(webviewView.webview) @@ -116,6 +124,21 @@ export class HomeViewProvider implements vscode.WebviewViewProvider { await this.refreshAll() } + private postMessageToWebview(message: unknown) { + this._view?.webview.postMessage(message) + } + + private setFuseSearchData(data: Data[], folder: vscode.WorkspaceFolder) { + this.postMessageToWebview({ type: 'searchData', searchType: `${SearchType.projects}${folder.index}`, value: JSON.stringify(data)}) + } + + private getSelectedProject(activeProjectKey: string | undefined, projects: Record) { + if (!activeProjectKey) { + return '' + } + return projects[activeProjectKey]?.name || projects[activeProjectKey]?.key + } + private async getBodyHtml(folder: vscode.WorkspaceFolder, showHeader?: boolean): Promise { const organizationsController = new OrganizationsCLIController(folder) const projectsController = new ProjectsCLIController(folder) @@ -125,7 +148,13 @@ export class HomeViewProvider implements vscode.WebviewViewProvider { const activeOrganizationName = StateManager.getFolderState(folder.name, KEYS.ORGANIZATION)?.name const isLoggedIn = StateManager.getFolderState(folder.name, KEYS.LOGGED_IN) - const projectId = `project${folder.index}` + const projectOptions = Object.values(projects).map((project) => ({ + label: project.name || project.key, + value: project.key, + })) + this.projectOptions[folder.index] = projectOptions + this.setFuseSearchData(projectOptions, folder) + const organizationId = `organization${folder.index}` const editButtonId = `editConfigButton${folder.index}` @@ -133,13 +162,10 @@ export class HomeViewProvider implements vscode.WebviewViewProvider { const orgOptions = Object.values(organizations).map((organization) => `${organization.display_name || organization.name}` ) - const projectOptions = Object.values(projects).map((project) => - `${project.name}` + const projectOptionElements = Object.values(projects).map((project) => + `
${project.name}
` ) - if (!activeProjectKey) { - projectOptions.unshift(`Select a project...`) - } - + const selectedProject = this.getSelectedProject(activeProjectKey, projects) return ` ${ showHeader ? ` @@ -162,9 +188,7 @@ export class HomeViewProvider implements vscode.WebviewViewProvider { - - ${projectOptions.join('')} - + ${getCustomDropdown(projectOptionElements.join(''), selectedProject, folder.index, 'project')} @@ -207,6 +231,14 @@ export class HomeViewProvider implements vscode.WebviewViewProvider { const homeViewStylesUri = webview.asWebviewUri( vscode.Uri.joinPath(this._extensionUri, 'media', 'styles', 'homeView.css'), ) + const dropdownStylesUri = webview.asWebviewUri( + vscode.Uri.joinPath( + this._extensionUri, + 'media', + 'styles', + 'customDropdown.css', + ), + ) const webViewUri = webview.asWebviewUri( vscode.Uri.joinPath(this._extensionUri, 'out', 'homeView.js'), ) @@ -236,6 +268,7 @@ export class HomeViewProvider implements vscode.WebviewViewProvider { + diff --git a/src/views/inspector/InspectorViewProvider.ts b/src/views/inspector/InspectorViewProvider.ts index a70cc3c..8be1e37 100644 --- a/src/views/inspector/InspectorViewProvider.ts +++ b/src/views/inspector/InspectorViewProvider.ts @@ -32,6 +32,7 @@ type InspectorViewMessage = } | { type: 'folder'; value: number } | { type: 'jsonReadonly'; value: string } + | { type: 'setFuseData' } const htmlMessage = (message: string) => ` @@ -96,16 +97,8 @@ export class InspectorViewProvider implements vscode.WebviewViewProvider { this.selectedKey = '' - // Send variables data to the webview to initialize fuse - const variableOptions = this.orderedVariables.map(variable => ({ - label: variable.key - })) - const featureOptions = this.orderedFeatures.map(feature => ({ - label: feature.name || feature.key, - value: feature._id - })) - this.postMessageToWebview({type: 'searchData', searchType: SearchType.variables, value: JSON.stringify(variableOptions)}) - this.postMessageToWebview({type: 'searchData', searchType: SearchType.features, value: JSON.stringify(featureOptions)}) + // Send data to the webview to initialize Fuse + this.setFuseSearchData() } catch (e) { vscode.window.showErrorMessage( `Error initializing features and variables in inspector: ${e}`, @@ -133,6 +126,11 @@ export class InspectorViewProvider implements vscode.WebviewViewProvider { webviewView.webview.onDidReceiveMessage( async (data: InspectorViewMessage) => { + if (data.type === 'setFuseData') { + this.setFuseSearchData() + return + } + if (data.type === 'variableOrFeature') { this.selectedType = data.value this.selectedKey = '' @@ -175,6 +173,18 @@ export class InspectorViewProvider implements vscode.WebviewViewProvider { this.webviewIsDisposed = false } + private setFuseSearchData() { + const variableOptions = this.orderedVariables.map(variable => ({ + label: variable.key + })) + const featureOptions = this.orderedFeatures.map(feature => ({ + label: feature.name || feature.key, + value: feature._id + })) + this.postMessageToWebview({ type: 'searchData', searchType: SearchType.variables, value: JSON.stringify(variableOptions) }) + this.postMessageToWebview({ type: 'searchData', searchType: SearchType.features, value: JSON.stringify(featureOptions) }) + } + private async refreshInspectorView() { if (!this._view?.webview) { return diff --git a/src/webview/homeView.ts b/src/webview/homeView.ts index 34a88d0..ceb54b2 100644 --- a/src/webview/homeView.ts +++ b/src/webview/homeView.ts @@ -1,4 +1,5 @@ import { provideVSCodeDesignSystem, vsCodeDropdown, vsCodeOption, Dropdown } from "@vscode/webview-ui-toolkit"; +import { SearchType, handleFuzzySearchDropdown } from "../components/fuzzySearch"; provideVSCodeDesignSystem().register(vsCodeDropdown(), vsCodeOption()); @@ -46,6 +47,18 @@ function main() { dropdown.addEventListener('change', handleDropdownValueChange); } + const optionsLists = document.querySelectorAll('.dropdown-options') as NodeListOf + optionsLists.forEach(optionsList => { + const folderIndexAttribute = optionsList.getAttribute('data-folder') + const folderIndex = folderIndexAttribute ? folderIndexAttribute : null + + if (folderIndex) { + handleFuzzySearchDropdown(vscode, `${SearchType.projects}${folderIndex}`, undefined, `#dropdown-input-${folderIndex}`, `#dropdown-optionsList-${folderIndex}`) + } else { + handleFuzzySearchDropdown(vscode, SearchType.projects) + } + }) + const editConfigButtons = document.getElementsByClassName("edit-config-button") for (let i = 0; i < editConfigButtons.length; i++) { // Edit config button diff --git a/src/webview/inspectorView.ts b/src/webview/inspectorView.ts index 7d6d1a4..0cc8453 100644 --- a/src/webview/inspectorView.ts +++ b/src/webview/inspectorView.ts @@ -20,6 +20,10 @@ if (focusedElement) { window.addEventListener('message', (event) => { const message = event.data + // if message type is 'searchData' then we don't want to post it back to the extension + if (message.type === 'searchData') { + return + } vscode.postMessage(message) })