Skip to content

Commit 7f72c17

Browse files
committed
Two new controls, TimeInput and FilterColumn
this is an initial outline of these components still much to do
1 parent 3fe9bde commit 7f72c17

29 files changed

Lines changed: 1478 additions & 14 deletions

File tree

vuu-ui/packages/vuu-data-react/src/data-editing/get-data-item-edit-control.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@ import type {
55
import {
66
VuuDatePicker,
77
VuuInput,
8+
VuuTimePicker,
89
VuuTypeaheadInput,
910
VuuTypeaheadInputProps,
1011
} from "@vuu-ui/vuu-ui-controls";
11-
import { CommitHandler, isDateTimeDataValue } from "@vuu-ui/vuu-utils";
12+
import {
13+
CommitHandler,
14+
isDateTimeDataValue,
15+
isTimeDataValue,
16+
} from "@vuu-ui/vuu-utils";
1217
import { InputProps } from "@salt-ds/core";
1318

1419
export interface DataItemEditControlProps {
1520
InputProps?: Partial<InputProps>;
1621
TypeaheadProps?: Pick<VuuTypeaheadInputProps, "highlightFirstSuggestion">;
22+
className?: string;
1723
commitWhenCleared?: boolean;
1824
/**
1925
* A table column or form field Descriptor.
@@ -29,6 +35,7 @@ export type ValidationStatus = "initial" | true | string;
2935
export const getDataItemEditControl = ({
3036
InputProps,
3137
TypeaheadProps,
38+
className,
3239
commitWhenCleared,
3340
dataDescriptor,
3441
errorMessage,
@@ -42,6 +49,10 @@ export const getDataItemEditControl = ({
4249
onCommit(evt, value.toString());
4350
};
4451

52+
const handleCommitDate: CommitHandler<HTMLElement, Date> = (evt, value) => {
53+
onCommit(evt, value.toString());
54+
};
55+
4556
if (dataDescriptor.editable === false) {
4657
return (
4758
<VuuInput
@@ -51,13 +62,20 @@ export const getDataItemEditControl = ({
5162
readOnly
5263
/>
5364
);
65+
} else if (isTimeDataValue(dataDescriptor)) {
66+
return (
67+
<VuuTimePicker className={className} onCommit={handleCommitNumber} />
68+
);
5469
} else if (isDateTimeDataValue(dataDescriptor)) {
55-
return <VuuDatePicker onCommit={handleCommitNumber} />;
70+
return (
71+
<VuuDatePicker className={className} onCommit={handleCommitNumber} />
72+
);
5673
} else if (dataDescriptor.serverDataType === "string" && table) {
5774
return (
5875
<VuuTypeaheadInput
5976
{...InputProps}
6077
{...TypeaheadProps}
78+
className={className}
6179
column={dataDescriptor.name}
6280
onCommit={onCommit}
6381
table={table}
@@ -68,6 +86,7 @@ export const getDataItemEditControl = ({
6886
<VuuInput
6987
variant="secondary"
7088
{...InputProps}
89+
className={className}
7190
commitWhenCleared={commitWhenCleared}
7291
onCommit={onCommit}
7392
errorMessage={errorMessage}

vuu-ui/packages/vuu-data-types/index.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,14 @@ export declare type DataValueTypeSimple =
118118
| "boolean"
119119
| "json"
120120
| DateTimeDataValueTypeSimple
121+
| TimeDataValueTypeSimple
121122
| "checkbox";
122123

123124
export declare type DataValueType =
124125
| DataValueTypeSimple
125126
| DataValueTypeDescriptor;
126127

128+
export declare type TimeDataValueTypeSimple = "time";
127129
export declare type DateTimeDataValueTypeSimple = "date/time";
128130

129131
export declare type DateTimeDataValueType =
@@ -160,6 +162,13 @@ export declare type DateTimeDataValueDescriptor = Omit<
160162
type: DateTimeDataValueType;
161163
};
162164

165+
export declare type TimeDataValueDescriptor = Omit<
166+
DataValueDescriptor,
167+
"type"
168+
> & {
169+
type: TimeDataValueType;
170+
};
171+
163172
export interface DataSourceFilter extends VuuFilter {
164173
filterStruct?: Filter;
165174
}

vuu-ui/packages/vuu-filters/src/__tests__/__component__/FilterClause.cy.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe("FilterClause", () => {
4141
it("THEN component is rendered with controls for column, operator and value, value suggestions are offered", () => {
4242
cy.mount(
4343
<LocalDataSourceProvider>
44-
<PartialFilterClauseColumnAndOperatorWithDataSource />
44+
<PartialFilterClauseColumnAndOperator />{" "}
4545
</LocalDataSourceProvider>,
4646
);
4747
const container = cy.findByTestId("filterclause");
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.vuuColumnFilter {
2+
border: solid 1px var(--salt-editable-borderColor);
3+
border-radius: 4px;
4+
max-width: 200px;
5+
}
6+
7+
.vuuColumnFilter-trigger {
8+
--saltButton-borderRadius: 4px 0px 0px 4px;
9+
--saltButton-padding: var(--salt-spacing-200);
10+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {
2+
Button,
3+
Menu,
4+
MenuItem,
5+
MenuPanel,
6+
MenuTrigger,
7+
SegmentedButtonGroup,
8+
SegmentedButtonGroupProps,
9+
} from "@salt-ds/core";
10+
import { useComponentCssInjection } from "@salt-ds/styles";
11+
import { useWindow } from "@salt-ds/window";
12+
import cx from "clsx";
13+
14+
import columnFilterCss from "./ColumnFilter.css";
15+
import { getDataItemEditControl } from "@vuu-ui/vuu-data-react";
16+
import { useCallback, useMemo } from "react";
17+
import { CommitHandler } from "@vuu-ui/vuu-utils";
18+
import { VuuTable } from "@vuu-ui/vuu-protocol-types";
19+
import {
20+
assertValidValue,
21+
ColumnFilterHookProps,
22+
FilterValue,
23+
} from "./useColumnFilter";
24+
25+
const classBase = "vuuColumnFilter";
26+
27+
export interface ColumnFilterProps
28+
extends ColumnFilterHookProps,
29+
SegmentedButtonGroupProps {
30+
showOperatorPicker?: boolean;
31+
/**
32+
* VuuTable is required if typeahead support is expected.
33+
*/
34+
table?: VuuTable;
35+
36+
/**
37+
* Initial filter value. Pair of values expewcted when operator is
38+
* 'between'
39+
*/
40+
value?: FilterValue | [FilterValue, FilterValue];
41+
}
42+
43+
export const ColumnFilter = ({
44+
column,
45+
className,
46+
operator = "=",
47+
showOperatorPicker = false,
48+
table,
49+
value,
50+
...buttonGroupProps
51+
}: ColumnFilterProps) => {
52+
const targetWindow = useWindow();
53+
useComponentCssInjection({
54+
testId: "vuu-filter-bar",
55+
css: columnFilterCss,
56+
window: targetWindow,
57+
});
58+
59+
useMemo(
60+
() => assertValidValue(column, operator, value),
61+
[column, operator, value],
62+
);
63+
64+
const onCommit = useCallback<CommitHandler<HTMLElement>>((e, value) => {
65+
console.log(`onCommit ${value}`);
66+
}, []);
67+
68+
return (
69+
<SegmentedButtonGroup
70+
{...buttonGroupProps}
71+
className={cx(classBase, className)}
72+
>
73+
{showOperatorPicker ? (
74+
<Menu placement="bottom-start">
75+
<MenuTrigger>
76+
<Button
77+
appearance="solid"
78+
aria-label="Open Menu"
79+
className={`${classBase}-trigger`}
80+
data-embedded
81+
sentiment="neutral"
82+
>
83+
{operator}
84+
</Button>
85+
</MenuTrigger>
86+
<MenuPanel>
87+
<MenuItem>=</MenuItem>
88+
<MenuItem>!=</MenuItem>
89+
<MenuItem>starts</MenuItem>
90+
<MenuItem>ends</MenuItem>
91+
<MenuItem>contains</MenuItem>
92+
</MenuPanel>
93+
</Menu>
94+
) : null}
95+
{getDataItemEditControl({
96+
dataDescriptor: column,
97+
onCommit,
98+
table,
99+
})}
100+
{operator === "between"
101+
? getDataItemEditControl({
102+
dataDescriptor: column,
103+
onCommit,
104+
table,
105+
})
106+
: null}
107+
</SegmentedButtonGroup>
108+
);
109+
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { FilterOp } from "@vuu-ui/vuu-filter-types";
2+
import { ColumnDescriptor } from "@vuu-ui/vuu-table-types";
3+
4+
export type FilterValue = string | number;
5+
6+
export const assertValidValue = (
7+
{ serverDataType }: ColumnDescriptor,
8+
operator: FilterOp | "between",
9+
value?: FilterValue | FilterValue[],
10+
) => {
11+
if (value !== undefined) {
12+
if (operator === "between") {
13+
if (!Array.isArray(value)) {
14+
throw Error(
15+
"[useColumnFilter] 'between operator requires array of two values'",
16+
);
17+
} else if (value.length !== 2) {
18+
throw Error(
19+
`[useColumnFilter] 'between operator requires two values, received ${value.length}'`,
20+
);
21+
} else if (typeof value[0] !== typeof value[1]) {
22+
throw Error(
23+
`[useColumnFilter] 'between operator values must be of same type, received ${typeof value[0]} and ${typeof value[1]}`,
24+
);
25+
}
26+
}
27+
// TODO validate value(s) against serverDataType
28+
}
29+
};
30+
31+
export interface ColumnFilterHookProps {
32+
column: ColumnDescriptor;
33+
operator?: FilterOp | "between";
34+
}
35+
36+
export const useColumnFilter = ({
37+
column,
38+
operator = "=",
39+
}: ColumnFilterHookProps) => {};

vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import { HTMLAttributes, useMemo } from "react";
1010
import { CustomFilters } from "../custom-filters";
1111
import { QuickFilterProps, QuickFilters } from "../quick-filters";
1212
import { FilterMode, useFilterBar } from "./useFilterBar";
13+
import type { VuuTable } from "@vuu-ui/vuu-protocol-types";
1314

1415
import filterBarCss from "./FilterBar.css";
15-
import { VuuTable } from "@vuu-ui/vuu-protocol-types";
1616

1717
export type FilterBarVariant =
1818
| "custom-filters-only"

vuu-ui/packages/vuu-filters/src/filter-clause/filterClauseFocusManagement.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const getFocusedFieldDetails = (): [number, string] | [] => {
3737
return [getElementDataIndex(filterClause), getFilterClauseDetails(field)];
3838
}
3939
} else if (field?.matches(".saltCalendar")) {
40-
console.log("muthafucker is ins a calendar");
40+
console.log("its in a calendar");
4141
}
4242
// const filterClause = queryClosest(field, ".vuuFilterClause");
4343
// if (filterClause && field) {

vuu-ui/packages/vuu-filters/src/filter-clause/value-editors/FilterClauseValueEditor.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import {
88
NumericFilterClauseOp,
99
SingleValueFilterClauseOp,
1010
} from "@vuu-ui/vuu-filter-types";
11-
import { isDateTimeDataValue } from "@vuu-ui/vuu-utils";
11+
import { isDateTimeDataValue, isTimeDataValue } from "@vuu-ui/vuu-utils";
1212
import { ForwardedRef, forwardRef } from "react";
1313
import { FilterClauseValueEditorDate } from "./FilterClauseValueEditorDate";
14+
import { FilterClauseValueEditorTime } from "./FilterClauseValueEditorTime";
1415

1516
const classBase = "vuuFilterClause";
1617

@@ -51,7 +52,16 @@ export const FilterClauseValueEditor = forwardRef(
5152
<FilterClauseValueEditorDate
5253
inputProps={inputProps}
5354
className={cx(`${classBase}Field`, `${classBase}Value`)}
54-
// ref={forwardedRef}
55+
value={value as number}
56+
operator={operator as NumericFilterClauseOp}
57+
onChangeValue={onChangeValue}
58+
/>
59+
);
60+
} else if (isTimeDataValue(selectedColumn)) {
61+
return (
62+
<FilterClauseValueEditorTime
63+
inputProps={inputProps}
64+
className={cx(`${classBase}Field`, `${classBase}Value`)}
5565
value={value as number}
5666
operator={operator as NumericFilterClauseOp}
5767
onChangeValue={onChangeValue}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { useCallback, useState } from "react";
2+
import { getLocalTimeZone, DateValue } from "@internationalized/date";
3+
import { CommitHandler, toCalendarDate } from "@vuu-ui/vuu-utils";
4+
import { NumericFilterClauseOp } from "@vuu-ui/vuu-filter-types";
5+
import { FilterClauseValueEditor } from "../filterClauseTypes";
6+
import { VuuTimePicker } from "@vuu-ui/vuu-ui-controls";
7+
8+
interface FilterClauseValueEditorTimeProps
9+
extends Pick<FilterClauseValueEditor, "onChangeValue" | "inputProps"> {
10+
className?: string;
11+
value: number | undefined;
12+
operator: NumericFilterClauseOp;
13+
}
14+
15+
export const FilterClauseValueEditorTime = (
16+
props: FilterClauseValueEditorTimeProps,
17+
) => {
18+
const { inputProps, className, onChangeValue, operator, value } = props;
19+
const toEpochMilliS = getEpochMillisConverter(operator);
20+
21+
const [date, setDate] = useState<DateValue | undefined>(() =>
22+
getInitialState(value),
23+
);
24+
25+
const handleCommit = useCallback<CommitHandler<HTMLElement, number>>(
26+
(e, selectedDateInputValue) => {
27+
console.log("change time");
28+
if (selectedDateInputValue) {
29+
const dateValue = toCalendarDate(new Date(selectedDateInputValue));
30+
setDate(dateValue);
31+
if (selectedDateInputValue /* && source === "calendar"*/) {
32+
onChangeValue(toEpochMilliS(dateValue));
33+
}
34+
}
35+
},
36+
[onChangeValue, toEpochMilliS],
37+
);
38+
39+
const onBlur = useCallback(() => {
40+
date && onChangeValue(toEpochMilliS(date));
41+
}, [date, onChangeValue, toEpochMilliS]);
42+
43+
return (
44+
<VuuTimePicker
45+
data-field="value"
46+
inputProps={inputProps}
47+
className={className}
48+
onBlur={onBlur}
49+
onCommit={handleCommit}
50+
selectedDate={date}
51+
/>
52+
);
53+
};
54+
55+
function getInitialState(value: FilterClauseValueEditorTimeProps["value"]) {
56+
return value ? toCalendarDate(new Date(value)) : undefined;
57+
}
58+
59+
const getEpochMillisConverter =
60+
(op: NumericFilterClauseOp) =>
61+
(date: DateValue, timezone: string = getLocalTimeZone()): number => {
62+
const d = date.toDate(timezone);
63+
switch (op) {
64+
case ">":
65+
case "<=":
66+
d.setHours(23, 59, 59, 999);
67+
return d.getTime();
68+
case ">=":
69+
case "<":
70+
case "=": // converted to "< `start of next day` and >= `start of this day`" when query is created
71+
case "!=": // converted to ">= `start of next day` or < `start of this day`" when query is created
72+
d.setHours(0, 0, 0, 0);
73+
return d.getTime();
74+
}
75+
};

0 commit comments

Comments
 (0)