a couple more fixes for table filter (#15121)

* a few fixes

* add comments and fix for non-ideal scenario
This commit is contained in:
Alan Ren
2021-04-14 10:20:29 -07:00
committed by GitHub
parent 5922047f6b
commit 842a33e318
9 changed files with 131 additions and 106 deletions

View File

@@ -132,25 +132,24 @@
margin: 0;
}
.slick-header-menuicon
.slick-header-menu a.monaco-button.monaco-text-button.slick-header-menuicon
{
display: inline-block;
height: 16px;
margin-right: 4px;
vertical-align: middle;
width: 16px;
background-position: 2px center;
background-repeat: no-repeat;
padding-left: 18px;
text-align: left;
width: 100%;
padding: 0 0 0 18px;
margin: 0px;
}
.slick-header-menuicon.ascending {
background: url('sort-asc.gif');
background-position: center center;
background-repeat: no-repeat;
background-image: url('sort-asc.gif');
}
.slick-header-menuicon.descending {
background: url('sort-desc.gif');
background-position: center center;
background-repeat: no-repeat;
background-image: url('sort-desc.gif');
}
.slick-header-menucontent {

View File

@@ -12,15 +12,18 @@ import { DisposableStore } from 'vs/base/common/lifecycle';
import { withNullAsUndefined } from 'vs/base/common/types';
import { IDisposableDataProvider } from 'sql/base/common/dataProvider';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IInputBoxStyles, InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
export type HeaderFilterCommands = 'sort-asc' | 'sort-desc';
export interface CommandEventArgs<T extends Slick.SlickData> {
grid: Slick.Grid<T>,
column: Slick.Column<T>,
command: HeaderFilterCommands
}
export interface ITableFilterStyle extends IButtonStyles, IInputBoxStyles { }
const ShowFilterText: string = localize('headerFilter.showFilter', "Show Filter");
export class HeaderFilter<T extends Slick.SlickData> {
@@ -35,9 +38,12 @@ export class HeaderFilter<T extends Slick.SlickData> {
private okButton?: Button;
private clearButton?: Button;
private cancelButton?: Button;
private sortAscButton?: Button;
private sortDescButton?: Button;
private searchInputBox?: InputBox;
private workingFilters!: Array<string>;
private columnDef!: FilterableColumn<T>;
private buttonStyles?: IButtonStyles;
private filterStyles?: ITableFilterStyle;
private disposableStore = new DisposableStore();
private _enabled: boolean = true;
@@ -117,36 +123,31 @@ export class HeaderFilter<T extends Slick.SlickData> {
.remove();
}
private addMenuItem(menu: JQuery<HTMLElement>, columnDef: Slick.Column<T>, title: string, command: HeaderFilterCommands) {
const $item = jQuery('<div class="slick-header-menuitem">')
.data('command', command)
.data('column', columnDef)
.bind('click', async (e) => await this.handleMenuItemClick(e, command, columnDef))
.appendTo(menu);
const $icon = jQuery('<div class="slick-header-menuicon">')
.appendTo($item);
if (title === 'Sort Ascending') {
$icon.get(0).className += ' ascending';
} else if (title === 'Sort Descending') {
$icon.get(0).className += ' descending';
}
jQuery('<span class="slick-header-menucontent">')
.text(title)
.appendTo($item);
private createButtonMenuItem(menu: HTMLElement, columnDef: Slick.Column<T>, title: string, command: HeaderFilterCommands, iconClass: string): Button {
const buttonContainer = document.createElement('div');
menu.appendChild(buttonContainer);
const button = new Button(buttonContainer);
button.icon = { classNames: `slick-header-menuicon ${iconClass}` };
button.label = title;
button.onDidClick(async () => {
await this.handleMenuItemClick(command, columnDef);
});
return button;
}
private addMenuInput(menu: JQuery<HTMLElement>, columnDef: Slick.Column<T>): void {
const self = this;
jQuery('<input class="input" placeholder="Search" style="margin-top: 5px; width: 206px">')
.data('column', columnDef)
.bind('keyup', async (e) => {
const filterVals = await this.getFilterValuesByInput(jQuery(e.target));
self.updateFilterInputs(menu, columnDef, filterVals);
})
.appendTo(menu);
private createSearchInput(menu: JQuery<HTMLElement>, columnDef: Slick.Column<T>): InputBox {
const inputContainer = document.createElement('div');
inputContainer.style.width = '206px';
inputContainer.style.marginTop = '5px';
menu[0].appendChild(inputContainer);
const input = new InputBox(inputContainer, this.contextViewProvider, {
placeholder: localize('table.searchPlaceHolder', "Search")
});
input.onDidChange(async (newString) => {
const filterVals = await this.getFilterValuesByInput(columnDef, newString);
this.updateFilterInputs(menu, columnDef, filterVals);
});
return input;
}
private updateFilterInputs(menu: JQuery<HTMLElement>, columnDef: FilterableColumn<T>, filterItems: Array<string>) {
@@ -171,11 +172,32 @@ export class HeaderFilter<T extends Slick.SlickData> {
});
}
private showFilter(filterButton: HTMLElement): void {
private async showFilter(filterButton: HTMLElement): Promise<void> {
await this.createFilterMenu(filterButton);
const menuElement = this.$menu[0];
// Get the absolute coordinates of the filter button
const offset = jQuery(filterButton).offset();
// Calculate the position of menu item
let menuleft = offset.left - menuElement.offsetWidth + filterButton.offsetWidth;
let menutop = offset.top + filterButton.offsetHeight;
// Make sure the entire menu is on screen.
// If there is not enough vertical space under the filter button, we will move up the menu.
// If the left of the menu is off screen (negative value), we will show the menu next to the left edge of window.
// We don't really consider the case when there is not enough space to show the entire menu since in that case the application is not usable already.
if (menutop + menuElement.offsetHeight > window.innerHeight) {
menutop = window.innerHeight - menuElement.offsetHeight;
}
menuleft = menuleft > 0 ? menuleft : 0;
this.contextViewProvider.showContextView({
getAnchor: () => filterButton,
getAnchor: () => {
return {
x: menuleft,
y: menutop
};
},
render: (container: HTMLElement) => {
this.renderFilter(filterButton, container).catch(onUnexpectedError);
container.appendChild(menuElement);
return {
dispose: () => {
if (this.$menu) {
@@ -184,11 +206,14 @@ export class HeaderFilter<T extends Slick.SlickData> {
}
}
};
},
focus: () => {
this.okButton.focus();
}
});
}
private async renderFilter(filterButton: HTMLElement, container: HTMLElement) {
private async createFilterMenu(filterButton: HTMLElement) {
const target = withNullAsUndefined(filterButton);
const $menuButton = jQuery(target);
this.columnDef = $menuButton.data('column');
@@ -220,16 +245,18 @@ export class HeaderFilter<T extends Slick.SlickData> {
}
if (!this.$menu) {
this.$menu = jQuery('<div class="slick-header-menu">').appendTo(container);
// first add it to the document so that we can get the actual size of the menu
// later, it will be added to the correct container
this.$menu = jQuery('<div class="slick-header-menu">').appendTo(document.body);
}
this.$menu.empty();
this.addMenuItem(this.$menu, this.columnDef, localize('table.sortAscending', "Sort Ascending"), 'sort-asc');
this.addMenuItem(this.$menu, this.columnDef, localize('table.sortDescending', "Sort Descending"), 'sort-desc');
this.addMenuInput(this.$menu, this.columnDef);
this.sortAscButton = this.createButtonMenuItem(this.$menu[0], this.columnDef, localize('table.sortAscending', "Sort Ascending"), 'sort-asc', 'ascending');
this.sortDescButton = this.createButtonMenuItem(this.$menu[0], this.columnDef, localize('table.sortDescending', "Sort Descending"), 'sort-desc', 'descending');
this.searchInputBox = this.createSearchInput(this.$menu, this.columnDef);
let filterOptions = '<label><input type="checkbox" value="-1" />(Select All)</label>';
let filterOptions = '<label class="filter-option"><input type="checkbox" value="-1" />(Select All)</label>';
for (let i = 0; i < filterItems.length; i++) {
const filtered = this.workingFilters.some(x => x === filterItems[i]);
@@ -251,58 +278,50 @@ export class HeaderFilter<T extends Slick.SlickData> {
this.okButton.label = localize('headerFilter.ok', "OK");
this.okButton.title = localize('headerFilter.ok', "OK");
this.okButton.element.id = 'filter-ok-button';
const okElement = jQuery('#filter-ok-button');
okElement.bind('click', (ev) => {
this.okButton.onDidClick(() => {
this.columnDef.filterValues = this.workingFilters.splice(0);
this.setButtonImage($menuButton, this.columnDef.filterValues.length > 0);
this.handleApply(ev, this.columnDef);
this.handleApply(this.columnDef);
});
this.clearButton = new Button($clearButtonDiv.get(0), { secondary: true });
this.clearButton.label = localize('headerFilter.clear', "Clear");
this.clearButton.title = localize('headerFilter.clear', "Clear");
this.clearButton.element.id = 'filter-clear-button';
const clearElement = jQuery('#filter-clear-button');
clearElement.bind('click', (ev) => {
this.okButton.onDidClick(() => {
this.columnDef.filterValues!.length = 0;
this.setButtonImage($menuButton, false);
this.handleApply(ev, this.columnDef);
this.handleApply(this.columnDef);
});
this.cancelButton = new Button($cancelButtonDiv.get(0), { secondary: true });
this.cancelButton.label = localize('headerFilter.cancel', "Cancel");
this.cancelButton.title = localize('headerFilter.cancel', "Cancel");
this.cancelButton.element.id = 'filter-cancel-button';
const cancelElement = jQuery('#filter-cancel-button');
cancelElement.bind('click', () => this.hideMenu());
this.cancelButton.onDidClick(() => {
this.hideMenu();
});
this.applyStyles();
jQuery(':checkbox', $filter).bind('click', (e) => {
this.workingFilters = this.changeWorkingFilter(filterItems, this.workingFilters, jQuery(e.target));
});
this.okButton.focus();
}
public style(styles: IButtonStyles): void {
this.buttonStyles = styles;
public style(styles: ITableFilterStyle): void {
this.filterStyles = styles;
this.applyStyles();
}
private applyStyles() {
if (this.buttonStyles) {
const styles = this.buttonStyles;
if (this.okButton) {
this.okButton.style(styles);
}
if (this.clearButton) {
this.clearButton.style(styles);
}
if (this.cancelButton) {
this.cancelButton.style(styles);
}
if (this.filterStyles) {
this.okButton?.style(this.filterStyles);
this.cancelButton?.style(this.filterStyles);
this.clearButton?.style(this.filterStyles);
this.sortAscButton?.style(this.filterStyles);
this.sortDescButton?.style(this.filterStyles);
this.searchInputBox?.style(this.filterStyles);
}
}
@@ -355,7 +374,7 @@ export class HeaderFilter<T extends Slick.SlickData> {
}
}
private handleApply(e: JQuery.Event<HTMLElement, null>, columnDef: Slick.Column<T>) {
private handleApply(columnDef: Slick.Column<T>) {
this.hideMenu();
const provider = this.grid.getData() as IDisposableDataProvider<T>;
@@ -365,9 +384,7 @@ export class HeaderFilter<T extends Slick.SlickData> {
this.grid.updateRowCount();
this.grid.render();
}
this.onFilterApplied.notify({ grid: this.grid, column: columnDef }, e, self);
e.preventDefault();
e.stopPropagation();
this.onFilterApplied.notify({ grid: this.grid, column: columnDef });
}
private getFilterValues(dataView: Slick.DataProvider<T>, column: Slick.Column<T>): Array<any> {
@@ -381,10 +398,8 @@ export class HeaderFilter<T extends Slick.SlickData> {
return Array.from(seen);
}
private async getFilterValuesByInput($input: JQuery<HTMLElement>): Promise<Array<string>> {
const column = $input.data('column'),
filter = $input.val() as string,
dataView = this.grid.getData() as IDisposableDataProvider<T>,
private async getFilterValuesByInput(column: Slick.Column<T>, filter: string): Promise<Array<string>> {
const dataView = this.grid.getData() as IDisposableDataProvider<T>,
seen: Set<any> = new Set();
let columnValues: any[];
@@ -423,7 +438,7 @@ export class HeaderFilter<T extends Slick.SlickData> {
return Array.from(seen).sort((v) => { return v; });
}
private async handleMenuItemClick(e: JQuery.Event<HTMLElement, null>, command: HeaderFilterCommands, columnDef: Slick.Column<T>) {
private async handleMenuItemClick(command: HeaderFilterCommands, columnDef: Slick.Column<T>) {
this.hideMenu();
const provider = this.grid.getData() as IDisposableDataProvider<T>;
@@ -442,10 +457,7 @@ export class HeaderFilter<T extends Slick.SlickData> {
grid: this.grid,
column: columnDef,
command: command
}, e, self);
e.preventDefault();
e.stopPropagation();
});
}
public get enabled(): boolean {

View File

@@ -326,3 +326,22 @@ export const defaultInfoButtonStyles: IInfoButtonStyleOverrides = {
export function attachInfoButtonStyler(widget: IThemable, themeService: IThemeService, style?: IInfoButtonStyleOverrides): IDisposable {
return attachStyler(themeService, { ...defaultInfoButtonStyles, ...style }, widget);
}
export function attachTableFilterStyler(widget: IThemable, themeService: IThemeService): IDisposable {
return attachStyler(themeService, {
inputBackground: cr.inputBackground,
inputForeground: cr.inputForeground,
inputBorder: cr.inputBorder,
buttonForeground: cr.buttonForeground,
buttonBackground: cr.buttonBackground,
buttonHoverBackground: cr.buttonHoverBackground,
buttonSecondaryForeground: cr.buttonSecondaryForeground,
buttonSecondaryBackground: cr.buttonSecondaryBackground,
buttonSecondaryHoverBackground: cr.buttonSecondaryHoverBackground,
buttonBorder: cr.buttonBorder,
buttonSecondaryBorder: cr.buttonSecondaryBorder,
buttonDisabledBorder: cr.buttonDisabledBorder,
buttonDisabledBackground: cr.buttonDisabledBackground,
buttonDisabledForeground: cr.buttonDisabledForeground
}, widget);
}

View File

@@ -15,7 +15,7 @@ import { ComponentBase } from 'sql/workbench/browser/modelComponents/componentBa
import { Table } from 'sql/base/browser/ui/table/table';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { attachTableFilterStyler, attachTableStyler } from 'sql/platform/theme/common/styler';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { getContentHeight, getContentWidth, Dimension, isAncestor } from 'vs/base/browser/dom';
import { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectionModel.plugin';
@@ -34,7 +34,6 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { ILogService } from 'vs/platform/log/common/log';
import { TableCellClickEventArgs } from 'sql/base/browser/ui/table/plugins/tableColumn';
import { HyperlinkCellValue, HyperlinkColumn } from 'sql/base/browser/ui/table/plugins/hyperlinkColumn.plugin';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
export enum ColumnSizingMode {
@@ -503,7 +502,7 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
private registerFilterPlugin() {
const filterPlugin = new HeaderFilter<Slick.SlickData>(this.contextViewService);
this._register(attachButtonStyler(filterPlugin, this.themeService));
this._register(attachTableFilterStyler(filterPlugin, this.themeService));
this._filterPlugin = filterPlugin;
this._filterPlugin.onFilterApplied.subscribe((e, args) => {
let filterValues = (<any>args).column.filterValues;

View File

@@ -45,8 +45,8 @@ import { ITableStyles } from 'sql/base/browser/ui/table/interfaces';
import { TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys';
import { LocalizedStrings } from 'sql/workbench/contrib/assessment/common/strings';
import { ConnectionManagementInfo } from 'sql/platform/connection/common/connectionManagementInfo';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { attachTableFilterStyler } from 'sql/platform/theme/common/styler';
export const ASMTRESULTSVIEW_SELECTOR: string = 'asmt-results-view-component';
export const ROW_HEIGHT: number = 25;
@@ -319,7 +319,7 @@ export class AsmtResultsViewComponent extends TabChild implements IAssessmentCom
columns.unshift(columnDef);
let filterPlugin = new HeaderFilter<Slick.SlickData>(this._contextViewService);
this._register(attachButtonStyler(filterPlugin, this._themeService));
this._register(attachTableFilterStyler(filterPlugin, this._themeService));
this.filterPlugin = filterPlugin;
this.filterPlugin.onFilterApplied.subscribe((e, args) => {
let filterValues = args.column.filterValues;
@@ -588,10 +588,8 @@ export class AsmtResultsViewComponent extends TabChild implements IAssessmentCom
return seen.sort((v) => { return v; });
}
private async getFilterValuesByInput($input: JQuery<HTMLElement>): Promise<Array<string>> {
const column = $input.data('column'),
filter = $input.val() as string,
dataView = this['grid'].getData() as Slick.DataProvider<Slick.SlickData>,
private async getFilterValuesByInput(column: Slick.Column<any>, filter: string): Promise<Array<string>> {
const dataView = this['grid'].getData() as Slick.DataProvider<Slick.SlickData>,
seen: Array<any> = [];
for (let i = 0; i < dataView.getLength(); i++) {

View File

@@ -33,7 +33,7 @@ import { TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys';
import { IColorTheme } from 'vs/platform/theme/common/themeService';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { attachTableFilterStyler } from 'sql/platform/theme/common/styler';
export const JOBSVIEW_SELECTOR: string = 'jobsview-component';
export const ROW_HEIGHT: number = 45;
@@ -183,7 +183,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
this.rowDetail = rowDetail;
columns.unshift(this.rowDetail.getColumnDefinition());
let filterPlugin = new HeaderFilter<IItem>(this._contextViewService);
this._register(attachButtonStyler(filterPlugin, this._themeService));
this._register(attachTableFilterStyler(filterPlugin, this._themeService));
this.filterPlugin = filterPlugin;
jQuery(this._gridEl.nativeElement).empty();
jQuery(this.actionBarContainer.nativeElement).empty();

View File

@@ -34,7 +34,7 @@ import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IColorTheme } from 'vs/platform/theme/common/themeService';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { attachTableFilterStyler } from 'sql/platform/theme/common/styler';
export const NOTEBOOKSVIEW_SELECTOR: string = 'notebooksview-component';
@@ -183,7 +183,7 @@ export class NotebooksViewComponent extends JobManagementView implements OnInit,
this.rowDetail = rowDetail;
columns.unshift(this.rowDetail.getColumnDefinition());
let filterPlugin = new HeaderFilter<IItem>(this._contextViewService);
this._register(attachButtonStyler(filterPlugin, this._themeService));
this._register(attachTableFilterStyler(filterPlugin, this._themeService));
this.filterPlugin = filterPlugin;
jQuery(this._gridEl.nativeElement).empty();
jQuery(this.actionBarContainer.nativeElement).empty();

View File

@@ -6,7 +6,7 @@
import 'vs/css!./media/gridPanel';
import { ITableStyles, ITableMouseEvent, FilterableColumn } from 'sql/base/browser/ui/table/interfaces';
import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { attachTableFilterStyler, attachTableStyler } from 'sql/platform/theme/common/styler';
import QueryRunner, { QueryGridDataProvider } from 'sql/workbench/services/query/common/queryRunner';
import { ResultSetSummary, IColumn, ICellValue } from 'sql/workbench/services/query/common/query';
import { VirtualizedCollection } from 'sql/base/browser/ui/table/asyncDataView';
@@ -50,7 +50,6 @@ import { IQueryEditorConfiguration } from 'sql/platform/query/common/query';
import { Orientation } from 'vs/base/browser/ui/splitview/splitview';
import { IQueryModelService } from 'sql/workbench/services/query/common/queryModel';
import { HeaderFilter } from 'sql/base/browser/ui/table/plugins/headerFilter.plugin';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { HybridDataProvider } from 'sql/base/browser/ui/table/hybridDataProvider';
const ROW_HEIGHT = 29;
@@ -529,7 +528,7 @@ export abstract class GridTableBase<T> extends Disposable implements IView {
}));
if (this.enableFilteringFeature) {
this.filterPlugin = new HeaderFilter(this.contextViewService);
attachButtonStyler(this.filterPlugin, this.themeService);
attachTableFilterStyler(this.filterPlugin, this.themeService);
this.table.registerPlugin(this.filterPlugin);
}
if (this.styles) {

View File

@@ -6,7 +6,7 @@
import 'vs/css!./media/resourceViewerTable';
import * as azdata from 'azdata';
import { Table } from 'sql/base/browser/ui/table/table';
import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { attachTableFilterStyler, attachTableStyler } from 'sql/platform/theme/common/styler';
import { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectionModel.plugin';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
@@ -24,7 +24,6 @@ import { ColumnDefinition } from 'sql/workbench/browser/editor/resourceViewer/re
import { Emitter } from 'vs/base/common/event';
import { ContextMenuAnchor } from 'sql/workbench/contrib/resourceViewer/browser/resourceViewerEditor';
import { LoadingSpinnerPlugin } from 'sql/base/browser/ui/table/plugins/loadingSpinner.plugin';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
export class ResourceViewerTable extends Disposable {
@@ -57,7 +56,7 @@ export class ResourceViewerTable extends Disposable {
this._resourceViewerTable.setSelectionModel(new RowSelectionModel());
let filterPlugin = new HeaderFilter<azdata.DataGridItem>(this._contextViewService);
this._register(attachButtonStyler(filterPlugin, this._themeService));
this._register(attachTableFilterStyler(filterPlugin, this._themeService));
this._register(attachTableStyler(this._resourceViewerTable, this._themeService));
this._register(this._resourceViewerTable.onClick(this.onTableClick, this));
this._register(this._resourceViewerTable.onContextMenu((e: ITableMouseEvent) => {