Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AC#1121: Any column contains filter #419

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/app/RowFilters/AnyColumn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ColumnKinds } from "../constants/TableauxConstants";
import Text from "./Text";

const Mode = {
contains: "contains"
};

const filterableColumnKinds = new Set([
ColumnKinds.attachment,
ColumnKinds.concat,
ColumnKinds.date,
ColumnKinds.dateTime,
ColumnKinds.group,
ColumnKinds.integer,
ColumnKinds.link,
ColumnKinds.numeric,
ColumnKinds.richtext,
ColumnKinds.shorttext,
ColumnKinds.text
]);

// We use `getDisplayValue`, so we can nly use strings
export default {
Mode,
readValue: Text.readValue,
[Mode.contains]: (ctx, query) => {
const pred = Text.contains(query);
return (row, _, columnMatches) => {
const findMatchingRows = (found, column) => {
const dv =
column.kind === "link"
? ctx.getValue(column.name)(row)
: ctx.getDisplayValue(column.name, row);
if (
filterableColumnKinds.has(column.kind) &&
pred(Array.isArray(dv) ? dv.join(" ") : dv)
) {
columnMatches.get().add(column.id);
return true;
} else {
return found;
}
};
return ctx.columns.reduce(findMatchingRows, false);
};
}
};
6 changes: 6 additions & 0 deletions src/app/RowFilters/Number.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { maybe } from "../helpers/functools";
import Text from "./Text";

