Summary: The sort and filter UI now has a more unified UI, with similar capabilities that are accessible from different parts of Grist. It's now also possible to pin individual filters to the filter bar, which replaces the old toggle for showing all filters in the filter bar. Test Plan: Various tests (browser, migration, project). Reviewers: jarek, dsagal Reviewed By: jarek, dsagal Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D3669pull/345/head
parent
af462fc938
commit
1a6d427339
@ -0,0 +1,149 @@
|
|||||||
|
import {makeT} from 'app/client/lib/localization';
|
||||||
|
import {ViewSectionRec} from 'app/client/models/DocModel';
|
||||||
|
import {attachColumnFilterMenu} from 'app/client/ui/ColumnFilterMenu';
|
||||||
|
import {addFilterMenu} from 'app/client/ui/FilterBar';
|
||||||
|
import {cssIcon, cssPinButton, cssRow, cssSortFilterColumn} from 'app/client/ui/RightPanelStyles';
|
||||||
|
import {theme} from 'app/client/ui2018/cssVars';
|
||||||
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
|
import {Computed, Disposable, dom, makeTestId, styled} from 'grainjs';
|
||||||
|
import {IMenuOptions} from 'popweasel';
|
||||||
|
|
||||||
|
const testId = makeTestId('test-filter-config-');
|
||||||
|
|
||||||
|
const t = makeT('SortConfig');
|
||||||
|
|
||||||
|
export interface FilterConfigOptions {
|
||||||
|
/** Options to pass to the menu and popup components. */
|
||||||
|
menuOptions?: IMenuOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that renders controls for managing filters for a view section.
|
||||||
|
*
|
||||||
|
* Active filters (i.e. columns that have non-blank filters set) are displayed in
|
||||||
|
* a vertical list of pill-shaped buttons. These buttons can be clicked to open their
|
||||||
|
* respective filter menu. Additionally, there are buttons to the right of each filter
|
||||||
|
* for removing and pinning them.
|
||||||
|
*/
|
||||||
|
export class FilterConfig extends Disposable {
|
||||||
|
private _popupControls = new WeakMap();
|
||||||
|
|
||||||
|
private _canAddFilter = Computed.create(this, (use) => {
|
||||||
|
return use(this._section.filters).some(f => !use(f.isFiltered));
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(private _section: ViewSectionRec, private _options: FilterConfigOptions = {}) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildDom() {
|
||||||
|
const {menuOptions} = this._options;
|
||||||
|
return dom('div',
|
||||||
|
dom.forEach(this._section.activeFilters, (filterInfo) => {
|
||||||
|
const {fieldOrColumn, filter, pinned, isPinned} = filterInfo;
|
||||||
|
return cssRow(
|
||||||
|
cssSortFilterColumn(
|
||||||
|
cssIconWrapper(
|
||||||
|
cssFilterIcon('FilterSimple',
|
||||||
|
cssFilterIcon.cls('-accent', use => !use(filter.isSaved) || !use(pinned.isSaved)),
|
||||||
|
testId('filter-icon'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cssLabel(dom.text(fieldOrColumn.label)),
|
||||||
|
attachColumnFilterMenu(filterInfo, {
|
||||||
|
popupOptions: {
|
||||||
|
placement: 'bottom-end',
|
||||||
|
...menuOptions,
|
||||||
|
trigger: [
|
||||||
|
'click',
|
||||||
|
(_el, popupControl) => this._popupControls.set(fieldOrColumn.origCol(), popupControl)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
testId('column'),
|
||||||
|
),
|
||||||
|
cssPinFilterButton(
|
||||||
|
icon('PinTilted'),
|
||||||
|
dom.on('click', () => this._section.setFilter(fieldOrColumn.origCol().origColRef(), {
|
||||||
|
pinned: !isPinned.peek()
|
||||||
|
})),
|
||||||
|
cssPinButton.cls('-pinned', isPinned),
|
||||||
|
testId('pin-filter'),
|
||||||
|
),
|
||||||
|
cssIconWrapper(
|
||||||
|
cssRemoveFilterButton('Remove',
|
||||||
|
dom.on('click',
|
||||||
|
() => this._section.setFilter(fieldOrColumn.origCol().origColRef(), {
|
||||||
|
filter: '',
|
||||||
|
pinned: false,
|
||||||
|
})),
|
||||||
|
testId('remove-filter'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
testId('filter'),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
cssRow(
|
||||||
|
dom.domComputed((use) => {
|
||||||
|
const filters = use(this._section.filters);
|
||||||
|
return cssTextBtn(
|
||||||
|
t('AddColumn'),
|
||||||
|
addFilterMenu(filters, this._popupControls, {
|
||||||
|
menuOptions: {
|
||||||
|
placement: 'bottom-end',
|
||||||
|
...this._options.menuOptions,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
dom.on('click', (ev) => ev.stopPropagation()),
|
||||||
|
dom.hide(u => !u(this._canAddFilter)),
|
||||||
|
testId('add-filter-btn'),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
testId('container'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssIconWrapper = styled('div', ``);
|
||||||
|
|
||||||
|
const cssLabel = styled('div', `
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-grow: 1;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssTextBtn = styled('div', `
|
||||||
|
color: ${theme.controlFg};
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ${theme.controlHoverFg};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssFilterIcon = styled(cssIcon, `
|
||||||
|
flex: none;
|
||||||
|
margin: 0px 6px 0px 0px;
|
||||||
|
background-color: ${theme.controlSecondaryFg};
|
||||||
|
|
||||||
|
&-accent {
|
||||||
|
background-color: ${theme.accentIcon};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssRemoveFilterButton = styled(cssIcon, `
|
||||||
|
flex: none;
|
||||||
|
margin: 0 6px;
|
||||||
|
background-color: ${theme.controlSecondaryFg};
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${theme.controlSecondaryHoverFg};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssPinFilterButton = styled(cssPinButton, `
|
||||||
|
margin-left: 6px;
|
||||||
|
`);
|
@ -0,0 +1,372 @@
|
|||||||
|
import {GristDoc} from 'app/client/components/GristDoc';
|
||||||
|
import koArray from 'app/client/lib/koArray';
|
||||||
|
import * as kf from 'app/client/lib/koForm';
|
||||||
|
import {makeT} from 'app/client/lib/localization';
|
||||||
|
import {addToSort, updatePositions} from 'app/client/lib/sortUtil';
|
||||||
|
import {ViewSectionRec} from 'app/client/models/DocModel';
|
||||||
|
import {ObjObservable} from 'app/client/models/modelUtil';
|
||||||
|
import {cssIcon, cssRow, cssSortFilterColumn} from 'app/client/ui/RightPanelStyles';
|
||||||
|
import {labeledLeftSquareCheckbox} from 'app/client/ui2018/checkbox';
|
||||||
|
import {theme} from 'app/client/ui2018/cssVars';
|
||||||
|
import {cssDragger} from 'app/client/ui2018/draggableList';
|
||||||
|
import {menu, menuItem} from 'app/client/ui2018/menus';
|
||||||
|
import {Sort} from 'app/common/SortSpec';
|
||||||
|
import {Computed, Disposable, dom, makeTestId, MultiHolder, styled} from 'grainjs';
|
||||||
|
import difference = require('lodash/difference');
|
||||||
|
import isEqual = require('lodash/isEqual');
|
||||||
|
import {cssMenuItem, IMenuOptions} from 'popweasel';
|
||||||
|
|
||||||
|
interface SortableColumn {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
icon: 'FieldColumn';
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SortConfigOptions {
|
||||||
|
/** Options to pass to all menus created by `SortConfig`. */
|
||||||
|
menuOptions?: IMenuOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testId = makeTestId('test-sort-config-');
|
||||||
|
|
||||||
|
const t = makeT('SortConfig');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that renders controls for managing sorting for a view section.
|
||||||
|
*
|
||||||
|
* Sorted columns are displayed in a vertical list of pill-shaped buttons. These
|
||||||
|
* buttons can be clicked to toggle their sort direction, and can be clicked and
|
||||||
|
* dragged to re-arrange their order. Additionally, there are buttons to the right
|
||||||
|
* of each sorted column for removing them, and opening a menu with advanced sort
|
||||||
|
* options.
|
||||||
|
*/
|
||||||
|
export class SortConfig extends Disposable {
|
||||||
|
// Computed array of sortable columns.
|
||||||
|
private _columns: Computed<SortableColumn[]> = Computed.create(this, (use) => {
|
||||||
|
// Columns is an observable holding an observable array - must call 'use' on it 2x.
|
||||||
|
const cols = use(use(use(this._section.table).columns).getObservable());
|
||||||
|
return cols.filter(col => !use(col.isHiddenCol)).map(col => ({
|
||||||
|
label: use(col.label),
|
||||||
|
value: col.getRowId(),
|
||||||
|
icon: 'FieldColumn',
|
||||||
|
type: col.type(),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// We only want to recreate rows, when the actual columns change.
|
||||||
|
private _colRefs = Computed.create(this, (use) => {
|
||||||
|
return use(this._section.activeSortSpec).map(col => Sort.getColRef(col));
|
||||||
|
});
|
||||||
|
private _sortRows = this.autoDispose(koArray(this._colRefs.get()));
|
||||||
|
|
||||||
|
private _changedColRefs = Computed.create(this, (use) => {
|
||||||
|
const changedSpecs = difference(
|
||||||
|
use(this._section.activeSortSpec),
|
||||||
|
Sort.parseSortColRefs(use(this._section.sortColRefs))
|
||||||
|
);
|
||||||
|
return new Set(changedSpecs.map(spec => Sort.getColRef(spec)));
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(private _section: ViewSectionRec, private _gristDoc: GristDoc, private _options: SortConfigOptions = {}) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.autoDispose(this._colRefs.addListener((curr, prev) => {
|
||||||
|
if (!isEqual(curr, prev)){
|
||||||
|
this._sortRows.assign(curr);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildDom() {
|
||||||
|
return dom('div',
|
||||||
|
// Sort rows.
|
||||||
|
kf.draggableList(this._sortRows, (colRef: number) => this._createRow(colRef), {
|
||||||
|
reorder: (colRef: number, nextColRef: number | null) => this._reorder(colRef, nextColRef),
|
||||||
|
removeButton: false,
|
||||||
|
drag_indicator: cssDragger,
|
||||||
|
itemClass: cssDragRow.className,
|
||||||
|
}),
|
||||||
|
// Add to sort btn & menu.
|
||||||
|
this._buildAddToSortButton(this._columns),
|
||||||
|
this._buildUpdateDataButton(),
|
||||||
|
testId('container'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createRow(colRef: number) {
|
||||||
|
return this._buildSortRow(colRef, this._section.activeSortSpec, this._columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a single row of the sort dom.
|
||||||
|
* Takes the colRef, current sortSpec and array of column select options to show
|
||||||
|
* in the column select dropdown.
|
||||||
|
*/
|
||||||
|
private _buildSortRow(
|
||||||
|
colRef: number,
|
||||||
|
sortSpec: ObjObservable<Sort.SortSpec>,
|
||||||
|
columns: Computed<SortableColumn[]>
|
||||||
|
) {
|
||||||
|
const holder = new MultiHolder();
|
||||||
|
const {menuOptions} = this._options;
|
||||||
|
|
||||||
|
const col = Computed.create(holder, () => colRef);
|
||||||
|
const details = Computed.create(holder, (use) => Sort.specToDetails(Sort.findCol(use(sortSpec), colRef)!));
|
||||||
|
const hasSpecs = Computed.create(holder, details, (_, specDetails) => Sort.hasOptions(specDetails));
|
||||||
|
const isAscending = Computed.create(holder, details, (_, specDetails) => specDetails.direction === Sort.ASC);
|
||||||
|
|
||||||
|
col.onWrite((newRef) => {
|
||||||
|
let specs = sortSpec.peek();
|
||||||
|
const colSpec = Sort.findCol(specs, colRef);
|
||||||
|
const newSpec = Sort.findCol(specs, newRef);
|
||||||
|
if (newSpec) {
|
||||||
|
// this column is already there so only swap order
|
||||||
|
specs = Sort.swap(specs, colRef, newRef);
|
||||||
|
// but keep the directions
|
||||||
|
specs = Sort.setSortDirection(specs, colRef, Sort.direction(newSpec));
|
||||||
|
specs = Sort.setSortDirection(specs, newRef, Sort.direction(colSpec!));
|
||||||
|
} else {
|
||||||
|
specs = Sort.replace(specs, colRef, Sort.createColSpec(newRef, Sort.direction(colSpec!)));
|
||||||
|
}
|
||||||
|
this._saveSort(specs);
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedFlag = (
|
||||||
|
flag: keyof Sort.ColSpecDetails,
|
||||||
|
allowedTypes: string[] | null,
|
||||||
|
label: string
|
||||||
|
) => {
|
||||||
|
const computed = Computed.create(holder, details, (_, d) => d[flag] || false);
|
||||||
|
computed.onWrite(value => {
|
||||||
|
const specs = sortSpec.peek();
|
||||||
|
// Get existing details
|
||||||
|
const specDetails = Sort.specToDetails(Sort.findCol(specs, colRef)!) as any;
|
||||||
|
// Update flags
|
||||||
|
specDetails[flag] = value;
|
||||||
|
// Replace the colSpec at the index
|
||||||
|
this._saveSort(Sort.replace(specs, Sort.getColRef(colRef), specDetails));
|
||||||
|
});
|
||||||
|
return {computed, allowedTypes, flag, label};
|
||||||
|
};
|
||||||
|
const orderByChoice = computedFlag('orderByChoice', ['Choice'], t('UseChoicePosition'));
|
||||||
|
const naturalSort = computedFlag('naturalSort', ['Text'], t('NaturalSort'));
|
||||||
|
const emptyLast = computedFlag('emptyLast', null, t('EmptyValuesLast'));
|
||||||
|
const flags = [orderByChoice, emptyLast, naturalSort];
|
||||||
|
|
||||||
|
const column = columns.get().find(c => c.value === Sort.getColRef(colRef));
|
||||||
|
|
||||||
|
return cssSortRow(
|
||||||
|
dom.autoDispose(holder),
|
||||||
|
cssSortFilterColumn(
|
||||||
|
dom.domComputed(isAscending, ascending =>
|
||||||
|
cssSortIcon(
|
||||||
|
"Sort",
|
||||||
|
cssSortIcon.cls('-accent', use => use(this._changedColRefs).has(column!.value)),
|
||||||
|
dom.style("transform", ascending ? "scaleY(-1)" : "none"),
|
||||||
|
testId('order'),
|
||||||
|
testId(ascending ? "sort-order-asc" : "sort-order-desc"),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
cssLabel(column!.label),
|
||||||
|
dom.on("click", () => {
|
||||||
|
this._saveSort(Sort.flipSort(sortSpec.peek(), colRef));
|
||||||
|
}),
|
||||||
|
testId('column'),
|
||||||
|
),
|
||||||
|
cssMenu(
|
||||||
|
cssBigIconWrapper(
|
||||||
|
cssIcon('Dots', dom.cls(cssBgAccent.className, hasSpecs)),
|
||||||
|
testId('options-icon'),
|
||||||
|
),
|
||||||
|
menu(_ctl => flags.map(({computed, allowedTypes, flag, label}) => {
|
||||||
|
// when allowedTypes is null, flag can be used for every column
|
||||||
|
const enabled = !allowedTypes || allowedTypes.includes(column!.type);
|
||||||
|
return cssMenuItem(
|
||||||
|
labeledLeftSquareCheckbox(
|
||||||
|
computed as any,
|
||||||
|
label,
|
||||||
|
dom.prop('disabled', !enabled),
|
||||||
|
),
|
||||||
|
dom.cls(cssOptionMenuItem.className),
|
||||||
|
dom.cls('disabled', !enabled),
|
||||||
|
testId('option'),
|
||||||
|
testId(`option-${flag}`),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
), menuOptions),
|
||||||
|
),
|
||||||
|
cssSortIconBtn('Remove',
|
||||||
|
dom.on('click', () => {
|
||||||
|
const specs = sortSpec.peek();
|
||||||
|
if (Sort.findCol(specs, colRef)) {
|
||||||
|
this._saveSort(Sort.removeCol(specs, colRef));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
testId('remove')
|
||||||
|
),
|
||||||
|
testId('row'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _buildAddToSortButton(columns: Computed<SortableColumn[]>) {
|
||||||
|
const available = Computed.create(null, (use) => {
|
||||||
|
const currentSection = this._section;
|
||||||
|
const currentSortSpec = use(currentSection.activeSortSpec);
|
||||||
|
const specRowIds = new Set(currentSortSpec.map(_sortRef => Sort.getColRef(_sortRef)));
|
||||||
|
return use(columns).filter(_col => !specRowIds.has(_col.value));
|
||||||
|
});
|
||||||
|
const {menuOptions} = this._options;
|
||||||
|
return cssButtonRow(
|
||||||
|
dom.autoDispose(available),
|
||||||
|
dom.domComputed(use => {
|
||||||
|
const cols = use(available);
|
||||||
|
return cssTextBtn(
|
||||||
|
t('AddColumn'),
|
||||||
|
menu((ctl) => [
|
||||||
|
...cols.map((col) => (
|
||||||
|
menuItem(
|
||||||
|
() => addToSort(this._section.activeSortSpec, col.value, 1),
|
||||||
|
col.label,
|
||||||
|
testId('add-menu-row')
|
||||||
|
)
|
||||||
|
)),
|
||||||
|
// 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();
|
||||||
|
}),
|
||||||
|
], menuOptions),
|
||||||
|
dom.on('click', (ev) => { ev.stopPropagation(); }),
|
||||||
|
testId('add'),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
dom.hide(use => !use(available).length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _buildUpdateDataButton() {
|
||||||
|
return dom.maybe(this._section.isSorted, () =>
|
||||||
|
cssButtonRow(
|
||||||
|
cssTextBtn(t('UpdateData'),
|
||||||
|
dom.on('click', () => updatePositions(this._gristDoc, this._section)),
|
||||||
|
testId('update'),
|
||||||
|
dom.show((use) => (
|
||||||
|
use(use(this._section.table).supportsManualSort)
|
||||||
|
&& !use(this._gristDoc.isReadonly)
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _reorder(colRef: number, nextColRef: number | null) {
|
||||||
|
const activeSortSpec = this._section.activeSortSpec.peek();
|
||||||
|
const colSpec = Sort.findCol(activeSortSpec, colRef);
|
||||||
|
if (colSpec === undefined) {
|
||||||
|
throw new Error(`Col ${colRef} not found in active sort spec`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSpec = Sort.reorderSortRefs(this._section.activeSortSpec.peek(), colSpec, nextColRef);
|
||||||
|
this._saveSort(newSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _saveSort(sortSpec: Sort.SortSpec) {
|
||||||
|
this._section.activeSortSpec(sortSpec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssDragRow = styled('div', `
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 16px 0px 0px;
|
||||||
|
& > .kf_draggable_content {
|
||||||
|
margin: 4px 0;
|
||||||
|
flex: 1 1 0px;
|
||||||
|
min-width: 0px;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssLabel = styled('div', `
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-grow: 1;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssSortRow = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssTextBtn = styled('div', `
|
||||||
|
color: ${theme.controlFg};
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ${theme.controlHoverFg};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssSortIconBtn = styled(cssIcon, `
|
||||||
|
flex: none;
|
||||||
|
margin: 0 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: ${theme.controlSecondaryFg};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${theme.controlSecondaryHoverFg};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssSortIcon = styled(cssIcon, `
|
||||||
|
flex: none;
|
||||||
|
margin: 0px 6px 0px 0px;
|
||||||
|
background-color: ${theme.controlSecondaryFg};
|
||||||
|
|
||||||
|
&-accent {
|
||||||
|
background-color: ${theme.accentIcon};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssBigIconWrapper = styled('div', `
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssBgAccent = styled(`div`, `
|
||||||
|
background: ${theme.accentIcon}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssMenu = styled('div', `
|
||||||
|
display: inline-flex;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
margin-left: 6px;
|
||||||
|
&:hover, &.weasel-popup-open {
|
||||||
|
background-color: ${theme.hover};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssOptionMenuItem = styled('div', `
|
||||||
|
&:hover {
|
||||||
|
background-color: ${theme.hover};
|
||||||
|
}
|
||||||
|
& label {
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
&.disabled * {
|
||||||
|
color: ${theme.menuItemDisabledFg} important;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssButtonRow = styled(cssRow, `
|
||||||
|
margin-top: 4px;
|
||||||
|
`);
|
@ -0,0 +1,68 @@
|
|||||||
|
import {GristDoc} from 'app/client/components/GristDoc';
|
||||||
|
import {makeT} from 'app/client/lib/localization';
|
||||||
|
import {ViewSectionRec} from 'app/client/models/DocModel';
|
||||||
|
import {FilterConfig} from 'app/client/ui/FilterConfig';
|
||||||
|
import {cssLabel, cssSaveButtonsRow} from 'app/client/ui/RightPanelStyles';
|
||||||
|
import {SortConfig} from 'app/client/ui/SortConfig';
|
||||||
|
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
|
||||||
|
import {Computed, Disposable, dom, makeTestId, styled} from 'grainjs';
|
||||||
|
|
||||||
|
const testId = makeTestId('test-sort-filter-config-');
|
||||||
|
|
||||||
|
const t = makeT('SortFilterConfig');
|
||||||
|
|
||||||
|
export class SortFilterConfig extends Disposable {
|
||||||
|
private _docModel = this._gristDoc.docModel;
|
||||||
|
private _isReadonly = this._gristDoc.isReadonly;
|
||||||
|
|
||||||
|
private _hasChanges: Computed<boolean> = Computed.create(this, (use) => (
|
||||||
|
use(this._section.filterSpecChanged) || !use(this._section.activeSortJson.isSaved)
|
||||||
|
));
|
||||||
|
|
||||||
|
constructor(private _section: ViewSectionRec, private _gristDoc: GristDoc) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildDom() {
|
||||||
|
return [
|
||||||
|
cssLabel(t('Sort')),
|
||||||
|
dom.create(SortConfig, this._section, this._gristDoc, {
|
||||||
|
menuOptions: {attach: 'body'},
|
||||||
|
}),
|
||||||
|
cssLabel(t('Filter')),
|
||||||
|
dom.create(FilterConfig, this._section, {
|
||||||
|
menuOptions: {attach: 'body'},
|
||||||
|
}),
|
||||||
|
dom.maybe(this._hasChanges, () => [
|
||||||
|
cssSaveButtonsRow(
|
||||||
|
cssSaveButton(t('Save'),
|
||||||
|
dom.on('click', () => this._save()),
|
||||||
|
dom.boolAttr('disabled', this._isReadonly),
|
||||||
|
testId('save'),
|
||||||
|
),
|
||||||
|
basicButton(t('Revert'),
|
||||||
|
dom.on('click', () => this._revert()),
|
||||||
|
testId('revert'),
|
||||||
|
),
|
||||||
|
testId('save-btns'),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _save() {
|
||||||
|
await this._docModel.docData.bundleActions(t('UpdateSortFilterSettings'), () => Promise.all([
|
||||||
|
this._section.activeSortJson.save(),
|
||||||
|
this._section.saveFilters(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _revert() {
|
||||||
|
this._section.activeSortJson.revert();
|
||||||
|
this._section.revertFilters();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssSaveButton = styled(primaryButton, `
|
||||||
|
margin-right: 8px;
|
||||||
|
`);
|
After Width: | Height: | Size: 615 B |
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue