mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-19 17:22:48 -05:00
Agent: Filtering & Sorting (#1441)
* have a working filter * fixed error details when filtering * filtering with styling done * fix transition styling * fixed more styling issues * optimized errors when switching pages * added sorting functionality * removed dead code * fixed styling issues when sorting * added sorting with styling for every column * code review comments * fixed styling issue when a bigger filter was applied, followed by a smaller one and then cleared * use absolute paths in imports * fixed issues with styling when sorting is performed on a filtered dataset
This commit is contained in:
1
src/sql/base/browser/ui/table/media/down-inverse.svg
Normal file
1
src/sql/base/browser/ui/table/media/down-inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#2d2d30;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#c5c5c5;}</style></defs><title>CollapseChevronDown_md_16x</title><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/><path class="icon-vs-out" d="M15.444,6.061,8,13.5.556,6.061,3.03,3.586,8,8.556l4.97-4.97Z" style="display: none;"/><path class="icon-vs-bg" d="M14.03,6.061,8,12.091,1.97,6.061,3.03,5,8,9.97,12.97,5Z"/></svg>
|
||||
|
After Width: | Height: | Size: 507 B |
1
src/sql/base/browser/ui/table/media/down.svg
Normal file
1
src/sql/base/browser/ui/table/media/down.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>CollapseChevronDown_md_16x</title><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/><path class="icon-vs-out" d="M15.444,6.061,8,13.5.556,6.061,3.03,3.586,8,8.556l4.97-4.97Z" style="display: none;"/><path class="icon-vs-bg" d="M14.03,6.061,8,12.091,1.97,6.061,3.03,5,8,9.97,12.97,5Z"/></svg>
|
||||
|
After Width: | Height: | Size: 507 B |
1
src/sql/base/browser/ui/table/media/filter.svg
Normal file
1
src/sql/base/browser/ui/table/media/filter.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{font-size:12px;font-family:FullMDL2Assets, Full MDL2 Assets;}</style></defs><title>filter_16x16</title><text class="cls-1" transform="translate(0 12)"> </text><path d="M0,1.53H16V3.24l-6,6v6.27H6V9.22l-6-6ZM15,2.82V2.53H1v.29l6,6v5.69H9V8.8Z"/></svg>
|
||||
|
After Width: | Height: | Size: 363 B |
1
src/sql/base/browser/ui/table/media/filter_inverse.svg
Normal file
1
src/sql/base/browser/ui/table/media/filter_inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{font-size:12px;font-family:FullMDL2Assets, Full MDL2 Assets;}.cls-1,.cls-2{fill:#fff;}</style></defs><title>filter_inverse_16x16</title><text class="cls-1" transform="translate(0.03 12.1)"> </text><path class="cls-2" d="M.05,1.63H16V3.33l-6,6v6.27H6V9.31l-6-6ZM15,2.91V2.62H1v.29l6,6v5.69H9V8.89Z"/></svg>
|
||||
|
After Width: | Height: | Size: 418 B |
@@ -31,4 +31,117 @@
|
||||
height: 5px;
|
||||
margin-left: 4px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.slick-header-menubutton {
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
bottom: 0;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 0;
|
||||
width: 18px;
|
||||
background-image: url('down.svg');
|
||||
}
|
||||
|
||||
.vs-dark .slick-header-menubutton {
|
||||
background-image: url('down-inverse.svg');
|
||||
}
|
||||
|
||||
.slick-header-menubutton.filtered {
|
||||
background-image: url('filter.svg');
|
||||
}
|
||||
|
||||
.vs-dark .slick-header-menubutton.filtered {
|
||||
background-image: url('filter_inverse.svg');
|
||||
}
|
||||
|
||||
.slick-header-menu {
|
||||
background: none repeat scroll 0 0 white;
|
||||
border: 1px solid #BFBDBD;
|
||||
min-width: 175px;
|
||||
padding: 4px;
|
||||
z-index: 100000;
|
||||
cursor: default;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.vs-dark .slick-header-menu {
|
||||
background: none repeat scroll 0 0 #333333;
|
||||
}
|
||||
|
||||
.slick-header-menu a.monaco-button.monaco-text-button {
|
||||
width: 60px;
|
||||
margin: 6px 6px 6px 6px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.slick-header-menu .filter
|
||||
{
|
||||
border: 1px solid #BFBDBD;
|
||||
font-size: 8pt;
|
||||
height: 400px;
|
||||
margin-top: 6px;
|
||||
overflow: scroll;
|
||||
padding: 4px;
|
||||
white-space: nowrap;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.slick-header-menuitem
|
||||
{
|
||||
border: 1px solid transparent;
|
||||
padding: 2px 4px;
|
||||
cursor: pointer;
|
||||
list-style: none outside none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.slick-header-menuicon
|
||||
{
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.slick-header-menuicon.ascending {
|
||||
background: url('sort-asc.gif');
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.slick-header-menuicon.descending {
|
||||
background: url('sort-desc.gif');
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.slick-header-menucontent {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.slick-header-menuitem:hover {
|
||||
border-color: #BFBDBD;
|
||||
}
|
||||
|
||||
.header-overlay, .cell-overlay, .selection-cell-overlay {
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.vs-dark .slick-header-menu > input.input {
|
||||
color: #4a4a4a;
|
||||
}
|
||||
353
src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts
Normal file
353
src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
// Adopted and converted to typescript from https://github.com/danny-sg/slickgrid-spreadsheet-plugins/blob/master/ext.headerfilter.js
|
||||
// heavily modified
|
||||
import 'vs/css!sql/base/browser/ui/table/media/table';
|
||||
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { SlickGrid } from 'angular2-slickgrid';
|
||||
import { Button } from '../../button/button';
|
||||
import { attachButtonStyler } from 'sql/common/theme/styler';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
export class HeaderFilter {
|
||||
|
||||
public onFilterApplied = new Slick.Event();
|
||||
public onCommand = new Slick.Event();
|
||||
|
||||
private grid;
|
||||
private handler = new Slick.EventHandler();
|
||||
private defaults = {
|
||||
filterImage: 'src/sql/media/icons/filter.svg',
|
||||
sortAscImage: 'sort-asc.gif',
|
||||
sortDescImage: 'sort-desc.gif'
|
||||
};
|
||||
|
||||
private $menu;
|
||||
private options: any;
|
||||
private okButton: Button;
|
||||
private clearButton: Button;
|
||||
private cancelButton: Button;
|
||||
private workingFilters: any;
|
||||
private columnDef: any;
|
||||
|
||||
constructor(options: any, private _themeService: IThemeService) {
|
||||
this.options = mixin(options, this.defaults, false);
|
||||
}
|
||||
|
||||
public init(grid: Slick.Grid<any>): void {
|
||||
this.grid = grid;
|
||||
this.handler.subscribe(this.grid.onHeaderCellRendered, (e, args) => this.handleHeaderCellRendered(e , args))
|
||||
.subscribe(this.grid.onBeforeHeaderCellDestroy, (e, args) => this.handleBeforeHeaderCellDestroy(e, args))
|
||||
.subscribe(this.grid.onClick, (e) => this.handleBodyMouseDown)
|
||||
.subscribe(this.grid.onColumnsResized, () => this.columnsResized());
|
||||
|
||||
this.grid.setColumns(this.grid.getColumns());
|
||||
|
||||
$(document.body).bind('mousedown', this.handleBodyMouseDown);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.handler.unsubscribeAll();
|
||||
$(document.body).unbind('mousedown', this.handleBodyMouseDown);
|
||||
}
|
||||
|
||||
private handleBodyMouseDown = (e) => {
|
||||
if (this.$menu && this.$menu[0] !== e.target && !$.contains(this.$menu[0], e.target)) {
|
||||
this.hideMenu();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
private hideMenu() {
|
||||
if (this.$menu) {
|
||||
this.$menu.remove();
|
||||
this.$menu = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleHeaderCellRendered(e, args) {
|
||||
let column = args.column;
|
||||
if (column.id === '_detail_selector') {
|
||||
return;
|
||||
}
|
||||
let $el = $('<div></div>')
|
||||
.addClass('slick-header-menubutton')
|
||||
.data('column', column);
|
||||
|
||||
$el.bind('click', (e) => this.showFilter(e)).appendTo(args.node);
|
||||
}
|
||||
|
||||
private handleBeforeHeaderCellDestroy(e, args) {
|
||||
$(args.node)
|
||||
.find('.slick-header-menubutton')
|
||||
.remove();
|
||||
}
|
||||
|
||||
private addMenuItem(menu, columnDef, title, command, image) {
|
||||
let $item = $('<div class="slick-header-menuitem">')
|
||||
.data('command', command)
|
||||
.data('column', columnDef)
|
||||
.bind('click', (e) => this.handleMenuItemClick(e, command, columnDef))
|
||||
.appendTo(menu);
|
||||
|
||||
let $icon = $('<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';
|
||||
}
|
||||
|
||||
$('<span class="slick-header-menucontent">')
|
||||
.text(title)
|
||||
.appendTo($item);
|
||||
}
|
||||
|
||||
private addMenuInput(menu, columnDef) {
|
||||
const self = this;
|
||||
$('<input class="input" placeholder="Search" style="margin-top: 5px; width: 206px">')
|
||||
.data('column', columnDef)
|
||||
.bind('keyup', (e) => {
|
||||
let filterVals = this.getFilterValuesByInput($(e.target));
|
||||
self.updateFilterInputs(menu, columnDef, filterVals);
|
||||
})
|
||||
.appendTo(menu);
|
||||
}
|
||||
|
||||
private updateFilterInputs(menu, columnDef, filterItems) {
|
||||
let filterOptions = '<label><input type="checkbox" value="-1" />(Select All)</label>';
|
||||
columnDef.filterValues = columnDef.filterValues || [];
|
||||
|
||||
// WorkingFilters is a copy of the filters to enable apply/cancel behaviour
|
||||
this.workingFilters = columnDef.filterValues.slice(0);
|
||||
|
||||
for (let i = 0; i < filterItems.length; i++) {
|
||||
let filtered = _.contains(this.workingFilters, filterItems[i]);
|
||||
|
||||
filterOptions += '<label><input type="checkbox" value="' + i + '"'
|
||||
+ (filtered ? ' checked="checked"' : '')
|
||||
+ '/>' + filterItems[i] + '</label>';
|
||||
}
|
||||
let $filter = menu.find('.filter');
|
||||
$filter.empty().append($(filterOptions));
|
||||
|
||||
$(':checkbox', $filter).bind('click', (e) => {
|
||||
this.workingFilters = this.changeWorkingFilter(filterItems, this.workingFilters, $(e.target));
|
||||
});
|
||||
}
|
||||
|
||||
private showFilter(e) {
|
||||
let $menuButton = $(e.target);
|
||||
this.columnDef = $menuButton.data('column');
|
||||
|
||||
this.columnDef.filterValues = this.columnDef.filterValues || [];
|
||||
|
||||
// WorkingFilters is a copy of the filters to enable apply/cancel behaviour
|
||||
this.workingFilters = this.columnDef.filterValues.slice(0);
|
||||
|
||||
let filterItems;
|
||||
|
||||
if (this.workingFilters.length === 0) {
|
||||
// Filter based all available values
|
||||
filterItems = this.getFilterValues(this.grid.getData(), this.columnDef);
|
||||
}
|
||||
else {
|
||||
// Filter based on current dataView subset
|
||||
filterItems = this.getAllFilterValues(this.grid.getData().getItems(), this.columnDef);
|
||||
}
|
||||
|
||||
if (!this.$menu) {
|
||||
this.$menu = $('<div class="slick-header-menu">').appendTo(document.body);
|
||||
}
|
||||
|
||||
this.$menu.empty();
|
||||
|
||||
this.addMenuItem(this.$menu, this.columnDef, 'Sort Ascending', 'sort-asc', this.options.sortAscImage);
|
||||
this.addMenuItem(this.$menu, this.columnDef, 'Sort Descending', 'sort-desc', this.options.sortDescImage);
|
||||
this.addMenuInput(this.$menu, this.columnDef);
|
||||
|
||||
let filterOptions = '<label><input type="checkbox" value="-1" />(Select All)</label>';
|
||||
|
||||
for (let i = 0; i < filterItems.length; i++) {
|
||||
let filtered = _.contains(this.workingFilters, filterItems[i]);
|
||||
if (filterItems[i] && filterItems[i].indexOf('Error:') < 0) {
|
||||
filterOptions += '<label><input type="checkbox" value="' + i + '"'
|
||||
+ (filtered ? ' checked="checked"' : '')
|
||||
+ '/>' + filterItems[i] + '</label>';
|
||||
}
|
||||
}
|
||||
let $filter = $('<div class="filter">')
|
||||
.append($(filterOptions))
|
||||
.appendTo(this.$menu);
|
||||
|
||||
this.okButton = new Button(this.$menu.get(0));
|
||||
this.okButton.label = 'OK';
|
||||
this.okButton.title = 'OK';
|
||||
this.okButton.element.id = 'filter-ok-button';
|
||||
let okElement = $('#filter-ok-button');
|
||||
okElement.bind('click', (ev) => {
|
||||
this.columnDef.filterValues = this.workingFilters.splice(0);
|
||||
this.setButtonImage($menuButton, this.columnDef.filterValues.length > 0);
|
||||
this.handleApply(ev, this.columnDef);
|
||||
});
|
||||
|
||||
this.clearButton = new Button(this.$menu.get(0));
|
||||
this.clearButton.label = 'Clear';
|
||||
this.clearButton.title = 'Clear';
|
||||
this.clearButton.element.id = 'filter-clear-button';
|
||||
let clearElement = $('#filter-clear-button');
|
||||
clearElement.bind('click', (ev) => {
|
||||
this.columnDef.filterValues.length = 0;
|
||||
this.setButtonImage($menuButton, false);
|
||||
this.handleApply(ev, this.columnDef);
|
||||
});
|
||||
|
||||
this.cancelButton = new Button(this.$menu.get(0));
|
||||
this.cancelButton.label = 'Cancel';
|
||||
this.cancelButton.title = 'Cancel';
|
||||
this.cancelButton.element.id = 'filter-cancel-button';
|
||||
let cancelElement = $('#filter-cancel-button');
|
||||
cancelElement.bind('click', () => this.hideMenu());
|
||||
attachButtonStyler(this.okButton, this._themeService);
|
||||
attachButtonStyler(this.clearButton, this._themeService);
|
||||
attachButtonStyler(this.cancelButton, this._themeService);
|
||||
|
||||
$(':checkbox', $filter).bind('click', (e) => {
|
||||
this.workingFilters = this.changeWorkingFilter(filterItems, this.workingFilters, $(e.target));
|
||||
});
|
||||
|
||||
let offset = $(e.target).offset();
|
||||
let left = offset.left - this.$menu.width() + $(e.target).width() - 8;
|
||||
|
||||
let menutop = offset.top + $(e.target).height();
|
||||
|
||||
if (menutop + offset.top > $(window).height()) {
|
||||
menutop -= (this.$menu.height() + $(e.target).height() + 8);
|
||||
}
|
||||
this.$menu.css('top', menutop)
|
||||
.css('left', (left > 0 ? left : 0));
|
||||
}
|
||||
|
||||
private columnsResized() {
|
||||
this.hideMenu();
|
||||
}
|
||||
|
||||
private changeWorkingFilter(filterItems, workingFilters, $checkbox) {
|
||||
let value = $checkbox.val();
|
||||
let $filter = $checkbox.parent().parent();
|
||||
|
||||
if ($checkbox.val() < 0) {
|
||||
// Select All
|
||||
if ($checkbox.prop('checked')) {
|
||||
$(':checkbox', $filter).prop('checked', true);
|
||||
workingFilters = filterItems.slice(0);
|
||||
} else {
|
||||
$(':checkbox', $filter).prop('checked', false);
|
||||
workingFilters.length = 0;
|
||||
}
|
||||
} else {
|
||||
let index = _.indexOf(workingFilters, filterItems[value]);
|
||||
|
||||
if ($checkbox.prop('checked') && index < 0) {
|
||||
workingFilters.push(filterItems[value]);
|
||||
let nextRow = filterItems[(parseInt(value)+1).toString()];
|
||||
if (nextRow && nextRow.indexOf('Error:') >= 0) {
|
||||
workingFilters.push(nextRow);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (index > -1) {
|
||||
workingFilters.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return workingFilters;
|
||||
}
|
||||
|
||||
private setButtonImage($el, filtered) {
|
||||
let element: HTMLElement = $el.get(0);
|
||||
if (filtered) {
|
||||
element.className += ' filtered';
|
||||
} else {
|
||||
let classList = element.classList;
|
||||
if (classList.contains('filtered')) {
|
||||
classList.remove('filtered');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleApply(e, columnDef) {
|
||||
this.hideMenu();
|
||||
|
||||
this.onFilterApplied.notify({ 'grid': this.grid, 'column': columnDef }, e, self);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
private getFilterValues(dataView, column) {
|
||||
let seen = [];
|
||||
for (let i = 0; i < dataView.getLength() ; i++) {
|
||||
let value = dataView.getItem(i)[column.field];
|
||||
|
||||
if (!_.contains(seen, value)) {
|
||||
seen.push(value);
|
||||
}
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
|
||||
private getFilterValuesByInput($input) {
|
||||
let column = $input.data('column'),
|
||||
filter = $input.val(),
|
||||
dataView = this.grid.getData(),
|
||||
seen = [];
|
||||
|
||||
for (let i = 0; i < dataView.getLength() ; i++) {
|
||||
let value = dataView.getItem(i)[column.field];
|
||||
|
||||
if (filter.length > 0) {
|
||||
let itemValue = !value ? '' : value;
|
||||
let lowercaseFilter = filter.toString().toLowerCase();
|
||||
let lowercaseVal = itemValue.toString().toLowerCase();
|
||||
if (!_.contains(seen, value) && lowercaseVal.indexOf(lowercaseFilter) > -1) {
|
||||
seen.push(value);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!_.contains(seen, value)) {
|
||||
seen.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _.sortBy(seen, (v) => { return v; });
|
||||
}
|
||||
|
||||
private getAllFilterValues(data, column) {
|
||||
let seen = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let value = data[i][column.field];
|
||||
|
||||
if (!_.contains(seen, value)) {
|
||||
seen.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
return _.sortBy(seen, (v) => { return v; });
|
||||
}
|
||||
|
||||
private handleMenuItemClick(e, command, columnDef) {
|
||||
this.hideMenu();
|
||||
|
||||
this.onCommand.notify({
|
||||
'grid': this.grid,
|
||||
'column': columnDef,
|
||||
'command': command
|
||||
}, e, self);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
@@ -277,8 +277,8 @@ export class RowDetailView {
|
||||
item._isPadding = false;
|
||||
item._parent = parent;
|
||||
item._offset = offset;
|
||||
item.jobId = parent.jobId;
|
||||
item.name = parent.message ? parent.message : nls.localize('rowDetailView.loadError','Loading Error...');
|
||||
parent._child = item;
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface IFindPosition {
|
||||
row: number;
|
||||
}
|
||||
|
||||
function defaultSort<T>(args: Slick.OnSortEventArgs<T>, data: Array<T>): Array<T> {
|
||||
export function defaultSort<T>(args: Slick.OnSortEventArgs<T>, data: Array<T>): Array<T> {
|
||||
let field = args.sortCol.field;
|
||||
let sign = args.sortAsc ? 1 : -1;
|
||||
return data.sort((a, b) => (a[field] === b[field] ? 0 : (a[field] > b[field] ? 1 : -1)) * sign);
|
||||
|
||||
Reference in New Issue
Block a user