const Mode = {
contains: "contains",
equals: "equals",
gt: "gt",
gte: "gte",
Expand All @@ -17,6 +19,10 @@ export default {
.map(parseFloat)
.filter(isFinite)
.getOrElse(null),
[Mode.contains]: x => {
const test = Text.contains(String(x));
return y => test(String(y));
},
[Mode.equals]: x => y => y === x,
[Mode.gt]: x => y => y > x,
[Mode.gte]: x => y => y >= x,
Expand Down
16 changes: 15 additions & 1 deletion src/app/RowFilters/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import f from "lodash/fp";
import { ColumnKinds, SortValue } from "../constants/TableauxConstants";
import FilterAnnotation from "./Annotation";
import FilterAnyColumn from "./AnyColumn";
import FilterBoolean from "./Boolean";
import FilterDate from "./Date";
import FilterDateTime from "./DateTime";
Expand All @@ -19,6 +20,7 @@ export const Text = FilterText.Mode;
export const RowProp = FilterRowProp.Mode;

const ModesForKind = {
"any-column": FilterAnyColumn,
[ColumnKinds.attachment]: null,
[ColumnKinds.boolean]: FilterBoolean,
[ColumnKinds.concat]: FilterText,
Expand Down Expand Up @@ -48,6 +50,7 @@ const canSortByColumnKind = canFilterByColumnKind;
*
* Filter := [Predicate | And | Or]
* Predicate := ["value" ColumnName Operator ?OperatorValue]
* | ["any-value" Operator ?OperatorValue]
* | ["row-prop" PropPath Operator ?OperatorValue]
* | ["annotation" AnnotationProp FlagName Operator ?OperatorValue]
* PropPath := \w+(\.\w+)*
Expand Down Expand Up @@ -89,21 +92,31 @@ const parse = ctx => {
return parseRowPropFilter(list);
case "annotation":
return parseAnnotationFilter(ctx, list);
case "any-value":
return parseAnyColumnFilter(ctx, list);
default:
throw new Error(`Could not parse filter instruction of kind ${kind}`);
}
};
return parseImpl;
};

const parseAnyColumnFilter = (ctx, [_, op, opValue]) => {
const pred = FilterAnyColumn[op];
if (typeof pred !== "function")
throw new Error(
`Can not test if any columns' display value "${op}", unknown operation`
);
return pred(ctx, opValue);
};

const parseAnnotationFilter = (ctx, [_, findBy, kind, op, opValue]) => {
const find = FilterAnnotation.get[findBy];
const pred = FilterAnnotation[op];
if (typeof find !== "function")
throw new Error(`Can not find annotation by "${find}", unknown operation`);
if (typeof pred !== "function")
throw new Error(`Can not compare annotation by "${op}", unknown operation`);

return pred(find(kind), ctx.columns, opValue);
};

Expand Down Expand Up @@ -221,6 +234,7 @@ const buildContext = (tableId, langtag, store) => {

return {
columns,
getDisplayValue: (name, row) => retrieveDisplayValue(name)(row),
getValue: name =>
name === "rowId" ? row => row.id : lookupFn[columnKindLookup[name]](name),
getValueFilter: buildValueFilter
Expand Down
14 changes: 12 additions & 2 deletions src/app/components/header/filter/FilterPopup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,19 @@ const of = el => (Array.isArray(el) ? el : [el]);

const FilterPopup = ({
actions,
columns,
columns: rawColumns,
langtag,
onClickedOutside,
currentFilter
}) => {
const tableId = useSelector(f.prop("tableView.currentTable"));
const anyColumnContains = {
id: -1,
name: "any-column",
kind: "any-column",
displayName: { [langtag]: t("table:filter.any-column") }
};
const columns = useMemo(() => [anyColumnContains, ...rawColumns]);

const [showFilterSavePopup, setShowFilterSavePopup] = useState(false);

Expand Down Expand Up @@ -241,9 +248,11 @@ const settingToFilter = ({ column, mode, value }) => {
const needsValueArg = RowFilters.needsFilterValue(column?.kind, mode);
const hasValue = !f.isNil(value) && value !== "";
const isIncomplete = !column || !mode || (needsValueArg && !hasValue);
const isAnyColumnFilter = !isIncomplete && column.name === "any-column";

return match({ isIncomplete })(
return match({ isIncomplete, isAnyColumnFilter })(
when({ isIncomplete: true }, () => null),
when({ isAnyColumnFilter: true }, () => ["any-value", mode, value]),
otherwise(() => ["value", column.name, mode, value])
);
};
Expand Down Expand Up @@ -315,6 +324,7 @@ const AnnotationBadge = ({ title, onClick, active, color }) => {
</button>
);
};

const ColumnFilterArea = ({ columns, filters, langtag, onChange }) => {
const addFilterRow = () => onChange([...filters, {}]);
const removeFilterRow = idxToRemove => () =>
Expand Down
4 changes: 4 additions & 0 deletions src/app/components/header/filter/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export const fromCombinedFilter = (columns, langtag) => {
const [colName, mode, value] = rest;
const column = columnLookup[colName];
rowFilters.push({ column, mode, value });
} else if (kind === "any-value") {
const [mode, value] = rest;
const column = columnLookup["any-column"];
rowFilters.push({ column, mode, value });
} else if (kind === "row-prop" && rest[0] === "id") {
rowFilters.push(["value", "rowId", ...rest.slice(1)]);
} else if (kind === "and") {
Expand Down
2 changes: 1 addition & 1 deletion src/app/constants/TableauxConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { getCssVar } from "../helpers/getCssVar";
* First language is default language.
* Also, this is the order an expanded row shows the languages
*/
let languagetags;
let languagetags = ["de-DE"];
let _config = {};

const AnnotationKind = {
Expand Down
2 changes: 1 addition & 1 deletion src/locales/de/table.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
"rows_hidden": "Es werden nur bestimmte Datensätze angezeigt",
"needs_translation": "Übersetzung benötigt",
"is_final": "Als freigegeben markiert",
"row-contains": "Irgendeine Spalte enthält...",
"any-column": "Irgendeine Spalte",
"generic": "Allgemeine Filter",
"specific": "Spaltenwerte",
"toggle-list": "Bestehende Filter wählen",
Expand Down
2 changes: 1 addition & 1 deletion src/locales/en/table.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
"rows_hidden": "Only displaying specific rows",
"needs_translation": "Needs translation",
"is_final": "Is marked as final",
"row-contains": "Any row contains...",
"any-column": "Any column",
"generic": "Generic filters",
"specific": "Row values",
"toggle-list": "Choose existing filter",
Expand Down
29 changes: 29 additions & 0 deletions tests/rowFilters/rowFilters.ng.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ describe("buildContext()", () => {
});
describe("Number", () => {
const valueOf = ctx.getValue("integer");
it("contains", () => {
const matches = ctx.getValueFilter("integer", Number.contains, 23);
expect(matches(valueOf(rows[0]))).toBe(true);
expect(matches(valueOf(rows[1]))).toBe(false);
});
it("equals", () => {
const matches = ctx.getValueFilter("integer", Number.equals, 123);
expect(matches(valueOf(rows[0]))).toBe(true);
Expand Down Expand Up @@ -341,6 +346,30 @@ describe("buildContext()", () => {
expect(result2).toEqual([]);
});
});
describe("AnyColumn", () => {
it("should search for values across columns (A)", () => {
const parse = RowFilters.parse(ctx);
const testAllColumns = parse(["any-value", "contains", "s"]);
const [foundRows, foundColumns] = filterStateful(
testAllColumns,
new Set()
)(rows);
// Matches "Schnappt Shortie" in row 1, columns 0, 11
// and "Dolor sit amet" in row 2, columns 10, 11
expect(foundRows.map(row => row.id)).toEqual([1, 2]);
expect(Array.from(foundColumns).sort()).toEqual([0, 10, 11, 12]);
});
it("should search for values across columns (B)", () => {
const parse = RowFilters.parse(ctx);
const testAllColumns = parse(["any-value", "contains", "1"]);
const [foundRows, foundColumns] = filterStateful(
testAllColumns,
new Set()
)(rows);
expect(foundRows.map(row => row.id)).toEqual([1, 2]);
expect(Array.from(foundColumns).sort()).toEqual([5, 7, 8]);
});
});
describe("Annotation", () => {
const parse = RowFilters.parse(ctx);
it("should find simple flag annotations", () => {
Expand Down