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

feat: DH-14630 - ACL Editor Hooks #1257

Merged
merged 54 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
1f4e4ae
useRollupTable hook
bmingles Apr 21, 2023
1145b33
useInitializeViewportData: handle size change
bmingles Apr 25, 2023
65a792c
createOnTableUpdatedHandler: Remove table arg
bmingles Apr 25, 2023
a145a1d
useInitializeViewportData: handle size change
bmingles Apr 25, 2023
b19bc58
TableUtils.applyCustomColumns
bmingles Apr 25, 2023
dbda2ef
ViewportDataUtils.getItems
bmingles Apr 25, 2023
e535b3c
useViewportData: applyFiltersAndRefresh function
bmingles Apr 25, 2023
a8f445e
useTableSize hook
bmingles Apr 26, 2023
05b973d
Selection utils
bmingles Apr 27, 2023
b7ae694
useTableSize tests
bmingles Apr 27, 2023
db37d61
useViewportData: switched to useTableSize
bmingles Apr 27, 2023
5414821
Renamed getItems to getItemsFromListData
bmingles Apr 27, 2023
7f0f9e8
useViewportData: added table to return data
bmingles Apr 27, 2023
58b638b
useTableCloseOnUnmount hook
bmingles Apr 27, 2023
660b08f
useViewportData: removed console log
bmingles Apr 27, 2023
234941a
useDebouncedViewportSearch
bmingles Apr 27, 2023
825a69e
useFormWithDetachedSubmitButton hook
bmingles Apr 28, 2023
af403f1
TableUtils: wrappers for table apply methods
bmingles May 1, 2023
3d44c49
Comment cleanup
bmingles May 1, 2023
1f01a00
useDebouncedViewportSearch: unit tests
bmingles May 1, 2023
b37ee85
useInitializeView: bug fix + unit tests
bmingles May 1, 2023
2e0a9ca
useRollupTable: deleting unused hook
bmingles May 1, 2023
9b12198
useInitializeViewportData: re-enabled tests
bmingles May 1, 2023
da16cad
useTableCloseOnUnmount: unit tests
bmingles May 1, 2023
ee6e407
useTableSize: unit test improvement
bmingles May 1, 2023
131d24d
useViewportData: fixed tests
bmingles May 1, 2023
8e0b2af
useViewportData: consolidated size check
bmingles May 1, 2023
e16e959
TestUtils.asMock
bmingles May 2, 2023
fae21b0
Simplified mock
bmingles May 2, 2023
8dc80cc
useViewportData:
bmingles May 2, 2023
adf735b
useViewportData: unit test
bmingles May 2, 2023
751ece2
TableUtils unit tests
bmingles May 2, 2023
6a64ca1
TableUtils: split out executeAndWaitForEvent
bmingles May 2, 2023
4dd3d3c
executeAndWaitForEvent tests
bmingles May 2, 2023
b4463d7
TableUtils: unit tests
bmingles May 2, 2023
9b045e6
Renamed arg + added comment
bmingles May 2, 2023
ba467bb
ViewportDataUtils: fixed tests
bmingles May 2, 2023
5039de8
getItemsFromListData test
bmingles May 2, 2023
2d48455
useFormWithDetachedSubmitButton: Unit tests
bmingles May 2, 2023
3659886
useFormWithDetachedSubmitButton: Cleanup counter
bmingles May 2, 2023
34050f4
useFormWithDetachedSubmitButton test
bmingles May 2, 2023
36d9816
findLastCall test
bmingles May 2, 2023
7ce7ea0
createMockProxy test
bmingles May 2, 2023
396ca14
Added missing import after rebase
bmingles May 2, 2023
21d7b82
Changed some imports to @deephaven/jsapi-types
bmingles May 2, 2023
0187f2a
Change to a type only import
bmingles May 2, 2023
124b423
Code coverage
bmingles May 2, 2023
7f6eaa5
Code coverage
bmingles May 2, 2023
b437f6e
Code coverage
bmingles May 2, 2023
0c3e0f4
Code coverage
bmingles May 3, 2023
ebdb99e
Addressed some code review comments
bmingles May 4, 2023
55aee4f
Addressed code review comments
bmingles May 4, 2023
73f57d7
Moved @testing-library/react-hooks dev dependency
bmingles May 4, 2023
3a6c828
Address code review comment
bmingles May 4, 2023
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
11 changes: 7 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"@playwright/test": "^1.30.0",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.4.3",
"@types/bootstrap": "^4.4.1",
"@types/color-convert": "^2.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/jsapi-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@deephaven/log": "file:../log",
"@deephaven/react-hooks": "file:../react-hooks",
"@deephaven/utils": "file:../utils",
"lodash.debounce": "^4.0.8",
"@react-stately/data": "^3.9.1",
"@types/js-cookie": "^3.0.3",
"classnames": "^2.3.2",
Expand All @@ -38,7 +39,6 @@
"devDependencies": {
"@deephaven/jsapi-shim": "file:../jsapi-shim",
"@deephaven/tsconfig": "file:../tsconfig",
"@testing-library/react-hooks": "^8.0.1",
"react-test-renderer": "^17.0.2"
},
"peerDependencies": {
Expand Down
5 changes: 5 additions & 0 deletions packages/jsapi-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ export * from './RefreshTokenBootstrap';
export * from './RefreshTokenUtils';
export { default as useBroadcastChannel } from './useBroadcastChannel';
export { default as useBroadcastLoginListener } from './useBroadcastLoginListener';
export { default as useDebouncedViewportSearch } from './useDebouncedViewportSearch';
export { default as useInitializeViewportData } from './useInitializeViewportData';
export { default as useTable } from './useTable';
export { default as useTableColumn } from './useTableColumn';
export { default as useTableListener } from './useTableListener';
export { default as useSelectDistinctTable } from './useSelectDistinctTable';
export { default as useSetPaddedViewportCallback } from './useSetPaddedViewportCallback';
export { default as useTableClose } from './useTableClose';
export { default as useTableSize } from './useTableSize';
export { default as useViewportData } from './useViewportData';
export type { UseSelectDistinctTableResult } from './useSelectDistinctTable';
export type { UseViewportDataResult } from './useViewportData';
132 changes: 132 additions & 0 deletions packages/jsapi-components/src/useDebouncedViewportSearch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { renderHook } from '@testing-library/react-hooks';
import type {
Column,
FilterCondition,
FilterValue,
Table,
} from '@deephaven/jsapi-types';
import { TableUtils } from '@deephaven/jsapi-utils';
import { TestUtils } from '@deephaven/utils';
import useDebouncedViewportSearch, {
DEBOUNCE_VIEWPORT_SEARCH_MS,
} from './useDebouncedViewportSearch';
import { UseViewportDataResult } from './useViewportData';

// Mock js api objects
const column = TestUtils.createMockProxy<Column>({
type: 'java.lang.String',
});
const filterCondition = TestUtils.createMockProxy<FilterCondition>({});
const columnFilterValue = TestUtils.createMockProxy<FilterValue>({});
const matchFilterValue = TestUtils.createMockProxy<FilterValue>({});
const table = TestUtils.createMockProxy<Table>({});
const viewportData = TestUtils.createMockProxy<
UseViewportDataResult<unknown, Table>
>({ table });

beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();

TestUtils.asMock(column.filter).mockReturnValue(columnFilterValue);
TestUtils.asMock(columnFilterValue.contains).mockReturnValue(filterCondition);
TestUtils.asMock(table.findColumn).mockReturnValue(column);

jest.spyOn(TableUtils, 'makeFilterValue').mockReturnValue(matchFilterValue);
});

