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

#1074 enable date filtering using DatePicker #1130

Merged
merged 5 commits into from
Jan 22, 2024
Merged
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
17 changes: 11 additions & 6 deletions vuu-ui/packages/vuu-filter-types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ColumnDescriptor } from "@finos/vuu-table-types";

export declare type NumericFilterClauseOp =
| "="
| "!="
Expand Down Expand Up @@ -37,10 +39,11 @@ export interface SingleValueFilterClause<T = string | number | boolean>
value: T;
}

export interface MultiValueFilterClause extends NamedFilter {
export interface MultiValueFilterClause<T = string[] | number[] | boolean[]>
extends NamedFilter {
op: MultipleValueFilterClauseOp;
column: string;
values: string[] | number[] | boolean[];
values: T;
}

export declare type FilterClause =
Expand All @@ -63,12 +66,12 @@ export interface OrFilter extends MultiClauseFilter {
/**
* A Filter structure that can represent any of the filters supported by the Vuu server.
* Note that a filter in this form is never passed directly to the Vuu server. For that,
* a string based filter language is used. Any filter can be expressed in string form
* a string based filter language is used. Any filter can be expressed in string form
* or the structure described here.
* an example of a simple filter expressed in both formats:
*
*
* 'currency = "EUR"'
*
*
{
op: "=".
column: 'currency'
Expand All @@ -81,11 +84,13 @@ export declare type Filter = FilterClause | MultiClauseFilter;
This interface is only valid for a Filter that is being edioted
*/
export interface FilterWithPartialClause extends MultiClauseFilter {
filters: Array<Filter | Partial<Filter>>;
filters: Array<Partial<Filter>>;
}

export declare type FilterState = {
filter: Filter | undefined;
filterQuery: string;
filterName?: string;
};

export declare type ColumnDescriptorsByName = Record<string, ColumnDescriptor>;
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { clear } from "console";
import React from "react";
// TODO try and get TS path alias working to avoid relative paths like this
import { defaultPatternsByType, formatDate } from "@finos/vuu-utils";
import { DefaultFilterBar } from "../../../../../../showcase/src/examples/Filters/FilterBar/FilterBar.examples";

// Common selectors
Expand Down Expand Up @@ -61,7 +60,7 @@ describe("The mouse user", () => {
cy.get(OVERFLOW_CONTAINER).find("> *").should("have.length", 3);
cy.get(OVERFLOW_CONTAINER)
.find('[data-index="0"] > *')
.should("have.class", "vuuFilterClause");
.should("have.class", "vuuFilterBar-Editor");

cy.get(OVERFLOW_CONTAINER)
.find('[data-index="1"] > *')
Expand Down Expand Up @@ -137,43 +136,38 @@ describe("The mouse user", () => {
});

describe("WHEN user clicks APPLY AND SAVE", () => {
it("THEN filtersChangedHandler callback is invoked", () => {
const onFiltersChanged = cy.stub().as("filtersChangedHandler");
cy.mount(<DefaultFilterBar onFiltersChanged={onFiltersChanged} />);
cy.get(ADD_BUTTON).realClick();
clickListItems("currency", "=", "USD");
clickButton("APPLY AND SAVE");
cy.get("@filtersChangedHandler").should("be.calledWith", [
{ column: "currency", op: "=", value: "USD" },
]);
});
const testFilter = {
column: "currency",
op: "!=",
value: "USD",
};

it("THEN filter is applied", () => {
beforeEach(() => {
const onFiltersChanged = cy.stub().as("filtersChangedHandler");
const onFilterApplied = cy.stub().as("onFilterApplied");
const onApplyFilter = cy.stub().as("applyFilterHandler");
cy.mount(
<DefaultFilterBar
onApplyFilter={onFilterApplied}
onApplyFilter={onApplyFilter}
onFiltersChanged={onFiltersChanged}
/>
);
cy.get(ADD_BUTTON).realClick();
clickListItems("currency", "=", "USD");
clickListItems(testFilter.column, testFilter.op, testFilter.value);
clickButton("APPLY AND SAVE");
cy.get("@filtersChangedHandler").should("be.calledWith", [
{ column: "currency", op: "=", value: "USD" },
]);
cy.get("@onFilterApplied").should("be.calledWith", {
filter: 'currency = "USD"',
filterStruct: { column: "currency", op: "=", value: "USD" },
});

it("THEN filtersChangedHandler callback is invoked", () => {
cy.get("@filtersChangedHandler").should("be.calledWith", [testFilter]);
});

it("THEN filter is applied", () => {
cy.get("@applyFilterHandler").should("be.calledWith", {
filter: 'currency != "USD"',
filterStruct: testFilter,
});
});

it("THEN filter pill is displayed, label is in edit state and focused", () => {
cy.mount(<DefaultFilterBar />);
cy.get(ADD_BUTTON).realClick();
clickListItems("currency", "=", "USD");
clickButton("APPLY AND SAVE");
cy.get(OVERFLOW_CONTAINER).find("> *").should("have.length", 2);
findOverflowItem(".vuuFilterPill").should("have.length", 1);
findOverflowItem(".vuuFilterPill")
Expand All @@ -187,33 +181,51 @@ describe("The mouse user", () => {

describe("WHEN user overtypes label and presses ENTER", () => {
it("THEN label is applied and exits edit mode", () => {
const onFiltersChanged = cy.stub().as("filtersChangedHandler");
cy.mount(<DefaultFilterBar onFiltersChanged={onFiltersChanged} />);
cy.get(ADD_BUTTON).realClick();
clickListItems("currency", "=", "USD");
clickButton("APPLY AND SAVE");
waitUntilEditableLabelIsFocused(".vuuFilterPill");
cy.realType("test");
cy.realPress("Enter");
findOverflowItem(".vuuFilterPill")
.find(".vuuEditableLabel")
.should("not.have.class", "vuuEditableLabel-editing");
cy.get("@filtersChangedHandler").should("be.calledWith", [
{ column: "currency", op: "=", value: "USD", name: "test" },
{ ...testFilter, name: "test" },
]);
});

it("THEN filter pill has focus", () => {
cy.mount(<DefaultFilterBar />);
cy.get(ADD_BUTTON).realClick();
clickListItems("currency", "=", "USD");
clickButton("APPLY AND SAVE");
waitUntilEditableLabelIsFocused(".vuuFilterPill");
cy.realType("test");
cy.realPress("Enter");
findOverflowItem(".vuuFilterPill").should("be.focused");
});
});

describe("AND WHEN user edits the saved filter", () => {
it("THEN onFiltersChanged & onApplyFilter is called with new filter", () => {
const filterName = "EditedFilter";
const newFilter = { ...testFilter, value: "CAD", name: filterName };

waitUntilEditableLabelIsFocused(".vuuFilterPill");
cy.realType(filterName);
cy.realPress("Enter");

// Edit an existing filter
findOverflowItem(".vuuFilterPill")
.find(".vuuFilterPillMenu")
.realClick();
clickButton("Edit");
clickListItems(newFilter.column, newFilter.op, newFilter.value);
clickButton("APPLY AND SAVE");

cy.get("@filtersChangedHandler").should("be.calledWithExactly", [
newFilter,
]);
cy.get("@applyFilterHandler").should("be.calledWithExactly", {
filter: 'currency != "CAD"',
filterStruct: newFilter,
});
});
});
});
});

Expand Down Expand Up @@ -321,3 +333,110 @@ describe("The keyboard user", () => {
});
});
});

const getDate = (t: "start-today" | "start-tomorrow" | "end-today") => {
const today = new Date();
switch (t) {
case "start-today":
today.setHours(0, 0, 0, 0);
return today;
case "start-tomorrow":
return new Date(
today.getFullYear(),
today.getMonth(),
today.getDate() + 1
);
case "end-today":
today.setHours(23, 59, 59, 999);
return today;
}
};

describe("WHEN a user applies a date filter", () => {
const DATE_COLUMN = "lastUpdated";
const todayDateFormatted = formatDate({ date: defaultPatternsByType.date })(
new Date()
);
const startOfToday = getDate("start-today").getTime();
const endOfToday = getDate("end-today").getTime();
const startOfTomorrow = getDate("start-tomorrow").getTime();

beforeEach(() => {
const onApplyFilter = cy.stub().as("applyFilterHandler");
const onFiltersChanged = cy.stub().as("filtersChangedHandler");
cy.mount(
<DefaultFilterBar
onApplyFilter={onApplyFilter}
onFiltersChanged={onFiltersChanged}
/>
);
});

const testParams: Array<{
op: string;
expectedValue: number;
expectedQuery: string;
}> = [
{
op: "=",
expectedValue: startOfToday,
expectedQuery: `${DATE_COLUMN} >= ${startOfToday} and ${DATE_COLUMN} < ${startOfTomorrow}`,
},
{
op: "!=",
expectedValue: startOfToday,
expectedQuery: `${DATE_COLUMN} < ${startOfToday} or ${DATE_COLUMN} >= ${startOfTomorrow}`,
},
{
op: ">",
expectedValue: endOfToday,
expectedQuery: `${DATE_COLUMN} > ${endOfToday}`,
},
{
op: ">=",
expectedValue: startOfToday,
expectedQuery: `${DATE_COLUMN} >= ${startOfToday}`,
},
{
op: "<",
expectedValue: startOfToday,
expectedQuery: `${DATE_COLUMN} < ${startOfToday}`,
},
{
op: "<=",
expectedValue: endOfToday,
expectedQuery: `${DATE_COLUMN} <= ${endOfToday}`,
},
];

testParams.forEach(({ op, expectedValue, expectedQuery }) =>
it(`AND uses ${op} THEN resulting filter query can be understood by the VUU
server while the filter on the ui appears as selected by the user`, () => {
const expectedFilter = {
column: DATE_COLUMN,
op,
value: expectedValue,
name: `${DATE_COLUMN} ${op} "${todayDateFormatted}"`,
};

// Add date filter
cy.get(ADD_BUTTON).realClick();
clickListItems(DATE_COLUMN, op);
findOverflowItem(".vuuDatePicker-calendarIconButton").realClick();
cy.get(".saltCalendarDay-today").realClick();
cy.realPress("ArrowRight");
clickButton("APPLY AND SAVE");
waitUntilEditableLabelIsFocused(".vuuFilterPill");
cy.realPress("Enter");

// Check called handlers
cy.get("@applyFilterHandler").should("be.calledWithExactly", {
filter: expectedQuery,
filterStruct: expectedFilter,
});
cy.get("@filtersChangedHandler").should("be.calledWithExactly", [
expectedFilter,
]);
})
);
});
54 changes: 30 additions & 24 deletions vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.css
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@

.vuuFilterBar {
--vuu-svg-tune: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 18C3 18.55 3.45 19 4 19H9V17H4C3.45 17 3 17.45 3 18ZM3 6C3 6.55 3.45 7 4 7H13V5H4C3.45 5 3 5.45 3 6ZM13 20V19H20C20.55 19 21 18.55 21 18C21 17.45 20.55 17 20 17H13V16C13 15.45 12.55 15 12 15C11.45 15 11 15.45 11 16V20C11 20.55 11.45 21 12 21C12.55 21 13 20.55 13 20ZM7 10V11H4C3.45 11 3 11.45 3 12C3 12.55 3.45 13 4 13H7V14C7 14.55 7.45 15 8 15C8.55 15 9 14.55 9 14V10C9 9.45 8.55 9 8 9C7.45 9 7 9.45 7 10ZM21 12C21 11.45 20.55 11 20 11H11V13H20C20.55 13 21 12.55 21 12ZM16 9C16.55 9 17 8.55 17 8V7H20C20.55 7 21 6.55 21 6C21 5.45 20.55 5 20 5H17V4C17 3.45 16.55 3 16 3C15.45 3 15 3.45 15 4V8C15 8.55 15.45 9 16 9Z" /></svg>');
--vuuToolbar-height: 28px;
--salt-container-primary-borderColor: var(--vuu-color-purple-10);
--vuuOverflowContainer-minWidth: 0;
--saltButton-height: 26px;
--saltButton-width: 26px;
--vuu-svg-tune: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 18C3 18.55 3.45 19 4 19H9V17H4C3.45 17 3 17.45 3 18ZM3 6C3 6.55 3.45 7 4 7H13V5H4C3.45 5 3 5.45 3 6ZM13 20V19H20C20.55 19 21 18.55 21 18C21 17.45 20.55 17 20 17H13V16C13 15.45 12.55 15 12 15C11.45 15 11 15.45 11 16V20C11 20.55 11.45 21 12 21C12.55 21 13 20.55 13 20ZM7 10V11H4C3.45 11 3 11.45 3 12C3 12.55 3.45 13 4 13H7V14C7 14.55 7.45 15 8 15C8.55 15 9 14.55 9 14V10C9 9.45 8.55 9 8 9C7.45 9 7 9.45 7 10ZM21 12C21 11.45 20.55 11 20 11H11V13H20C20.55 13 21 12.55 21 12ZM16 9C16.55 9 17 8.55 17 8V7H20C20.55 7 21 6.55 21 6C21 5.45 20.55 5 20 5H17V4C17 3.45 16.55 3 16 3C15.45 3 15 3.45 15 4V8C15 8.55 15.45 9 16 9Z" /></svg>');
--vuuToolbar-height: 28px;
--salt-container-primary-borderColor: var(--vuu-color-purple-10);
--vuuOverflowContainer-minWidth: 0;
--saltButton-height: 26px;
--saltButton-width: 26px;

align-items: center;
background-color: var(--salt-container-secondary-background);
border-bottom: solid 1px #D6D7DA;
display: flex;
flex: var(--vuuFilterBar-flex);
gap: 4px;
height: 33px;
padding: 0px 8px;
align-items: center;
background-color: var(--salt-container-secondary-background);
border-bottom: solid 1px #d6d7da;
display: flex;
flex: var(--vuuFilterBar-flex);
gap: 4px;
height: 33px;
padding: 0px 8px;
}

.vuuFilterbar-icon {
display: inline-block;
height: 16px;
width: 16px;
display: inline-block;
height: 16px;
width: 16px;
}

.vuuFilterBar [data-icon='tune']{
--vuu-icon-size: 16px;
--vuu-icon-svg: var(--vuu-svg-tune);
.vuuFilterBar [data-icon="tune"] {
--vuu-icon-size: 16px;
--vuu-icon-svg: var(--vuu-svg-tune);
}

.vuuFilterBar [data-icon='plus']{
--vuu-icon-size: 16px;
.vuuFilterBar [data-icon="plus"] {
--vuu-icon-size: 16px;
}

.vuuFilterBar .vuuToolbar {
flex: 0 1 auto;
flex: 0 1 auto;
}

.vuuFilterBar-Editor {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
}
Loading
Loading