mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) add + button to the filter section of the section menu
Test Plan: adds new browser tests Reviewers: paulfitz Reviewed By: paulfitz Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D2781
This commit is contained in:
parent
9696e24aac
commit
2823727da1
@ -6,12 +6,14 @@ import { colors, testId } from "app/client/ui2018/cssVars";
|
|||||||
import { icon } from "app/client/ui2018/icons";
|
import { icon } from "app/client/ui2018/icons";
|
||||||
import { menu, menuItemAsync } from "app/client/ui2018/menus";
|
import { menu, menuItemAsync } from "app/client/ui2018/menus";
|
||||||
import { dom, IDisposableOwner, IDomArgs, styled } from "grainjs";
|
import { dom, IDisposableOwner, IDomArgs, styled } from "grainjs";
|
||||||
|
import { IMenuOptions, PopupControl } from "popweasel";
|
||||||
|
|
||||||
export function filterBar(_owner: IDisposableOwner, viewSection: ViewSectionRec) {
|
export function filterBar(_owner: IDisposableOwner, viewSection: ViewSectionRec) {
|
||||||
|
const popupControls = new WeakMap<ViewFieldRec, PopupControl>();
|
||||||
return cssFilterBar(
|
return cssFilterBar(
|
||||||
testId('filter-bar'),
|
testId('filter-bar'),
|
||||||
dom.forEach(viewSection.filteredFields, (field) => makeFilterField(viewSection, field)),
|
dom.forEach(viewSection.filteredFields, (field) => makeFilterField(viewSection, field, popupControls)),
|
||||||
makePlusButton(viewSection),
|
makePlusButton(viewSection, popupControls),
|
||||||
cssSpacer(),
|
cssSpacer(),
|
||||||
dom.maybe(viewSection.filterSpecChanged, () => [
|
dom.maybe(viewSection.filterSpecChanged, () => [
|
||||||
primaryButton(
|
primaryButton(
|
||||||
@ -26,39 +28,64 @@ export function filterBar(_owner: IDisposableOwner, viewSection: ViewSectionRec)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeFilterField(viewSection: ViewSectionRec, field: ViewFieldRec) {
|
function makeFilterField(viewSection: ViewSectionRec, field: ViewFieldRec,
|
||||||
|
popupControls: WeakMap<ViewFieldRec, PopupControl>) {
|
||||||
return cssFilterBarItem(
|
return cssFilterBarItem(
|
||||||
testId('filter-field'),
|
testId('filter-field'),
|
||||||
primaryButton(
|
primaryButton(
|
||||||
testId('btn'),
|
testId('btn'),
|
||||||
cssIcon('FilterSimple'),
|
cssIcon('FilterSimple'),
|
||||||
cssMenuTextLabel(dom.text(field.label)),
|
cssMenuTextLabel(dom.text(field.label)),
|
||||||
cssBtn.cls('-saved', field.activeFilter.isSaved),
|
cssBtn.cls('-grayed', field.activeFilter.isSaved),
|
||||||
attachColumnFilterMenu(viewSection, field, {placement: 'bottom-start', attach: 'body'}),
|
attachColumnFilterMenu(viewSection, field, {
|
||||||
|
placement: 'bottom-start', attach: 'body',
|
||||||
|
trigger: ['click', (_el, popupControl) => popupControls.set(field, popupControl)]
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
deleteButton(
|
deleteButton(
|
||||||
testId('delete'),
|
testId('delete'),
|
||||||
cssIcon('CrossSmall'),
|
cssIcon('CrossSmall'),
|
||||||
cssBtn.cls('-saved', field.activeFilter.isSaved),
|
cssBtn.cls('-grayed', field.activeFilter.isSaved),
|
||||||
dom.on('click', () => field.activeFilter('')),
|
dom.on('click', () => field.activeFilter('')),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function makePlusButton(viewSectionRec: ViewSectionRec) {
|
export function addFilterMenu(fields: ViewFieldRec[], popupControls: WeakMap<ViewFieldRec, PopupControl>,
|
||||||
|
options?: IMenuOptions) {
|
||||||
|
return (
|
||||||
|
menu((ctl) => [
|
||||||
|
...fields.map((f) => (
|
||||||
|
menuItemAsync(
|
||||||
|
() => turnOnAndOpenFilter(f, popupControls),
|
||||||
|
f.label.peek(),
|
||||||
|
dom.cls('disabled', f.isFiltered),
|
||||||
|
testId('add-filter-item'),
|
||||||
|
)
|
||||||
|
)),
|
||||||
|
// We need to stop click event to propagate otherwise it would cause view section menu to
|
||||||
|
// close.
|
||||||
|
dom.on('click', (ev) => {
|
||||||
|
ctl.close();
|
||||||
|
ev.stopPropagation();
|
||||||
|
}),
|
||||||
|
], options)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function turnOnAndOpenFilter(f: ViewFieldRec, popupControls: WeakMap<ViewFieldRec, PopupControl>) {
|
||||||
|
f.activeFilter(allInclusive);
|
||||||
|
popupControls.get(f)?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePlusButton(viewSectionRec: ViewSectionRec, popupControls: WeakMap<ViewFieldRec, PopupControl>) {
|
||||||
return dom.domComputed((use) => {
|
return dom.domComputed((use) => {
|
||||||
const fields = use(use(viewSectionRec.viewFields).getObservable());
|
const fields = use(use(viewSectionRec.viewFields).getObservable());
|
||||||
const anyFilter = fields.find((f) => use(f.isFiltered));
|
const anyFilter = fields.find((f) => use(f.isFiltered));
|
||||||
return cssPlusButton(
|
return cssPlusButton(
|
||||||
cssBtn.cls('-saved'),
|
cssBtn.cls('-grayed'),
|
||||||
cssIcon('Plus'),
|
cssIcon('Plus'),
|
||||||
menu(() => fields.map((f) => (
|
addFilterMenu(fields, popupControls),
|
||||||
menuItemAsync(
|
|
||||||
() => f.activeFilter(allInclusive),
|
|
||||||
f.label.peek(),
|
|
||||||
dom.cls('disabled', f.isFiltered)
|
|
||||||
)
|
|
||||||
))),
|
|
||||||
anyFilter ? null : cssPlusLabel('Add Filter'),
|
anyFilter ? null : cssPlusLabel('Add Filter'),
|
||||||
testId('add-filter-btn')
|
testId('add-filter-btn')
|
||||||
);
|
);
|
||||||
@ -98,13 +125,13 @@ const cssBtn = styled('div', `
|
|||||||
.${cssFilterBar.className} > & {
|
.${cssFilterBar.className} > & {
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
}
|
}
|
||||||
&-saved {
|
&-grayed {
|
||||||
color: ${colors.light};
|
color: ${colors.light};
|
||||||
--icon-color: ${colors.light};
|
--icon-color: ${colors.light};
|
||||||
background-color: ${colors.slate};
|
background-color: ${colors.slate};
|
||||||
border-color: ${colors.slate};
|
border-color: ${colors.slate};
|
||||||
}
|
}
|
||||||
&-saved:hover {
|
&-grayed:hover {
|
||||||
background-color: ${colors.darkGrey};
|
background-color: ${colors.darkGrey};
|
||||||
border-color: ${colors.darkGrey};
|
border-color: ${colors.darkGrey};
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import {flipColDirection, parseSortColRefs} from 'app/client/lib/sortUtil';
|
|||||||
import {ColumnRec, DocModel, ViewFieldRec, ViewRec, ViewSectionRec} from 'app/client/models/DocModel';
|
import {ColumnRec, DocModel, ViewFieldRec, ViewRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||||
import {CustomComputed} from 'app/client/models/modelUtil';
|
import {CustomComputed} from 'app/client/models/modelUtil';
|
||||||
import {attachColumnFilterMenu} from 'app/client/ui/ColumnFilterMenu';
|
import {attachColumnFilterMenu} from 'app/client/ui/ColumnFilterMenu';
|
||||||
|
import {addFilterMenu} from 'app/client/ui/FilterBar';
|
||||||
import {makeViewLayoutMenu} from 'app/client/ui/ViewLayoutMenu';
|
import {makeViewLayoutMenu} from 'app/client/ui/ViewLayoutMenu';
|
||||||
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
|
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
|
||||||
import {colors, vars} from 'app/client/ui2018/cssVars';
|
import {colors, vars} from 'app/client/ui2018/cssVars';
|
||||||
@ -9,6 +10,7 @@ import {icon} from 'app/client/ui2018/icons';
|
|||||||
import {menu, menuDivider} from 'app/client/ui2018/menus';
|
import {menu, menuDivider} from 'app/client/ui2018/menus';
|
||||||
import {Computed, dom, fromKo, makeTestId, Observable, styled} from 'grainjs';
|
import {Computed, dom, fromKo, makeTestId, Observable, styled} from 'grainjs';
|
||||||
import difference = require('lodash/difference');
|
import difference = require('lodash/difference');
|
||||||
|
import {PopupControl} from 'popweasel';
|
||||||
|
|
||||||
const testId = makeTestId('test-section-menu-');
|
const testId = makeTestId('test-section-menu-');
|
||||||
|
|
||||||
@ -34,6 +36,8 @@ export function viewSectionMenu(docModel: DocModel, viewSection: ViewSectionRec,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const popupControls = new WeakMap<ViewFieldRec, PopupControl>();
|
||||||
|
|
||||||
return cssMenu(
|
return cssMenu(
|
||||||
testId('wrapper'),
|
testId('wrapper'),
|
||||||
dom.autoDispose(emptySortFilterObs),
|
dom.autoDispose(emptySortFilterObs),
|
||||||
@ -50,7 +54,8 @@ export function viewSectionMenu(docModel: DocModel, viewSection: ViewSectionRec,
|
|||||||
(row: number) => docModel.columns.getRowModel(row));
|
(row: number) => docModel.columns.getRowModel(row));
|
||||||
}),
|
}),
|
||||||
dom.domComputed(viewSection.filteredFields, fields =>
|
dom.domComputed(viewSection.filteredFields, fields =>
|
||||||
makeFilterPanel(viewSection, fields)),
|
makeFilterPanel(viewSection, fields, popupControls)),
|
||||||
|
makeAddFilterButton(viewSection, popupControls),
|
||||||
makeFilterBarToggle(viewSection.activeFilterBar),
|
makeFilterBarToggle(viewSection.activeFilterBar),
|
||||||
dom.domComputed(iconSuffixObs, iconSuffix => {
|
dom.domComputed(iconSuffixObs, iconSuffix => {
|
||||||
const displaySave = iconSuffix === '-unsaved';
|
const displaySave = iconSuffix === '-unsaved';
|
||||||
@ -119,6 +124,27 @@ function makeSortPanel(section: ViewSectionRec, sortSpec: number[], getColumn: (
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function makeAddFilterButton(viewSectionRec: ViewSectionRec,
|
||||||
|
popupControls: WeakMap<ViewFieldRec, PopupControl>) {
|
||||||
|
return dom.domComputed((use) => {
|
||||||
|
const fields = use(use(viewSectionRec.viewFields).getObservable());
|
||||||
|
return cssMenuText(
|
||||||
|
cssMenuIconWrapper(
|
||||||
|
cssIcon('Plus'),
|
||||||
|
addFilterMenu(fields, popupControls, {
|
||||||
|
placement: 'bottom-end',
|
||||||
|
// Attach content to triggerElem's parent, which is needed to prevent view section menu to
|
||||||
|
// close when clicking an item of the add filter menu.
|
||||||
|
attach: null
|
||||||
|
}),
|
||||||
|
testId('plus-button'),
|
||||||
|
dom.on('click', (ev) => ev.stopPropagation()),
|
||||||
|
),
|
||||||
|
cssMenuTextLabel('Add Filter'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function makeFilterBarToggle(activeFilterBar: CustomComputed<boolean>) {
|
export function makeFilterBarToggle(activeFilterBar: CustomComputed<boolean>) {
|
||||||
return cssMenuText(
|
return cssMenuText(
|
||||||
cssMenuIconWrapper(
|
cssMenuIconWrapper(
|
||||||
@ -138,7 +164,8 @@ export function makeFilterBarToggle(activeFilterBar: CustomComputed<boolean>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function makeFilterPanel(section: ViewSectionRec, filteredFields: ViewFieldRec[]) {
|
function makeFilterPanel(section: ViewSectionRec, filteredFields: ViewFieldRec[],
|
||||||
|
popupControls: WeakMap<ViewFieldRec, PopupControl>) {
|
||||||
const fields = filteredFields.map(field => {
|
const fields = filteredFields.map(field => {
|
||||||
const fieldChanged = Computed.create(null, fromKo(field.activeFilter.isSaved), (_use, isSaved) => !isSaved);
|
const fieldChanged = Computed.create(null, fromKo(field.activeFilter.isSaved), (_use, isSaved) => !isSaved);
|
||||||
return cssMenuText(
|
return cssMenuText(
|
||||||
@ -146,7 +173,10 @@ function makeFilterPanel(section: ViewSectionRec, filteredFields: ViewFieldRec[]
|
|||||||
cssMenuIconWrapper(
|
cssMenuIconWrapper(
|
||||||
cssMenuIconWrapper.cls('-changed', fieldChanged),
|
cssMenuIconWrapper.cls('-changed', fieldChanged),
|
||||||
cssIcon('FilterSimple'),
|
cssIcon('FilterSimple'),
|
||||||
attachColumnFilterMenu(section, field, {placement: 'bottom-end'}),
|
attachColumnFilterMenu(section, field, {
|
||||||
|
placement: 'bottom-end',
|
||||||
|
trigger: ['click', (_el, popupControl) => popupControls.set(field, popupControl)],
|
||||||
|
}),
|
||||||
testId('filter-icon'),
|
testId('filter-icon'),
|
||||||
),
|
),
|
||||||
cssMenuTextLabel(field.label()),
|
cssMenuTextLabel(field.label()),
|
||||||
|
@ -875,6 +875,25 @@ export async function toggleSidePanel(which: 'right'|'left', goal: 'open'|'close
|
|||||||
await driver.sleep((transitionDuration + delta) * 1000);
|
await driver.sleep((transitionDuration + delta) * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles (opens or closes) the filter bar for a section.
|
||||||
|
*/
|
||||||
|
export async function toggleFilterBar(goal: 'open'|'close'|'toggle' = 'toggle',
|
||||||
|
options: {section?: string|WebElement, save?: boolean} = {}) {
|
||||||
|
const isOpen = await driver.find('.test-filter-bar').isPresent();
|
||||||
|
if ((goal === 'close') && !isOpen ||
|
||||||
|
(goal === 'open') && isOpen ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const menu = await openSectionMenu(options.section);
|
||||||
|
await menu.findContent('.grist-floating-menu > div', /Toggle Filter Bar/).find('.test-section-menu-btn').click();
|
||||||
|
if (options.save) {
|
||||||
|
await menu.findContent('.grist-floating-menu button', /Save/).click();
|
||||||
|
await waitForServer();
|
||||||
|
}
|
||||||
|
await menu.sendKeys(Key.ESCAPE);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the section menu for a section, or the active section if no section is given.
|
* Opens the section menu for a section, or the active section if no section is given.
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user