@controleonline/ui-default 1.0.270 → 1.0.272

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/manager-worker.yml +19 -0
  3. package/AGENTS.md +46 -50
  4. package/docs/technical/Contrato-de-colunas.md +67 -0
  5. package/docs/technical/Default-Components.md +76 -0
  6. package/docs/technical/DefaultForm.md +80 -0
  7. package/docs/technical/DefaultInput-e-DefaultSelect.md +73 -0
  8. package/docs/technical/DefaultTable.md +203 -0
  9. package/docs/wiki.md +1 -0
  10. package/package.json +22 -21
  11. package/scripts/validate-wiki-pointer.cjs +13 -0
  12. package/src/react/components/address/DefaultAddress.js +435 -0
  13. package/src/react/components/address/DefaultAddress.styles.js +133 -0
  14. package/src/react/components/address/DefaultAddressMapPane.js +55 -0
  15. package/src/react/components/address/LatLonReadonlyFields.js +51 -0
  16. package/src/react/components/address/defaultAddressHelpers.js +163 -0
  17. package/src/react/components/address/defaultAddressStyles.js +136 -0
  18. package/src/react/components/address/defaultAddressUtils.js +129 -0
  19. package/src/react/components/extra-data/DefaultExtraData.js +152 -0
  20. package/src/react/components/extra-data/DefaultExtraData.styles.js +61 -0
  21. package/src/react/components/files/DefaultFile.js +231 -46
  22. package/src/react/components/files/DefaultFileColumn.js +123 -0
  23. package/src/react/components/files/DefaultFileColumn.styles.js +52 -0
  24. package/src/react/components/files/DefaultFileColumn.utils.js +104 -0
  25. package/src/react/components/filters/CompactFilterSelector.js +51 -35
  26. package/src/react/components/filters/DefaultColumnFilter.js +58 -41
  27. package/src/react/components/filters/DefaultExternalFilters.js +331 -265
  28. package/src/react/components/form/DefaultForm.js +99 -79
  29. package/src/react/components/help/DefaultTooltip.js +59 -33
  30. package/src/react/components/help/DefaultTooltip.styles.js +23 -3
  31. package/src/react/components/inputs/DefaultColorInput.js +153 -0
  32. package/src/react/components/inputs/DefaultFeatherIconPicker.js +146 -0
  33. package/src/react/components/inputs/DefaultFeatherIconPicker.styles.js +113 -0
  34. package/src/react/components/inputs/DefaultInput.js +69 -5
  35. package/src/react/components/inputs/DefaultInput.styles.js +26 -0
  36. package/src/react/components/inputs/DefaultSelect.js +134 -123
  37. package/src/react/components/inputs/defaultInputUtils.js +69 -43
  38. package/src/react/components/map/DefaultMap.native.js +73 -73
  39. package/src/react/components/map/DefaultMap.shared.js +911 -762
  40. package/src/react/components/map/DefaultMap.web.js +124 -68
  41. package/src/react/components/table/DefaultColumnMenu.js +109 -108
  42. package/src/react/components/table/DefaultDebug.js +44 -37
  43. package/src/react/components/table/DefaultSearchModal.js +9 -1
  44. package/src/react/components/table/DefaultTable.grouping.js +98 -0
  45. package/src/react/components/table/DefaultTable.js +213 -107
  46. package/src/react/components/table/DefaultTable.styles.js +367 -314
  47. package/src/react/components/table/DefaultTableBody.js +13 -8
  48. package/src/react/components/table/DefaultTableCards.js +20 -1
  49. package/src/react/components/table/DefaultTableControls.js +180 -205
  50. package/src/react/components/table/DefaultTableImportModal.js +108 -0
  51. package/src/react/components/table/DefaultTableInput.js +1 -0
  52. package/src/react/components/table/DefaultTableRowActions.js +1 -1
  53. package/src/react/components/table/DefaultTableRows.js +224 -363
  54. package/src/react/components/table/DefaultTableSearch.js +28 -5
  55. package/src/react/components/table/DefaultTableToolbar.js +92 -3
  56. package/src/react/components/table/useDefaultTablePagination.js +56 -7
  57. package/src/react/components/table/useDefaultTableStoreSync.js +164 -0
  58. package/src/react/components/upload/DefaultUpload.js +436 -0
  59. package/src/react/components/upload/DefaultUpload.styles.js +153 -0
  60. package/src/react/components/upload/DefaultUploadAttachmentsList.js +138 -0
  61. package/src/react/components/upload/DefaultUploadManagerModal.js +247 -0
  62. package/src/react/components/upload/defaultUploadHelpers.js +108 -0
  63. package/src/react/components/upload/defaultUploadLibrary.js +248 -0
  64. package/src/react/components/upload/fileUpload.js +93 -0
  65. package/src/react/hooks/usePostalCodeLookup.js +145 -0
  66. package/src/react/index.js +6 -0
  67. package/src/react/services/addressFormUtils.js +196 -0
  68. package/src/react/services/addressGeo.js +227 -0
  69. package/src/react/utils/tableVisibleColumnsPreferences.js +29 -12
  70. package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
  71. package/src/tests/react/components/files/DefaultFile.test.js +6 -6
  72. package/src/tests/react/components/form/DefaultForm.test.js +123 -0
  73. package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
  74. package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
  75. package/src/tests/react/components/table/DefaultTable.test.js +53 -2
  76. package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
  77. package/src/tests/react/components/table/resolveRowBackgroundColor.test.mjs +45 -0
  78. package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +161 -0
  79. package/src/tests/react/components/upload/DefaultUpload.test.js +353 -0
  80. package/src/tests/react/components/upload/defaultUploadLibrary.test.js +82 -0
  81. package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
  82. package/src/tests/react/hooks/usePostalCodeLookup.test.js +40 -0
  83. package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
  84. package/src/tests/react/services/addressFormUtils.test.js +176 -0
  85. package/src/tests/react/services/addressGeoLookup.async.test.js +104 -0
  86. package/src/tests/react/services/addressGeoLookup.test.js +105 -0
  87. package/src/tests/react/services/smoke-637/form-cep-12941040.png +0 -0
  88. package/src/tests/react/services/smoke-637/index.html +132 -0
  89. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +84 -48
