Skip to content

Commit 8d181ea

Browse files
committed
[IMP] filter: allow filter by criterion
With this commit, it is now possible to filter a table based on a criterion, eg. filters all the values greater than 10. Task: 4592118
1 parent 4dacb74 commit 8d181ea

File tree

19 files changed

+905
-163
lines changed

19 files changed

+905
-163
lines changed

src/components/filters/filter_menu/filter_menu.ts

Lines changed: 118 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,63 @@
1-
import { Component, onWillUpdateProps, useState } from "@odoo/owl";
1+
import { Component, onWillUpdateProps } from "@odoo/owl";
22
import { BUTTON_ACTIVE_BG } from "../../../constants";
3-
import { deepEquals } from "../../../helpers";
3+
import { deepEquals, isDateTimeFormat } from "../../../helpers";
44
import { interactiveSort } from "../../../helpers/sort";
5-
import { Position, SortDirection, SpreadsheetChildEnv } from "../../../types";
5+
import { _t } from "../../../translation";
6+
import {
7+
CellValueType,
8+
CriterionFilter,
9+
DataFilterValue,
10+
Position,
11+
SortDirection,
12+
SpreadsheetChildEnv,
13+
filterDateCriterionOperators,
14+
filterNumberCriterionOperators,
15+
filterTextCriterionOperators,
16+
} from "../../../types";
617
import { CellPopoverComponent, PopoverBuilders } from "../../../types/cell_popovers";
718
import { css } from "../../helpers/css";
19+
import { SidePanelCollapsible } from "../../side_panel/components/collapsible/side_panel_collapsible";
20+
import { FilterMenuCriterion } from "../filter_menu_criterion/filter_menu_criterion";
821
import { FilterMenuValueList } from "../filter_menu_value_list/filter_menu_value_list";
922

