Skip to content

Commit f06a141

Browse files
authored
feat: Picker - formatter settings (#1907)
- useFormatter hook + optional settings prop in jsapi Picker - Fixed a bug with `bindAllMethods` function. It now excludes getters resolves #1889
1 parent e17619a commit f06a141

10 files changed

Lines changed: 272 additions & 14 deletions

File tree

packages/jsapi-components/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export { default as useDebouncedViewportSearch } from './useDebouncedViewportSea
1111
export * from './useDebouncedViewportSelectionFilter';
1212
export * from './useFilterConditionFactories';
1313
export * from './useFilteredItemsWithDefaultValue';
14+
export * from './useFormatter';
1415
export * from './useGetItemIndexByValue';
1516
export * from './useGetItemPosition';
1617
export { default as useInitializeViewportData } from './useInitializeViewportData';

packages/jsapi-components/src/spectrum/Picker/Picker.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import {
33
Picker as PickerBase,
44
PickerProps as PickerPropsBase,
55
} from '@deephaven/components';
6-
import { useApi } from '@deephaven/jsapi-bootstrap';
76
import { dh as DhType } from '@deephaven/jsapi-types';
8-
import { Formatter } from '@deephaven/jsapi-utils';
7+
import { Settings } from '@deephaven/jsapi-utils';
98
import Log from '@deephaven/log';
109
import { PICKER_ITEM_HEIGHT, PICKER_TOP_OFFSET } from '@deephaven/utils';
1110
import { useCallback, useEffect, useMemo } from 'react';
11+
import useFormatter from '../../useFormatter';
1212
import useGetItemIndexByValue from '../../useGetItemIndexByValue';
1313
import { useViewportData } from '../../useViewportData';
1414
import { getPickerKeyColumn } from './PickerUtils';
@@ -24,21 +24,19 @@ export interface PickerProps extends Omit<PickerPropsBase, 'children'> {
2424
labelColumn?: string;
2525

2626
// TODO #1890 : descriptionColumn, iconColumn
27+
28+
settings?: Settings;
2729
}
2830

2931
export function Picker({
3032
table,
3133
keyColumn: keyColumnName,
3234
labelColumn: labelColumnName,
3335
selectedKey,
36+
settings,
3437
...props
3538
}: PickerProps): JSX.Element {
36-
const dh = useApi();
37-
38-
const formatValue = useMemo(() => {
39-
const formatter = new Formatter(dh);
40-
return formatter.getFormattedString.bind(formatter);
41-
}, [dh]);
39+
const { getFormattedString: formatValue } = useFormatter(settings);
4240

4341
const keyColumn = useMemo(
4442
() => getPickerKeyColumn(table, keyColumnName),
@@ -100,7 +98,7 @@ export function Picker({
10098
isCanceled = true;
10199
};
102100
},
103-
[getItemIndexByValue, setViewport]
101+
[getItemIndexByValue, settings, setViewport]
104102
);
105103

106104
return (
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { useApi } from '@deephaven/jsapi-bootstrap';
2+
import { bindAllMethods, TestUtils } from '@deephaven/utils';
3+
import {
4+
createFormatterFromSettings,
5+
Formatter,
6+
Settings,
7+
} from '@deephaven/jsapi-utils';
8+
import type { dh as DhType } from '@deephaven/jsapi-types';
9+
import { renderHook } from '@testing-library/react-hooks';
10+
import { useFormatter } from './useFormatter';
11+
12+
jest.mock('@deephaven/jsapi-bootstrap');
13+
jest.mock('@deephaven/jsapi-utils', () => {
14+
const actual = jest.requireActual('@deephaven/jsapi-utils');
15+
return {
16+
...actual,
17+
createFormatterFromSettings: jest.fn(),
18+
};
19+
});
20+
jest.mock('@deephaven/utils', () => ({
21+
...jest.requireActual('@deephaven/utils'),
22+
bindAllMethods: jest.fn(),
23+
}));
24+
25+
const { asMock, createMockProxy } = TestUtils;
26+
27+
beforeEach(() => {
28+
jest.clearAllMocks();
29+
expect.hasAssertions();
30+
});
31+
32+
describe('useFormatter', () => {
33+
const mock = {
34+
dh: createMockProxy<typeof DhType>(),
35+
formatter: createMockProxy<Formatter>(),
36+
settings: createMockProxy<Settings>(),
37+
};
38+
39+
beforeEach(() => {
40+
asMock(useApi).mockReturnValue(mock.dh);
41+
42+
asMock(bindAllMethods)
43+
.mockName('bindAllMethods')
44+
.mockImplementation(a => a);
45+
46+
asMock(createFormatterFromSettings)
47+
.mockName('createFormatterFromSettings')
48+
.mockReturnValue(mock.formatter);
49+
});
50+
51+
it('should return members of a `Formatter` instance based on settings', () => {
52+
const { result } = renderHook(() => useFormatter(mock.settings));
53+
54+
expect(createFormatterFromSettings).toHaveBeenCalledWith(
55+
mock.dh,
56+
mock.settings
57+
);
58+
59+
expect(bindAllMethods).toHaveBeenCalledWith(mock.formatter);
60+
61+
expect(result.current).toEqual({
62+
getColumnFormat: mock.formatter.getColumnFormat,
63+
getColumnFormatMapForType: mock.formatter.getColumnFormatMapForType,
64+
getColumnTypeFormatter: mock.formatter.getColumnTypeFormatter,
65+
getFormattedString: mock.formatter.getFormattedString,
66+
timeZone: mock.formatter.timeZone,
67+
});
68+
});
69+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useApi } from '@deephaven/jsapi-bootstrap';
2+
import {
3+
createFormatterFromSettings,
4+
Formatter,
5+
Settings,
6+
} from '@deephaven/jsapi-utils';
7+
import { bindAllMethods } from '@deephaven/utils';
8+
import { useMemo } from 'react';
9+
10+
export type UseFormatterResult = Pick<
11+
Formatter,
12+
| 'getColumnFormat'
13+
| 'getColumnFormatMapForType'
14+
| 'getColumnTypeFormatter'
15+
| 'getFormattedString'
16+
| 'timeZone'
17+
>;
18+
19+
/**
20+
* Returns a subset of members of a `Formatter` instance. The `Formatter` will be
21+
* constructed based on the given options or fallback to the configuration found
22+
* in the current `FormatSettingsContext`. Members that are functions are bound
23+
* to the `Formatter` instance, so they are safe to destructure. Static methods
24+
* can still be accessed statically from the `Formatter` class.
25+
* @param settings Optional settings to use when constructing the `Formatter`
26+
*/
27+
export function useFormatter(settings?: Settings): UseFormatterResult {
28+
const dh = useApi();
29+
30+
const formatter = useMemo(() => {
31+
const instance = createFormatterFromSettings(dh, settings);
32+
33+
// Bind all methods so we can destructure them
34+
bindAllMethods(instance);
35+
36+
return instance;
37+
}, [dh, settings]);
38+
39+
const {
40+
getColumnFormat,
41+
getColumnFormatMapForType,
42+
getColumnTypeFormatter,
43+
getFormattedString,
44+
} = formatter;
45+
46+
return {
47+
getColumnFormat,
48+
getColumnFormatMapForType,
49+
getColumnTypeFormatter,
50+
getFormattedString,
51+
timeZone: formatter.timeZone,
52+
};
53+
}
54+
55+
export default useFormatter;

packages/jsapi-components/src/useGetItemIndexByValue.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ describe('useGetItemIndexByValue', () => {
5454
}
5555
);
5656

57+
it('should throw if seekRow fails', async () => {
58+
asMock(mockTable.seekRow).mockRejectedValue('Some error');
59+
60+
const { result } = renderHook(() =>
61+
useGetItemIndexByValue({
62+
columnName: 'mock.columnName',
63+
table: mockTable,
64+
value: 'mock.value',
65+
})
66+
);
67+
68+
expect(result.current()).rejects.toEqual('Some error');
69+
});
70+
5771
it.each([
5872
['mock.value', 42, 42],
5973
['mock.value', -1, null],

packages/jsapi-components/src/useGetItemIndexByValue.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ export function useGetItemIndexByValue<TValue>({
3131
const columnValueType = tableUtils.getValueType(column.type);
3232

3333
const index = await table.seekRow(0, column, columnValueType, value);
34-
3534
return index === -1 ? null : index;
3635
}, [columnName, table, tableUtils, value]);
3736
}

packages/jsapi-utils/src/FormatterUtils.test.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import Formatter from './Formatter';
1+
import dh from '@deephaven/jsapi-shim';
2+
import { TestUtils } from '@deephaven/utils';
3+
import Formatter, { FormattingRule } from './Formatter';
24
import FormatterUtils, {
5+
createFormatterFromSettings,
36
getColumnFormats,
47
getDateTimeFormatterOptions,
58
} from './FormatterUtils';
@@ -9,7 +12,31 @@ import {
912
TableColumnFormatType,
1013
} from './formatters';
1114
import TableUtils from './TableUtils';
12-
import { ColumnFormatSettings, DateTimeFormatSettings } from './Settings';
15+
import Settings, {
16+
ColumnFormatSettings,
17+
DateTimeFormatSettings,
18+
} from './Settings';
19+
20+
jest.mock('./Formatter', () => {
21+
const FormatterActual = jest.requireActual('./Formatter').default;
22+
23+
// Extract static methods
24+
const { makeColumnFormatMap, makeColumnFormattingRule } = FormatterActual;
25+
26+
// Wrap Formatter constructor so we can spy on it
27+
const mockFormatter = jest.fn((...args) => new FormatterActual(...args));
28+
29+
return {
30+
__esModule: true,
31+
default: Object.assign(mockFormatter, {
32+
makeColumnFormatMap,
33+
makeColumnFormattingRule,
34+
}),
35+
};
36+
});
37+
38+
const { createMockProxy } = TestUtils;
39+
const FormatterActual = jest.requireActual('./Formatter').default;
1340

1441
function makeFormatter(...settings: ConstructorParameters<typeof Formatter>) {
1542
return new Formatter(...settings);
@@ -23,6 +50,46 @@ function makeFormattingRule(
2350
return { label, formatString, type };
2451
}
2552

53+
describe('createFormatterFromSettings', () => {
54+
const mockSettings = createMockProxy<Settings>({
55+
formatter: [
56+
createMockProxy<FormattingRule>({
57+
format: createMockProxy<TableColumnFormat>(),
58+
}),
59+
],
60+
});
61+
62+
it.each([undefined, mockSettings])(
63+
'should create a formatter with the given settings: %s',
64+
settings => {
65+
const columnRules = getColumnFormats(settings);
66+
const dateTimeOptions = getDateTimeFormatterOptions(settings);
67+
68+
const {
69+
defaultDecimalFormatOptions,
70+
defaultIntegerFormatOptions,
71+
truncateNumbersWithPound,
72+
showEmptyStrings,
73+
showNullStrings,
74+
} = settings ?? {};
75+
76+
const actual = createFormatterFromSettings(dh, settings);
77+
78+
expect(Formatter).toHaveBeenCalledWith(
79+
dh,
80+
columnRules,
81+
dateTimeOptions,
82+
defaultDecimalFormatOptions,
83+
defaultIntegerFormatOptions,
84+
truncateNumbersWithPound,
85+
showEmptyStrings,
86+
showNullStrings
87+
);
88+
expect(actual).toBeInstanceOf(FormatterActual);
89+
}
90+
);
91+
});
92+
2693
describe('isCustomColumnFormatDefined', () => {
2794
const columnFormats = [
2895
Formatter.makeColumnFormattingRule(

packages/jsapi-utils/src/FormatterUtils.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,44 @@
1+
import type { dh as DhType } from '@deephaven/jsapi-types';
12
import type { FormattingRule } from './Formatter';
23
import Formatter from './Formatter';
34
import { DateTimeColumnFormatter, TableColumnFormatter } from './formatters';
4-
import { ColumnFormatSettings, DateTimeFormatSettings } from './Settings';
5+
import Settings, {
6+
ColumnFormatSettings,
7+
DateTimeFormatSettings,
8+
} from './Settings';
9+
10+
/**
11+
* Instantiate a `Formatter` from the given settings.
12+
* @param dh The `dh` object
13+
* @param settings Optional settings to use
14+
* @returns A new `Formatter` instance
15+
*/
16+
export function createFormatterFromSettings(
17+
dh: typeof DhType,
18+
settings?: Settings
19+
): Formatter {
20+
const columnRules = getColumnFormats(settings);
21+
const dateTimeOptions = getDateTimeFormatterOptions(settings);
22+
23+
const {
24+
defaultDecimalFormatOptions,
25+
defaultIntegerFormatOptions,
26+
truncateNumbersWithPound,
27+
showEmptyStrings,
28+
showNullStrings,
29+
} = settings ?? {};
30+
31+
return new Formatter(
32+
dh,
33+
columnRules,
34+
dateTimeOptions,
35+
defaultDecimalFormatOptions,
36+
defaultIntegerFormatOptions,
37+
truncateNumbersWithPound,
38+
showEmptyStrings,
39+
showNullStrings
40+
);
41+
}
542

643
export function getColumnFormats(
744
settings?: ColumnFormatSettings
@@ -45,6 +82,7 @@ export function isCustomColumnFormatDefined(
4582
}
4683

4784
export default {
85+
createFormatterFromSettings,
4886
getColumnFormats,
4987
getDateTimeFormatterOptions,
5088
isCustomColumnFormatDefined,

packages/utils/src/ClassUtils.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ class Aaa {
77
getAaa() {
88
return this.nameA;
99
}
10+
11+
// eslint-disable-next-line class-methods-use-this
12+
get someGetter() {
13+
throw new Error('This should not get evaluated as a method');
14+
}
1015
}
1116

1217
class Bbb extends Aaa {
@@ -15,6 +20,11 @@ class Bbb extends Aaa {
1520
getBbb() {
1621
return this.nameB;
1722
}
23+
24+
// eslint-disable-next-line class-methods-use-this
25+
get someGetter() {
26+
throw new Error('This should not get evaluated as a method');
27+
}
1828
}
1929

2030
class Ccc extends Bbb {
@@ -25,6 +35,11 @@ class Ccc extends Bbb {
2535
}
2636

2737
getCcc2 = () => this.nameC;
38+
39+
// eslint-disable-next-line class-methods-use-this
40+
get someGetter() {
41+
throw new Error('This should not get evaluated as a method');
42+
}
2843
}
2944

3045
beforeEach(() => {

0 commit comments

Comments
 (0)