@@ -1,5 +1,5 @@
1
- const React = require('react');
2
- const renderer = require('react-test-renderer');
1
+ const React = require('react');
2
+ const renderer = require('react-test-renderer');
3
3
  const {jest} = require('@jest/globals');
4
4
 
5
5
  const {beforeEach, describe, expect, it} = global;
@@ -14,37 +14,37 @@ jest.mock('@store', () => ({
14
14
  }));
15
15
 
16
16
  jest.mock('react-native', () => {
17
- const React = require('react');
18
- const createComponent = name => props =>
19
- React.createElement(name, props, props.children);
20
-
21
- return {
22
- Modal: createComponent('Modal'),
23
- ScrollView: createComponent('ScrollView'),
24
- StyleSheet: {create: value => value},
25
- Text: createComponent('Text'),
26
- TextInput: createComponent('TextInput'),
27
- TouchableOpacity: createComponent('TouchableOpacity'),
28
- View: createComponent('View'),
29
- };
30
- });
31
-
32
- jest.mock('react-native-vector-icons/Feather', () => {
33
- const React = require('react');
34
- return props => React.createElement('Icon', props, props.children);
35
- });
36
-
17
+ const React = require('react');
18
+ const createComponent = name => props =>
19
+ React.createElement(name, props, props.children);
20
+
21
+ return {
22
+ Modal: createComponent('Modal'),
23
+ ScrollView: createComponent('ScrollView'),
24
+ StyleSheet: {create: value => value},
25
+ Text: createComponent('Text'),
26
+ TextInput: createComponent('TextInput'),
27
+ TouchableOpacity: createComponent('TouchableOpacity'),
28
+ View: createComponent('View'),
29
+ };
30
+ });
31
+
32
+ jest.mock('react-native-vector-icons/Feather', () => {
33
+ const React = require('react');
34
+ return props => React.createElement('Icon', props, props.children);
35
+ });
36
+
37
37
  const DefaultSelect = require('../../../../react/components/inputs/DefaultSelect').default;