it.each([undefined, 400])(
'should return a function that debounces search filtering',
debounceMs => {
const searchText = 'mock.searchText';

const { result } = renderHook(() =>
useDebouncedViewportSearch(viewportData, 'mock.column', debounceMs)
);

result.current(searchText);

expect(viewportData.applyFiltersAndRefresh).not.toHaveBeenCalled();

jest.advanceTimersByTime(debounceMs ?? DEBOUNCE_VIEWPORT_SEARCH_MS);

expect(TableUtils.makeFilterValue).toHaveBeenCalledWith(
column.type,
searchText
);
expect(viewportData.applyFiltersAndRefresh).toHaveBeenCalledWith([
filterCondition,
]);
}
);

it('should return a function that safely ignores no table', () => {
const viewportNoTable = TestUtils.createMockProxy<
UseViewportDataResult<unknown, Table>
>({ table: null });

const searchText = 'mock.searchText';
const debounceMs = 400;

const { result } = renderHook(() =>
useDebouncedViewportSearch(viewportNoTable, 'mock.column', debounceMs)
);

result.current(searchText);
jest.advanceTimersByTime(debounceMs);

expect(TableUtils.makeFilterValue).not.toHaveBeenCalled();
expect(viewportNoTable.applyFiltersAndRefresh).not.toHaveBeenCalled();
});

it('should trim search text', () => {
const searchText = ' mock.searchText ';
const trimmedSearchText = 'mock.searchText';
const debounceMs = 400;

const { result } = renderHook(() =>
useDebouncedViewportSearch(viewportData, 'mock.column', debounceMs)
);

result.current(searchText);
jest.advanceTimersByTime(debounceMs);

expect(TableUtils.makeFilterValue).toHaveBeenCalledWith(
column.type,
trimmedSearchText
);
});