10-
const FILTER_MENU_HEIGHT = 295;
11-
1223
css/* scss */ `
1324
.o-filter-menu {
14-
padding: 8px 16px;
15-
height: ${FILTER_MENU_HEIGHT}px;
16-
line-height: 1;
25+
width: 245px;
26+
padding: 8px 0;
27+
user-select: none;
28+
29+
.o-filter-menu-content {
30+
padding: 0 16px;
31+
}
32+
33+
.o-sort-item {
34+
padding-left: 34px;
35+
}
36+
37+
.o_side_panel_collapsible_title {
38+
font-size: inherit;
39+
padding: 0 0 4px 0 !important;
40+
font-weight: 400 !important;
41+
42+
.collapsor .o-icon {
43+
opacity: 0.8;
44+
}
45+
46+
.collapsor-arrow {
47+
transform-origin: 6px 8px;
48+
49+
.o-icon {
50+
width: 12px;
51+
height: 16px;
52+
}
53+
}
54+
}
1755
1856
.o-filter-menu-item {
1957
display: flex;
2058
cursor: pointer;
2159
user-select: none;
60+
line-height: 1;
2261
2362
&.selected,
2463
&:hover {
@@ -41,28 +80,27 @@ interface Props {
4180
onClosed?: () => void;
4281
}
4382

44-
interface State {
45-
updatedHiddenValue: string[] | undefined;
46-
}
83+
type CriterionCategory = "text" | "number" | "date";
4784

4885
export class FilterMenu extends Component<Props, SpreadsheetChildEnv> {
4986
static template = "o-spreadsheet-FilterMenu";
5087
static props = {
5188
filterPosition: Object,
5289
onClosed: { type: Function, optional: true },
5390
};
54-
static components = { FilterMenuValueList };
91+
static components = { FilterMenuValueList, SidePanelCollapsible, FilterMenuCriterion };
5592

56-
private state: State = useState({
57-
updatedHiddenValue: undefined,
58-
});
93+
private criterionCategory: CriterionCategory = "text";
94+
private updatedCriterionValue: DataFilterValue | undefined;
5995

6096
setup() {
6197
onWillUpdateProps((nextProps: Props) => {
6298
if (!deepEquals(nextProps.filterPosition, this.props.filterPosition)) {
63-
this.state.updatedHiddenValue = undefined;
99+
this.updatedCriterionValue = undefined;
100+
this.criterionCategory = this.getCriterionCategory(nextProps.filterPosition);
64101
}
65102
});
103+
this.criterionCategory = this.getCriterionCategory(this.props.filterPosition);
66104
}
67105

68106
get isSortable() {
@@ -82,24 +120,85 @@ export class FilterMenu extends Component<Props, SpreadsheetChildEnv> {
82120
return this.env.model.getters.getTable({ sheetId, ...position });
83121
}
84122

123+
get filterValueType() {
124+
const sheetId = this.env.model.getters.getActiveSheetId();
125+
const position = this.props.filterPosition;
126+
const filterValue = this.env.model.getters.getFilterValue({ sheetId, ...position });
127+
return filterValue?.filterType;
128+
}
129+
130+
private getCriterionCategory(position: Position): CriterionCategory {
131+
const sheetId = this.env.model.getters.getActiveSheetId();
132+
const filter = this.env.model.getters.getFilter({ sheetId, ...position });
133+
if (!filter || !filter.filteredRange) {
134+
return "text";
135+
}
136+
137+
const cellTypesCount: Record<CriterionCategory, number> = { text: 0, number: 0, date: 0 };
138+
const filteredZone = filter.filteredRange.zone;
139+
140+
for (let row = filteredZone.top; row <= filteredZone.bottom; row++) {
141+
// 100 rows should be enough to determine the type, let's not loop on 10,000 rows for nothing
142+
if (row > 100) {
143+
break;
144+
}
145+
const cell = this.env.model.getters.getEvaluatedCell({ sheetId, row, col: position.col });
146+
if (cell.type === CellValueType.text || cell.type === CellValueType.boolean) {
147+
cellTypesCount.text++;
148+
} else if (cell.type === CellValueType.number) {
149+
if (cell.format && isDateTimeFormat(cell.format)) {
150+
cellTypesCount.date++;
151+
} else {
152+
cellTypesCount.number++;
153+
}
154+
}
155+
}
156+
157+
const max = Math.max(cellTypesCount.text, cellTypesCount.number, cellTypesCount.date);
158+
const type = Object.keys(cellTypesCount).find((key) => cellTypesCount[key] === max);
159+
return (type || "text") as CriterionCategory;
160+
}
161+
85162
onUpdateHiddenValues(values: string[]) {
86-
this.state.updatedHiddenValue = values;
163+
this.updatedCriterionValue = { filterType: "values", hiddenValues: values };
164+
}
165+
166+
onCriterionChanged(criterion: CriterionFilter) {
167+
this.updatedCriterionValue = criterion;
87168
}
88169

89170
confirm() {
90-
if (!this.state.updatedHiddenValue) {
171+
if (!this.updatedCriterionValue) {
91172
this.props.onClosed?.();
92173
return;
93174
}
94175
const position = this.props.filterPosition;
95176
this.env.model.dispatch("UPDATE_FILTER", {
96177
...position,
97178
sheetId: this.env.model.getters.getActiveSheetId(),
98-
hiddenValues: this.state.updatedHiddenValue,
179+
value: this.updatedCriterionValue,
99180
});
100181
this.props.onClosed?.();
101182
}
102183

184+
get criterionOperators() {
185+
if (this.criterionCategory === "date") {
186+
return filterDateCriterionOperators;
187+
} else if (this.criterionCategory === "number") {
188+
return filterNumberCriterionOperators;
189+
}
190+
return filterTextCriterionOperators;
191+
}
192+
193+
get criterionTitle() {
194+
if (this.criterionCategory === "date") {
195+
return _t("Filter by date");
196+
} else if (this.criterionCategory === "number") {
197+
return _t("Filter by number");
198+
}
199+
return _t("Filter by text");
200+
}
201+
103202
cancel() {
104203
this.props.onClosed?.();
105204
}

src/components/filters/filter_menu/filter_menu.xml

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,50 @@
33
<div class="o-filter-menu d-flex flex-column bg-white" t-on-wheel.stop="">
44
<t t-if="isSortable">
55
<div>
6-
<div class="o-filter-menu-item py-2 mb-1" t-on-click="() => this.sortFilterZone('asc')">
6+
<div
7+
class="o-filter-menu-item o-sort-item py-2 mb-1"
8+
t-on-click="() => this.sortFilterZone('asc')">
79
Sort ascending (A ⟶ Z)
810
</div>
9-
<div class="o-filter-menu-item py-2" t-on-click="() => this.sortFilterZone('desc')">
11+
<div
12+
class="o-filter-menu-item o-sort-item py-2"
13+
t-on-click="() => this.sortFilterZone('desc')">
1014
Sort descending (Z ⟶ A)
1115
</div>
1216
</div>
13-
<div class="o-separator"/>
1417
</t>
15-
<FilterMenuValueList
16-
filterPosition="props.filterPosition"
17-
onUpdateHiddenValues.bind="onUpdateHiddenValues"
18-
/>
18+
<div class="o-filter-menu-content">
19+
<div class="o-separator"/>
20+
<SidePanelCollapsible
21+
isInitiallyCollapsed="filterValueType !== 'criterion'"
22+
title="criterionTitle">
23+
<t t-set-slot="content">
24+
<FilterMenuCriterion
25+
filterPosition="props.filterPosition"
26+
onCriterionChanged.bind="onCriterionChanged"
27+
criterionOperators="criterionOperators"
28+
/>
29+
<div class="mb-3"/>
30+
</t>
31+
</SidePanelCollapsible>
1932

20-
<div class="o-filter-menu-buttons d-flex justify-content-end">
21-
<button class="o-button o-filter-menu-cancel me-2" t-on-click="cancel">Cancel</button>
22-
<button class="o-button primary o-filter-menu-confirm" t-on-click="confirm">Confirm</button>
33+
<SidePanelCollapsible
34+
isInitiallyCollapsed="filterValueType === 'criterion'"
35+
title.translate="Filter by values">
36+
<t t-set-slot="content">
37+
<FilterMenuValueList
38+
filterPosition="props.filterPosition"
39+
onUpdateHiddenValues.bind="onUpdateHiddenValues"
40+
/>
41+
</t>
42+
</SidePanelCollapsible>
43+
44+
<div class="o-filter-menu-buttons d-flex justify-content-end">
45+
<button class="o-button o-filter-menu-cancel me-2" t-on-click="cancel">Cancel</button>
46+
<button class="o-button primary o-filter-menu-confirm" t-on-click="confirm">
47+
Confirm
48+
</button>
49+
</div>
2350
</div>
2451
</div>
2552
</t>
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Component, ComponentConstructor, onWillUpdateProps, useState } from "@odoo/owl";
2+
import { Action, createAction } from "../../../actions/action";
3+
import { deepCopy, deepEquals } from "../../../helpers";
4+
import {
5+
criterionComponentRegistry,
6+
getCriterionMenuItems,
7+
} from "../../../registries/criterion_component_registry";
8+
import { criterionEvaluatorRegistry } from "../../../registries/criterion_registry";
9+
import { _t } from "../../../translation";
10+
import {
11+
CriterionFilter,
12+
GenericCriterionType,
13+
Position,
14+
SpreadsheetChildEnv,
15+
} from "../../../types";
16+
import { SelectMenu } from "../../side_panel/select_menu/select_menu";
17+
18+
interface Props {
19+
filterPosition: Position;
20+
criterionOperators: GenericCriterionType[];
21+
onCriterionChanged: (criterion: CriterionFilter) => void;
22+
}
23+
24+
interface State {
25+
criterion: CriterionFilter;
26+
}
27+
28+
export class FilterMenuCriterion extends Component<Props, SpreadsheetChildEnv> {
29+
static template = "o-spreadsheet-FilterMenuCriterion";
30+
static props = {
31+
filterPosition: Object,
32+
onCriterionChanged: Function,
33+
criterionOperators: Array,
34+
};
35+
static components = { SelectMenu };
36+
37+
private state!: State;
38+
39+
setup() {
40+
onWillUpdateProps((nextProps: Props) => {
41+
if (!deepEquals(nextProps.filterPosition, this.props.filterPosition)) {
42+
this.state.criterion = this.getFilterCriterionValue(nextProps.filterPosition);
43+
}
44+
});
45+
46+
this.state = useState({
47+
criterion: this.getFilterCriterionValue(this.props.filterPosition),
48+
});
49+
}
50+
51+
private getFilterCriterionValue(position: Position): CriterionFilter {
52+
const sheetId = this.env.model.getters.getActiveSheetId();
53+
const filterValue = this.env.model.getters.getFilterCriterionValue({ sheetId, ...position });
54+
return filterValue?.filterType === "criterion"
55+
? deepCopy(filterValue)
56+
: { filterType: "criterion", type: "none", values: [] };
57+
}
58+
59+
get criterionMenuItems(): Action[] {
60+
const noCriterionMenuItem = createAction({
61+
name: _t("None"),
62+
id: "none",
63+
separator: true,
64+
execute: () => this.onCriterionTypeChange("none"),
65+
});
66+
return [
67+
noCriterionMenuItem,
68+
...getCriterionMenuItems(
69+
(type) => this.onCriterionTypeChange(type),
70+
new Set(this.props.criterionOperators)
71+
),
72+
];
73+
}
74+
75+
get selectedCriterionName(): string {
76+
return this.state.criterion.type === "none"
77+
? _t("None")
78+
: criterionEvaluatorRegistry.get(this.state.criterion.type).name;
79+
}
80+
81+
get criterionComponent(): ComponentConstructor | undefined {
82+
return this.state.criterion.type === "none"
83+
? undefined
84+
: criterionComponentRegistry.get(this.state.criterion.type).component;
85+
}
86+
87+
onCriterionChanged(criterion: CriterionFilter) {
88+
this.state.criterion.values = criterion.values;
89+
this.state.criterion.dateValue = criterion.dateValue;
90+
this.props.onCriterionChanged(this.state.criterion);
91+
}
92+
93+
private onCriterionTypeChange(type: CriterionFilter["type"]) {
94+
this.state.criterion.type = type;
95+
this.props.onCriterionChanged(this.state.criterion);
96+
}
97+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<templates>
2+
<t t-name="o-spreadsheet-FilterMenuCriterion">
3+
<SelectMenu
4+
class="'o-filter-criterion-type o-input m-1 mb-2'"
5+
menuItems="criterionMenuItems"
6+
selectedValue="selectedCriterionName"
7+
/>
8+
9+
<t
10+
t-if="criterionComponent"
11+
t-component="criterionComponent"
12+
t-key="selectedCriterionName"
13+
criterion="state.criterion"
14+
onCriterionChanged.bind="onCriterionChanged"
15+
disableFormulas="true"
16+
/>
17+
</t>
18+
</templates>

src/components/filters/filter_menu_value_list/filter_menu_value_list.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
flex: auto;
2222
overflow-y: auto;
2323
border: 1px solid $os-gray-300;
24+
height: 130px;
2425

2526
.o-filter-menu-no-values {
2627
color: #949494;

0 commit comments

Comments
 (0)