38
-
39
- const createDeferred = () => {
40
- let resolve;
41
- const promise = new Promise(nextResolve => {
42
- resolve = nextResolve;
43
- });
44
-
45
- return {promise, resolve};
46
- };
47
-
38
+
39
+ const createDeferred = () => {
40
+ let resolve;
41
+ const promise = new Promise(nextResolve => {
42
+ resolve = nextResolve;
43
+ });
44
+
45
+ return {promise, resolve};
46
+ };
47
+
48
48
  describe('DefaultSelect', () => {
49
49
  beforeEach(() => {
50
50
  mockStores = {
@@ -64,53 +64,53 @@ describe('DefaultSelect', () => {
64
64
  });
65
65
 
66
66
  it('waits for onBeforeOpen before showing the options modal', async () => {
67
- const gate = createDeferred();
68
- const onBeforeOpen = jest.fn(() => gate.promise);
69
- const onStartEditing = jest.fn();
70
- let tree;
71
- let openPromise;
72
-
73
- await renderer.act(async () => {
74
- tree = renderer.create(
75
- React.createElement(DefaultSelect, {
76
- column: {
77
- key: 'jobTitle',
78
- label: 'Cargo',
79
- list: 'categories/getItems',
80
- },
81
- editing: false,
82
- getOptionsForColumn: () => [
83
- {
84
- key: '1',
85
- label: 'Analista de RH',
86
- },
87
- ],
88
- onBeforeOpen,
89
- onStartEditing,
90
- row: {},
91
- storeName: 'employee_profiles',
92
- value: '1',
93
- }),
94
- );
95
- });
96
-
97
- const readButton = tree.root.findAllByType('TouchableOpacity')[0];
98
- const modal = tree.root.findByType('Modal');
99
-
100
- expect(modal.props.visible).toBe(false);
101
-
102
- renderer.act(() => {
103
- openPromise = readButton.props.onPress();
104
- });
105
-
106
- expect(onStartEditing).toHaveBeenCalledTimes(1);
107
- expect(onBeforeOpen).toHaveBeenCalledTimes(1);
108
- expect(modal.props.visible).toBe(false);
109
-
110
- await renderer.act(async () => {
111
- gate.resolve();
112
- await openPromise;
113
- });
67
+ const gate = createDeferred();
68
+ const onBeforeOpen = jest.fn(() => gate.promise);
69
+ const onStartEditing = jest.fn();
70
+ let tree;
71
+ let openPromise;
72
+
73
+ await renderer.act(async () => {
74
+ tree = renderer.create(
75
+ React.createElement(DefaultSelect, {
76
+ column: {
77
+ key: 'jobTitle',
78
+ label: 'Cargo',
79
+ list: 'categories/getItems',
80
+ },
81
+ editing: false,
82
+ getOptionsForColumn: () => [
83
+ {
84
+ key: '1',
85
+ label: 'Analista de RH',
86
+ },
87
+ ],
88
+ onBeforeOpen,
89
+ onStartEditing,
90
+ row: {},
91
+ storeName: 'employee_profiles',
92
+ value: '1',
93
+ }),
94
+ );
95
+ });
96
+
97
+ const readButton = tree.root.findAllByType('TouchableOpacity')[0];
98
+ const modal = tree.root.findByType('Modal');
99
+
100
+ expect(modal.props.visible).toBe(false);
101
+
102
+ renderer.act(() => {
103
+ openPromise = readButton.props.onPress();
104
+ });
105
+
106
+ expect(onStartEditing).toHaveBeenCalledTimes(1);
107
+ expect(onBeforeOpen).toHaveBeenCalledTimes(1);
108
+ expect(modal.props.visible).toBe(false);
109
+
110
+ await renderer.act(async () => {
111
+ gate.resolve();
112
+ await openPromise;
113
+ });
114
114
 
115
115
  expect(modal.props.visible).toBe(true);
116
116
  });
@@ -254,4 +254,33 @@ describe('DefaultSelect', () => {
254
254
  ]),
255
255
  );
256
256
  });
257
+
258
+ it('uses the resolved form label as the modal search placeholder', async () => {
259
+ let tree;
260
+
261
+ await renderer.act(async () => {
262
+ tree = renderer.create(
263
+ React.createElement(DefaultSelect, {
264
+ column: {
265
+ key: 'paymentType',
266
+ label: 'Payment Type.payment Type',
267
+ list: 'walletPaymentType/getItems',
268
+ searchParam: 'paymentType.paymentType',
269
+ },
270
+ editing: true,
271
+ label: 'Tipo de pagamento',
272
+ row: {},
273
+ storeName: 'invoice',
274
+ variant: 'form',
275
+ }),
276
+ );
277
+ });
278
+
279
+ await renderer.act(async () => {
280
+ await tree.root.findAllByType('TouchableOpacity')[0].props.onPress();
281
+ await Promise.resolve();
282
+ });
283
+
284
+ expect(tree.root.findByType('TextInput').props.placeholder).toBe('Tipo de pagamento');
285
+ });
257
286
  });