it.each(['', ' '])(
'should pass apply empty filter if search text is empty',
searchText => {
const debounceMs = 400;

const { result } = renderHook(() =>
useDebouncedViewportSearch(viewportData, 'mock.column', debounceMs)
);

result.current(searchText);
jest.advanceTimersByTime(debounceMs);

expect(TableUtils.makeFilterValue).not.toHaveBeenCalled();
expect(viewportData.applyFiltersAndRefresh).toHaveBeenCalledWith([]);
}
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add another test:

it('should cancel debounce on unmount', () => {
  const searchText = 'mock.searchText';
  const debounceMs = 400;

  const { result, unmount } = renderHook(() =>
    useDebouncedViewportSearch(viewportData, 'mock.column', debounceMs)
  );

  result.current(searchText);
  jest.advanceTimersByTime(5);
  unmount();
  jest.advanceTimersByTime(debounceMs);

  expect(TableUtils.makeFilterValue).not.toHaveBeenCalled();
  expect(viewportData.applyFiltersAndRefresh).not.toHaveBeenCalled();
});


it('should cancel debounce on unmount', () => {
const searchText = 'mock.searchText';
const debounceMs = 400;

const { result, unmount } = renderHook(() =>
useDebouncedViewportSearch(viewportData, 'mock.column', debounceMs)
);

result.current(searchText);
jest.advanceTimersByTime(5);
unmount();
jest.advanceTimersByTime(debounceMs);

expect(TableUtils.makeFilterValue).not.toHaveBeenCalled();
expect(viewportData.applyFiltersAndRefresh).not.toHaveBeenCalled();
});
58 changes: 58 additions & 0 deletions packages/jsapi-components/src/useDebouncedViewportSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import debounce from 'lodash.debounce';
import type { Table, TreeTable } from '@deephaven/jsapi-types';
import { TableUtils } from '@deephaven/jsapi-utils';
import { useEffect, useMemo } from 'react';
import { UseViewportDataResult } from './useViewportData';

export const DEBOUNCE_VIEWPORT_SEARCH_MS = 200;

/**
* React hook that returns a debounced search callback for filtering a table
* viewport.
* @param viewportData Table viewport to filter
* @param columnName Column name to filter by
* @param debounceMs Millisecond value to debounce
*/
export default function useDebouncedViewportSearch<
I,
T extends Table | TreeTable
>(
viewportData: UseViewportDataResult<I, T>,
columnName: string,
debounceMs = DEBOUNCE_VIEWPORT_SEARCH_MS
): (searchText: string) => void {
const debouncedSearch = useMemo(
() =>
debounce((searchText: string) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be cancelling the debounce on unmount.

if (viewportData.table == null) {
return;
}

const searchTextTrimmed = searchText.trim();

if (searchTextTrimmed === '') {
viewportData.applyFiltersAndRefresh([]);
return;
}

const column = viewportData.table.findColumn(columnName);
const value = TableUtils.makeFilterValue(
column.type,
searchTextTrimmed
);
const filter = [column.filter().contains(value)];

viewportData.applyFiltersAndRefresh(filter);
}, debounceMs),
[columnName, debounceMs, viewportData]
);

useEffect(
() => () => {
debouncedSearch.cancel();
},
[debouncedSearch]
);

return debouncedSearch;
}
37 changes: 36 additions & 1 deletion packages/jsapi-components/src/useInitializeViewportData.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { Table } from '@deephaven/jsapi-shim';
import type { Table } from '@deephaven/jsapi-types';
import { KeyedItem } from '@deephaven/jsapi-utils';
import { TestUtils } from '@deephaven/utils';
import useInitializeViewportData from './useInitializeViewportData';
import useTableSize from './useTableSize';

jest.mock('./useTableSize');

const tableA = TestUtils.createMockProxy<Table>({ size: 4 });
const expectedInitialA: KeyedItem<unknown>[] = [
Expand All @@ -15,6 +18,15 @@ const expectedInitialA: KeyedItem<unknown>[] = [
const tableB = TestUtils.createMockProxy<Table>({ size: 2 });
const expectedInitialB = [{ key: '0' }, { key: '1' }];

beforeEach(() => {
TestUtils.asMock(useTableSize).mockImplementation(table => table?.size ?? 0);
});

it.each([null])('should safely handle no table: %s', noTable => {
const { result } = renderHook(() => useInitializeViewportData(noTable));
expect(result.current.items).toEqual([]);
});

it('should initialize a ListData object based on Table size', () => {
const { result } = renderHook(() => useInitializeViewportData(tableA));

Expand Down Expand Up @@ -43,3 +55,26 @@ it('should re-initialize a ListData object if Table reference changes', () => {

expect(result.current.items).toEqual(expectedInitialB);
});

it.each([
[3, [...expectedInitialA.slice(0, -1)]],
[5, [...expectedInitialA, { key: '4' }]],
])(
'should re-initialize a ListData object if Table size changes',
(newSize, expectedAfterSizeChange) => {
const { result, rerender } = renderHook(
({ table }) => useInitializeViewportData(table),
{
initialProps: { table: tableA },
}
);

expect(result.current.items).toEqual(expectedInitialA);

// Re-render with new size
TestUtils.asMock(useTableSize).mockImplementation(() => newSize);
rerender({ table: tableA });

expect(result.current.items).toEqual(expectedAfterSizeChange);
}
);
Loading