diff --git a/app/client/ui/FilterBar.ts b/app/client/ui/FilterBar.ts index 9e56dc83..82fa13fe 100644 --- a/app/client/ui/FilterBar.ts +++ b/app/client/ui/FilterBar.ts @@ -6,12 +6,14 @@ import { colors, testId } from "app/client/ui2018/cssVars"; import { icon } from "app/client/ui2018/icons"; import { menu, menuItemAsync } from "app/client/ui2018/menus"; import { dom, IDisposableOwner, IDomArgs, styled } from "grainjs"; +import { IMenuOptions, PopupControl } from "popweasel"; export function filterBar(_owner: IDisposableOwner, viewSection: ViewSectionRec) { + const popupControls = new WeakMap(); return cssFilterBar( testId('filter-bar'), - dom.forEach(viewSection.filteredFields, (field) => makeFilterField(viewSection, field)), - makePlusButton(viewSection), + dom.forEach(viewSection.filteredFields, (field) => makeFilterField(viewSection, field, popupControls)), + makePlusButton(viewSection, popupControls), cssSpacer(), dom.maybe(viewSection.filterSpecChanged, () => [ 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) { return cssFilterBarItem( testId('filter-field'), primaryButton( testId('btn'), cssIcon('FilterSimple'), cssMenuTextLabel(dom.text(field.label)), - cssBtn.cls('-saved', field.activeFilter.isSaved), - attachColumnFilterMenu(viewSection, field, {placement: 'bottom-start', attach: 'body'}), + cssBtn.cls('-grayed', field.activeFilter.isSaved), + attachColumnFilterMenu(viewSection, field, { + placement: 'bottom-start', attach: 'body', + trigger: ['click', (_el, popupControl) => popupControls.set(field, popupControl)] + }), ), deleteButton( testId('delete'), cssIcon('CrossSmall'), - cssBtn.cls('-saved', field.activeFilter.isSaved), + cssBtn.cls('-grayed', field.activeFilter.isSaved), dom.on('click', () => field.activeFilter('')), ) ); } -function makePlusButton(viewSectionRec: ViewSectionRec) { +export function addFilterMenu(fields: ViewFieldRec[], popupControls: WeakMap, + 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) { + f.activeFilter(allInclusive); + popupControls.get(f)?.open(); +} + +function makePlusButton(viewSectionRec: ViewSectionRec, popupControls: WeakMap) { return dom.domComputed((use) => { const fields = use(use(viewSectionRec.viewFields).getObservable()); const anyFilter = fields.find((f) => use(f.isFiltered)); return cssPlusButton( - cssBtn.cls('-saved'), + cssBtn.cls('-grayed'), cssIcon('Plus'), - menu(() => fields.map((f) => ( - menuItemAsync( - () => f.activeFilter(allInclusive), - f.label.peek(), - dom.cls('disabled', f.isFiltered) - ) - ))), + addFilterMenu(fields, popupControls), anyFilter ? null : cssPlusLabel('Add Filter'), testId('add-filter-btn') ); @@ -98,13 +125,13 @@ const cssBtn = styled('div', ` .${cssFilterBar.className} > & { margin: 0 4px; } - &-saved { + &-grayed { color: ${colors.light}; --icon-color: ${colors.light}; background-color: ${colors.slate}; border-color: ${colors.slate}; } - &-saved:hover { + &-grayed:hover { background-color: ${colors.darkGrey}; border-color: ${colors.darkGrey}; } diff --git a/app/client/ui/ViewSectionMenu.ts b/app/client/ui/ViewSectionMenu.ts index 3ac9ba94..eeaaf9f4 100644 --- a/app/client/ui/ViewSectionMenu.ts +++ b/app/client/ui/ViewSectionMenu.ts @@ -2,6 +2,7 @@ import {flipColDirection, parseSortColRefs} from 'app/client/lib/sortUtil'; import {ColumnRec, DocModel, ViewFieldRec, ViewRec, ViewSectionRec} from 'app/client/models/DocModel'; import {CustomComputed} from 'app/client/models/modelUtil'; import {attachColumnFilterMenu} from 'app/client/ui/ColumnFilterMenu'; +import {addFilterMenu} from 'app/client/ui/FilterBar'; import {makeViewLayoutMenu} from 'app/client/ui/ViewLayoutMenu'; import {basicButton, primaryButton} from 'app/client/ui2018/buttons'; 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 {Computed, dom, fromKo, makeTestId, Observable, styled} from 'grainjs'; import difference = require('lodash/difference'); +import {PopupControl} from 'popweasel'; const testId = makeTestId('test-section-menu-'); @@ -34,6 +36,8 @@ export function viewSectionMenu(docModel: DocModel, viewSection: ViewSectionRec, } }); + const popupControls = new WeakMap(); + return cssMenu( testId('wrapper'), dom.autoDispose(emptySortFilterObs), @@ -50,7 +54,8 @@ export function viewSectionMenu(docModel: DocModel, viewSection: ViewSectionRec, (row: number) => docModel.columns.getRowModel(row)); }), dom.domComputed(viewSection.filteredFields, fields => - makeFilterPanel(viewSection, fields)), + makeFilterPanel(viewSection, fields, popupControls)), + makeAddFilterButton(viewSection, popupControls), makeFilterBarToggle(viewSection.activeFilterBar), dom.domComputed(iconSuffixObs, iconSuffix => { const displaySave = iconSuffix === '-unsaved'; @@ -119,6 +124,27 @@ function makeSortPanel(section: ViewSectionRec, sortSpec: number[], getColumn: ( ]; } +export function makeAddFilterButton(viewSectionRec: ViewSectionRec, + popupControls: WeakMap) { + 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) { return cssMenuText( cssMenuIconWrapper( @@ -138,7 +164,8 @@ export function makeFilterBarToggle(activeFilterBar: CustomComputed) { } -function makeFilterPanel(section: ViewSectionRec, filteredFields: ViewFieldRec[]) { +function makeFilterPanel(section: ViewSectionRec, filteredFields: ViewFieldRec[], + popupControls: WeakMap) { const fields = filteredFields.map(field => { const fieldChanged = Computed.create(null, fromKo(field.activeFilter.isSaved), (_use, isSaved) => !isSaved); return cssMenuText( @@ -146,7 +173,10 @@ function makeFilterPanel(section: ViewSectionRec, filteredFields: ViewFieldRec[] cssMenuIconWrapper( cssMenuIconWrapper.cls('-changed', fieldChanged), 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'), ), cssMenuTextLabel(field.label()), diff --git a/test/nbrowser/gristUtils.ts b/test/nbrowser/gristUtils.ts index 09c2c07c..7e7de066 100644 --- a/test/nbrowser/gristUtils.ts +++ b/test/nbrowser/gristUtils.ts @@ -875,6 +875,25 @@ export async function toggleSidePanel(which: 'right'|'left', goal: 'open'|'close 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. */