@@ -0,0 +1,49 @@
1
+ import {
2
+ flattenGroupedTableItems,
3
+ getGroupingColumns,
4
+ groupTableRows,
5
+ toggleGroupSelection,
6
+ toggleSelectedId,
7
+ } from '../../../../src/react/components/table/DefaultTable.grouping';
8
+
9
+ const columns = [
10
+ { name: 'companyName', grouping: true },
11
+ { name: 'addressLabel', grouping: true },
12
+ { name: 'invoiceNumber' },
13
+ ];
14
+
15
+ const rows = [
16
+ { id: 1, companyName: 'A', addressLabel: 'Rua 1', invoiceNumber: 10 },
17
+ { id: 2, companyName: 'A', addressLabel: 'Rua 1', invoiceNumber: 11 },
18
+ { id: 3, companyName: 'B', addressLabel: 'Rua 2', invoiceNumber: 12 },
19
+ ];
20
+
21
+ describe('DefaultTable grouping', () => {
22
+ it('reads boolean grouping from store columns', () => {
23
+ expect(getGroupingColumns(columns).map(column => column.name)).toEqual([
24
+ 'companyName',
25
+ 'addressLabel',
26
+ ]);
27
+ });
28
+
29
+ it('groups rows by grouping columns', () => {
30
+ const groups = groupTableRows(rows, columns);
31
+ expect(groups).toHaveLength(2);
32
+ expect(groups[0].count).toBe(2);
33
+ expect(groups[1].count).toBe(1);
34
+ });
35
+
36
+ it('flattens group headers and rows', () => {
37
+ const items = flattenGroupedTableItems(rows, columns);
38
+ expect(items[0].type).toBe('group');
39
+ expect(items.filter(item => item.type === 'row')).toHaveLength(3);
40
+ });
41
+
42
+ it('toggles row and group selection', () => {
43
+ const one = toggleSelectedId([], rows[0]);
44
+ expect(one).toEqual(['1']);
45
+ const group = toggleGroupSelection(one, [rows[0], rows[1]]);
46
+ expect(group).toEqual(['1', '2']);
47
+ expect(toggleGroupSelection(group, [rows[0], rows[1]])).toEqual([]);
48
+ });
49
+ });
@@ -9,6 +9,7 @@ let consoleErrorSpy = null;
9
9
  let mockStores = {};
10
10
  let mockWindowDimensions = {width: 480, height: 800};
11
11
  let mockCapturedColumnFilterProps = [];
12
+ let mockCapturedDefaultFormProps = [];
12
13
  let mockCapturedDefaultInputProps = [];
13
14
 
14
15
  const createLocalStorageMock = () => {
@@ -28,6 +29,8 @@ const createLocalStorageMock = () => {
28
29
  };
29
30
  };
30
31
 
32
+ global.localStorage = createLocalStorageMock();
33
+
31
34
  jest.mock('@store', () => ({
32
35
  getAllStores: jest.fn(() => ({})),
33
36
  useStore: jest.fn(name => mockStores[name] || {actions: {}, getters: {}}),
@@ -98,9 +101,15 @@ jest.mock('../../../../react/components/filters/DefaultSearch', () => props => {
98
101
  return React.createElement('DefaultSearch', props);
99
102
  });
100
103
 
101
- jest.mock('../../../../react/components/form/DefaultForm', () => () => {
104
+ jest.mock('../../../../react/components/table/DefaultTableImportModal', () => props => {
102
105
  const React = require('react');
103
- return React.createElement('DefaultForm');
106
+ return React.createElement('DefaultTableImportModal', props);
107
+ });
108
+
109
+ jest.mock('../../../../react/components/form/DefaultForm', () => props => {
110
+ const React = require('react');
111
+ mockCapturedDefaultFormProps.push(props);
112
+ return React.createElement('DefaultForm', props);
104
113
  });
105
114
 
106
115
  jest.mock('../../../../react/components/inputs/DefaultInput', () => props => {
@@ -230,6 +239,7 @@ describe('DefaultTable', () => {
230
239
  consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
231
240
  mockWindowDimensions = {width: 480, height: 800};
232
241
  mockCapturedColumnFilterProps = [];
242
+ mockCapturedDefaultFormProps = [];
233
243
  mockCapturedDefaultInputProps = [];
234
244
  getAllStores.mockImplementation(() => ({}));
235
245
  global.localStorage = createLocalStorageMock();
@@ -614,6 +624,47 @@ describe('DefaultTable', () => {
614
624
  expect(onAdd).toHaveBeenCalledTimes(1);
615
625
  });
616
626
 
627
+ it('opens the default create form when add has no custom handler', () => {
628
+ mockWindowDimensions = {width: 430, height: 932};
629
+ const save = jest.fn(() => Promise.resolve({id: 10}));
630
+ mockStores.invoice = {
631
+ actions: {save},
632
+ getters: {
633
+ add: true,
634
+ columns: [{key: 'payer', label: 'Pagador', editable: true}],
635
+ },
636
+ };
637
+ let tree;
638
+
639
+ renderer.act(() => {
640
+ tree = renderer.create(
641
+ React.createElement(DefaultTable, {
642
+ requestParams: {receiver: 21},
643
+ showToolbar: false,
644
+ storeName: 'invoice',
645
+ }),
646
+ );
647
+ });
648
+
649
+ const addButton = tree.root
650
+ .findAllByType('TouchableOpacity')
651
+ .find(node => node.findAllByType('icon').some(icon => icon.props.name === 'plus'));
652
+
653
+ expect(tree.root.findAllByType('Modal').some(node => node.props.visible)).toBe(false);
654
+
655
+ renderer.act(() => addButton.props.onPress());
656
+
657
+ expect(tree.root.findAllByType('Modal').some(node => node.props.visible)).toBe(true);
658
+ expect(mockCapturedDefaultFormProps[mockCapturedDefaultFormProps.length - 1]).toEqual(
659
+ expect.objectContaining({
660
+ actions: expect.objectContaining({save}),
661
+ mode: 'create',
662
+ row: {receiver: 21},
663
+ storeName: 'invoice',
664
+ }),
665
+ );
666
+ });
667
+
617
668
  it('collapses search into an icon on narrow toolbars and opens the search modal', () => {
618
669
  mockWindowDimensions = {width: 375, height: 667};
619
670
  mockStores.orders = {
@@ -0,0 +1,21 @@
1
+ // fluxo: outros | etapa: order-history-page-filtro | https://github.com/ControleOnline/app-community/wiki/Smoke-Test-Flows
2
+ import test from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { readFileSync } from 'node:fs';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const root = join(dirname(fileURLToPath(import.meta.url)), '../../../../../');
9
+ const source = readFileSync(
10
+ join(root, 'src/react/components/table/DefaultTableControls.js'),
11
+ 'utf8',
12
+ );
13
+
14
+ test('filter button opens DefaultFiltersModal in table and cards modes (#811)', () => {
15
+ assert.match(source, /DefaultFiltersModal/);
16
+ assert.match(source, /hasFilterableColumns \? \(/);
17
+ assert.match(source, /testID="default-table-filter-button"/);
18
+ // must not gate the modal only on cards / must not toggle orphan flag
19
+ assert.doesNotMatch(source, /tableFiltersVisible:\s*!tableFiltersVisible/);
20
+ assert.doesNotMatch(source, /effectiveViewMode === 'cards' \? \([\s\S]*DefaultFiltersModal/);
21
+ });
@@ -0,0 +1,45 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { readFileSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const root = join(dirname(fileURLToPath(import.meta.url)), '../../../../../');
8
+ const sourcePath = join(root, 'src/react/components/table/DefaultTableRows.js');
9
+ const source = readFileSync(sourcePath, 'utf8');
10
+
11
+ test('resolveRowBackgroundColor ignores status.color in source', () => {
12
+ assert.doesNotMatch(source, /row\?\.status\?\.color/);
13
+ assert.doesNotMatch(source, /withAlpha\(statusColor/);
14
+ assert.match(source, /listItemEvenRow/);
15
+ assert.match(source, /listItemOddRow/);
16
+ assert.match(source, /export const resolveRowBackgroundColor/);
17
+ });
18
+
19
+ const resolveRowBackgroundColor = ({
20
+ selected,
21
+ index,
22
+ tableOddColor,
23
+ tableEvenColor,
24
+ selectedBackground = '#ECFEFF',
25
+ }) => {
26
+ if (selected) return selectedBackground;
27
+ return index % 2 === 0 ? tableOddColor : tableEvenColor;
28
+ };
29
+
30
+ test('even/odd stripes use listItem tokens, not status', () => {
31
+ const even = '#111111';
32
+ const odd = '#222222';
33
+ assert.equal(
34
+ resolveRowBackgroundColor({ selected: false, index: 0, tableOddColor: odd, tableEvenColor: even }),
35
+ odd,
36
+ );
37
+ assert.equal(
38
+ resolveRowBackgroundColor({ selected: false, index: 1, tableOddColor: odd, tableEvenColor: even }),
39
+ even,
40
+ );
41
+ assert.equal(
42
+ resolveRowBackgroundColor({ selected: true, index: 0, tableOddColor: odd, tableEvenColor: even }),
43
+ '#ECFEFF',
44
+ );
45
+ });
@@ -0,0 +1,161 @@
1
+ const React = require('react');
2
+ const renderer = require('react-test-renderer');
3
+
4
+ const {beforeEach, describe, expect, it} = global;
5
+
6
+ global.IS_REACT_ACT_ENVIRONMENT = true;
7
+
8
+ const mockResolveStoredTableFiltersPreference = jest.fn(() => null);
9
+ const mockSanitizeTableFiltersPreference = jest.fn((args) => {
10
+ if (args && args.filters) return args.filters;
11
+ return args || {};
12
+ });
13
+
14
+ jest.mock('../../../../react/utils/tableVisibleColumnsPreferences', () => ({
15
+ resolveStoredTableFiltersPreference: (...args) =>
16
+ mockResolveStoredTableFiltersPreference(...args),
17
+ resolveStoredVisibleColumnsPreference: jest.fn(() => null),
18
+ sanitizeTableFiltersPreference: (...args) =>
19
+ mockSanitizeTableFiltersPreference(...args),
20
+ sanitizeVisibleColumnsPreference: jest.fn(v => v?.visibleColumns || []),
21
+ }));
22
+
23
+ const {
24
+ areTableFiltersEqual,
25
+ useDefaultTableStoreSync,
26
+ } = require('../../../../react/components/table/useDefaultTableStoreSync');
27
+
28
+ function Harness({data, store, storeFilters, columnsForTable, tablePreferenceScope}) {
29
+ useDefaultTableStoreSync({
30
+ columns: [],
31
+ columnsForTable: columnsForTable || [],
32
+ data,
33
+ defaultTableConfigs: {},
34
+ defaultTableConfigsSignature: 'sig',
35
+ store,
36
+ storeColumnsLength: 0,
37
+ storeFilters: storeFilters || {},
38
+ storeName: 'device_config',
39
+ tablePreferenceScope: tablePreferenceScope || {
40
+ companyKey: 'c1',
41
+ storeKey: 'orders',
42
+ routeKey: 'order-history-page',
43
+ },
44
+ });
45
+ return null;
46
+ }
47
+
48
+ describe('useDefaultTableStoreSync', () => {
49
+ beforeEach(() => {
50
+ mockResolveStoredTableFiltersPreference.mockReset();
51
+ mockResolveStoredTableFiltersPreference.mockReturnValue(null);
52
+ mockSanitizeTableFiltersPreference.mockClear();
53
+ });
54
+
55
+ it('publishes controlled data once per array reference (avoids #185 loop)', () => {
56
+ const setItems = jest.fn();
57
+ const store = {
58
+ actions: {setItems},
59
+ getters: {},
60
+ };
61
+ const data = [{id: 1}];
62
+
63
+ let tree;
64
+ renderer.act(() => {
65
+ tree = renderer.create(React.createElement(Harness, {data, store}));
66
+ });
67
+
68
+ expect(setItems).toHaveBeenCalledTimes(1);
69
+ expect(setItems).toHaveBeenCalledWith(data);
70
+
71
+ // New store identity (zustand commit) with same data reference must NOT re-publish
72
+ const storeAfterCommit = {
73
+ actions: {setItems},
74
+ getters: {items: data},
75
+ };
76
+ renderer.act(() => {
77
+ tree.update(React.createElement(Harness, {data, store: storeAfterCommit}));
78
+ });
79
+ expect(setItems).toHaveBeenCalledTimes(1);
80
+
81
+ // New data reference must publish again
82
+ const data2 = [{id: 2}];
83
+ renderer.act(() => {
84
+ tree.update(
85
+ React.createElement(Harness, {data: data2, store: storeAfterCommit}),
86
+ );
87
+ });
88
+ expect(setItems).toHaveBeenCalledTimes(2);
89
+ expect(setItems).toHaveBeenLastCalledWith(data2);
90
+
91
+ tree.unmount();
92
+ });
93
+
94
+ it('hydrates stored filters once and does not re-setFilters when storeFilters churns (period toggle #185)', () => {
95
+ const setFilters = jest.fn();
96
+ const setItems = jest.fn();
97
+ const stored = {
98
+ alterDate: {shortcut: 'today', customRange: {from: '', to: ''}},
99
+ };
100
+ mockResolveStoredTableFiltersPreference.mockReturnValue(stored);
101
+ mockSanitizeTableFiltersPreference.mockImplementation(({filters}) => filters || {});
102
+
103
+ const columnsForTable = [{name: 'alterDate', externalFilter: true}];
104
+ const store = {actions: {setFilters, setItems}, getters: {}};
105
+
106
+ let tree;
107
+ renderer.act(() => {
108
+ tree = renderer.create(
109
+ React.createElement(Harness, {
110
+ data: [],
111
+ store,
112
+ storeFilters: {},
113
+ columnsForTable,
114
+ }),
115
+ );
116
+ });
117
+
118
+ expect(setFilters).toHaveBeenCalledTimes(1);
119
+ expect(setFilters).toHaveBeenCalledWith(stored);
120
+
121
+ // Simulate user toggle today → all → today: storeFilters identity changes each time
122
+ const afterAll = {};
123
+ const afterToday = {
124
+ alterDate: {shortcut: 'today', customRange: {from: '', to: ''}},
125
+ };
126
+
127
+ renderer.act(() => {
128
+ tree.update(
129
+ React.createElement(Harness, {
130
+ data: [],
131
+ store: {actions: {setFilters, setItems}, getters: {filters: afterAll}},
132
+ storeFilters: afterAll,
133
+ columnsForTable,
134
+ }),
135
+ );
136
+ });
137
+ renderer.act(() => {
138
+ tree.update(
139
+ React.createElement(Harness, {
140
+ data: [],
141
+ store: {actions: {setFilters, setItems}, getters: {filters: afterToday}},
142
+ storeFilters: afterToday,
143
+ columnsForTable,
144
+ }),
145
+ );
146
+ });
147
+
148
+ // Must remain a single hydrate — no chase of storeFilters
149
+ expect(setFilters).toHaveBeenCalledTimes(1);
150
+
151
+ tree.unmount();
152
+ });
153
+
154
+ it('areTableFiltersEqual treats equivalent period payloads as equal', () => {
155
+ const a = {alterDate: {shortcut: 'today', customRange: {from: '', to: ''}}};
156
+ const b = {alterDate: {shortcut: 'today', customRange: {from: '', to: ''}}};
157
+ expect(areTableFiltersEqual(a, b)).toBe(true);
158
+ expect(areTableFiltersEqual(a, {alterDate: {shortcut: 'all'}})).toBe(false);
159
+ expect(areTableFiltersEqual({}, {})).toBe(true);
160
+ });
161
+ });