@controleonline/ui-default 1.0.270 → 1.0.273
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.
- package/.github/workflows/integration-source-gate.yml +68 -0
- package/.github/workflows/manager-worker.yml +19 -0
- package/.github/workflows/publish-npm.yml +91 -0
- package/AGENTS.md +46 -50
- package/docs/technical/Contrato-de-colunas.md +67 -0
- package/docs/technical/Default-Components.md +76 -0
- package/docs/technical/DefaultForm.md +80 -0
- package/docs/technical/DefaultInput-e-DefaultSelect.md +73 -0
- package/docs/technical/DefaultTable.md +272 -0
- package/docs/wiki.md +1 -0
- package/package.json +4 -3
- package/scripts/validate-wiki-pointer.cjs +13 -0
- package/src/react/components/address/DefaultAddress.js +446 -0
- package/src/react/components/address/DefaultAddress.styles.js +133 -0
- package/src/react/components/address/DefaultAddressCoordFields.js +59 -0
- package/src/react/components/address/DefaultAddressMapPane.js +55 -0
- package/src/react/components/address/LatLonReadonlyFields.js +51 -0
- package/src/react/components/address/defaultAddressHelpers.js +163 -0
- package/src/react/components/address/defaultAddressStyles.js +148 -0
- package/src/react/components/address/defaultAddressUtils.js +129 -0
- package/src/react/components/extra-data/DefaultExtraData.js +152 -0
- package/src/react/components/extra-data/DefaultExtraData.styles.js +61 -0
- package/src/react/components/files/DefaultFile.js +231 -46
- package/src/react/components/files/DefaultFileColumn.js +123 -0
- package/src/react/components/files/DefaultFileColumn.styles.js +52 -0
- package/src/react/components/files/DefaultFileColumn.utils.js +104 -0
- package/src/react/components/filters/CompactFilterSelector.js +51 -35
- package/src/react/components/filters/DefaultColumnFilter.js +60 -48
- package/src/react/components/filters/DefaultExternalFilters.js +324 -265
- package/src/react/components/filters/filterValue.js +65 -0
- package/src/react/components/form/DefaultForm.js +99 -79
- package/src/react/components/help/DefaultTooltip.js +48 -12
- package/src/react/components/help/DefaultTooltip.styles.js +20 -0
- package/src/react/components/inputs/DefaultColorInput.js +153 -0
- package/src/react/components/inputs/DefaultFeatherIconPicker.js +146 -0
- package/src/react/components/inputs/DefaultFeatherIconPicker.styles.js +113 -0
- package/src/react/components/inputs/DefaultInput.js +296 -141
- package/src/react/components/inputs/DefaultInput.styles.js +26 -0
- package/src/react/components/inputs/DefaultSelect.js +134 -123
- package/src/react/components/inputs/defaultInputUtils.js +262 -213
- package/src/react/components/map/DefaultMap.native.js +73 -73
- package/src/react/components/map/DefaultMap.shared.js +911 -762
- package/src/react/components/map/DefaultMap.web.js +124 -68
- package/src/react/components/table/DefaultColumnMenu.js +22 -3
- package/src/react/components/table/DefaultDebug.js +44 -37
- package/src/react/components/table/DefaultSearchModal.js +9 -1
- package/src/react/components/table/DefaultTable.grouping.js +98 -0
- package/src/react/components/table/DefaultTable.js +217 -111
- package/src/react/components/table/DefaultTable.styles.js +369 -310
- package/src/react/components/table/DefaultTable.utils.js +38 -4
- package/src/react/components/table/DefaultTableBody.js +13 -8
- package/src/react/components/table/DefaultTableCards.js +20 -1
- package/src/react/components/table/DefaultTableControls.js +180 -205
- package/src/react/components/table/DefaultTableImportModal.js +108 -0
- package/src/react/components/table/DefaultTableInput.js +1 -0
- package/src/react/components/table/DefaultTableRowActions.js +1 -1
- package/src/react/components/table/DefaultTableRows.js +357 -325
- package/src/react/components/table/DefaultTableSearch.js +28 -5
- package/src/react/components/table/DefaultTableToolbar.js +92 -3
- package/src/react/components/table/useDefaultTablePagination.js +56 -7
- package/src/react/components/table/useDefaultTableStoreSync.js +164 -0
- package/src/react/components/upload/DefaultUpload.js +406 -0
- package/src/react/components/upload/DefaultUpload.styles.js +153 -0
- package/src/react/components/upload/DefaultUploadAttachmentsList.js +146 -0
- package/src/react/components/upload/DefaultUploadManagerModal.js +247 -0
- package/src/react/components/upload/defaultUploadHelpers.js +108 -0
- package/src/react/components/upload/defaultUploadLibrary.js +248 -0
- package/src/react/components/upload/fileUpload.js +93 -0
- package/src/react/hooks/usePostalCodeLookup.js +159 -0
- package/src/react/index.js +6 -0
- package/src/react/services/addressFormUtils.js +262 -0
- package/src/react/services/addressGeo.js +230 -0
- package/src/react/utils/tableVisibleColumnsPreferences.js +113 -15
- package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
- package/src/tests/react/components/files/DefaultFile.test.js +22 -6
- package/src/tests/react/components/files/DefaultFileColumn.company.test.js +24 -0
- package/src/tests/react/components/form/DefaultForm.test.js +123 -0
- package/src/tests/react/components/help/DefaultTooltip.test.js +93 -0
- package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
- package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
- package/src/tests/react/components/table/DefaultTable.test.js +53 -2
- package/src/tests/react/components/table/DefaultTable.utils.test.js +10 -0
- package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
- package/src/tests/react/components/table/DefaultTableRowActions.test.js +23 -0
- package/src/tests/react/components/table/DefaultTableRows.rowActions.test.js +152 -0
- package/src/tests/react/components/table/resolveRowBackgroundColor.test.mjs +45 -0
- package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +161 -0
- package/src/tests/react/components/upload/DefaultUpload.test.js +353 -0
- package/src/tests/react/components/upload/defaultUploadLibrary.test.js +104 -0
- package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
- package/src/tests/react/filters/filterValue.test.js +46 -0
- package/src/tests/react/hooks/usePostalCodeLookup.test.js +107 -0
- package/src/tests/react/inputs/defaultInputUtils.test.js +9 -1
- package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
- package/src/tests/react/services/addressFormUtils.test.js +179 -0
- package/src/tests/react/services/addressGeoLookup.async.test.js +104 -0
- package/src/tests/react/services/addressGeoLookup.test.js +105 -0
- package/src/tests/react/services/smoke-637/form-cep-12941040.png +0 -0
- package/src/tests/react/services/smoke-637/index.html +132 -0
- package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +96 -49
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
const mockReact = require('react');
|
|
2
|
+
const renderer = require('react-test-renderer');
|
|
3
|
+
|
|
4
|
+
const {describe, expect, it, beforeEach} = global;
|
|
5
|
+
|
|
6
|
+
global.IS_REACT_ACT_ENVIRONMENT = true;
|
|
7
|
+
|
|
8
|
+
const mockFileActions = {
|
|
9
|
+
getItems: jest.fn(),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const mockRelationActions = {
|
|
13
|
+
save: jest.fn(),
|
|
14
|
+
remove: jest.fn(),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
jest.mock('@env', () => ({
|
|
18
|
+
env: {
|
|
19
|
+
API_ENTRYPOINT: 'https://api.controleonline.com',
|
|
20
|
+
DOMAIN: 'manager.controleonline.com',
|
|
21
|
+
},
|
|
22
|
+
}), {virtual: true});
|
|
23
|
+
|
|
24
|
+
jest.mock('@store', () => ({
|
|
25
|
+
useStore: jest.fn(name => {
|
|
26
|
+
if (name === 'file') {
|
|
27
|
+
return {
|
|
28
|
+
actions: mockFileActions,
|
|
29
|
+
getters: {},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (name === 'product_file' || name === 'category_file') {
|
|
34
|
+
return {
|
|
35
|
+
actions: mockRelationActions,
|
|
36
|
+
getters: {},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
actions: {},
|
|
42
|
+
getters: {},
|
|
43
|
+
};
|
|
44
|
+
}),
|
|
45
|
+
}), {virtual: true});
|
|
46
|
+
|
|
47
|
+
jest.mock(
|
|
48
|
+
'@expo/vector-icons',
|
|
49
|
+
() => ({
|
|
50
|
+
MaterialCommunityIcons: props =>
|
|
51
|
+
mockReact.createElement('icon', props, props.children),
|
|
52
|
+
}),
|
|
53
|
+
{virtual: true},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
jest.mock('react-native-vector-icons/Feather', () => props =>
|
|
57
|
+
mockReact.createElement('feathericon', props, props.children),
|
|
58
|
+
{virtual: true},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
jest.mock('@controleonline/ui-common/src/react/components/AnimatedModal', () => {
|
|
62
|
+
return props => (props.visible ? mockReact.createElement('modal', props, props.children) : null);
|
|
63
|
+
}, {virtual: true});
|
|
64
|
+
|
|
65
|
+
jest.mock('@controleonline/ui-default/src/react/components/files/DefaultFile', () => {
|
|
66
|
+
return props => mockReact.createElement('defaultfile', props, props.children);
|
|
67
|
+
}, {virtual: true});
|
|
68
|
+
|
|
69
|
+
jest.mock('../../../../react/components/upload/fileUpload', () => ({
|
|
70
|
+
selectFile: jest.fn(),
|
|
71
|
+
uploadFileToApi: jest.fn(),
|
|
72
|
+
toFileIri: jest.fn(),
|
|
73
|
+
extractFileId: value => value?.id || value,
|
|
74
|
+
}), {virtual: true});
|
|
75
|
+
|
|
76
|
+
jest.mock('react-native', () => ({
|
|
77
|
+
ActivityIndicator: props => mockReact.createElement('activityindicator', props, props.children),
|
|
78
|
+
Platform: {OS: 'web'},
|
|
79
|
+
ScrollView: props => mockReact.createElement('scrollview', props, props.children),
|
|
80
|
+
Text: props => mockReact.createElement('text', props, props.children),
|
|
81
|
+
TextInput: props => mockReact.createElement('textinput', props, props.children),
|
|
82
|
+
TouchableOpacity: props => mockReact.createElement('touchableopacity', props, props.children),
|
|
83
|
+
View: props => mockReact.createElement('view', props, props.children),
|
|
84
|
+
}), {virtual: true});
|
|
85
|
+
|
|
86
|
+
const {selectFile, uploadFileToApi, toFileIri} = require('../../../../react/components/upload/fileUpload');
|
|
87
|
+
const DefaultUpload = require('../../../../react/components/upload/DefaultUpload').default;
|
|
88
|
+
|
|
89
|
+
const flush = () => new Promise(resolve => setImmediate(resolve));
|
|
90
|
+
|
|
91
|
+
const collectText = node =>
|
|
92
|
+
node
|
|
93
|
+
.findAllByType('text')
|
|
94
|
+
.map(textNode => {
|
|
95
|
+
const children = textNode.props.children;
|
|
96
|
+
if (Array.isArray(children)) {
|
|
97
|
+
return children.flat(Infinity).join('');
|
|
98
|
+
}
|
|
99
|
+
return children || '';
|
|
100
|
+
})
|
|
101
|
+
.join('');
|
|
102
|
+
|
|
103
|
+
const findButtonByLabel = (root, label) =>
|
|
104
|
+
root.findAllByType('touchableopacity').find(button => collectText(button).includes(label));
|
|
105
|
+
|
|
106
|
+
const findButtonByAccessibilityLabel = (root, label) =>
|
|
107
|
+
root.findAllByType('touchableopacity').find(button => button.props.accessibilityLabel === label);
|
|
108
|
+
|
|
109
|
+
describe('DefaultUpload', () => {
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
jest.clearAllMocks();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('abre a biblioteca, carrega arquivos e envia um arquivo pelo helper centralizado', async () => {
|
|
115
|
+
mockFileActions.getItems.mockResolvedValue({
|
|
116
|
+
member: [
|
|
117
|
+
{id: 10, fileName: 'Biblioteca 1', context: 'products', fileType: 'image'},
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
mockRelationActions.save.mockResolvedValue({id: 42});
|
|
121
|
+
selectFile.mockResolvedValue({
|
|
122
|
+
name: 'nova-imagem.png',
|
|
123
|
+
uri: 'file:///nova-imagem.png',
|
|
124
|
+
mimeType: 'image/png',
|
|
125
|
+
});
|
|
126
|
+
uploadFileToApi.mockResolvedValue({id: 77, fileName: 'nova-imagem.png'});
|
|
127
|
+
toFileIri.mockReturnValue('/files/77');
|
|
128
|
+
|
|
129
|
+
let tree;
|
|
130
|
+
|
|
131
|
+
await renderer.act(async () => {
|
|
132
|
+
tree = renderer.create(
|
|
133
|
+
mockReact.createElement(DefaultUpload, {
|
|
134
|
+
relationStoreName: 'product_file',
|
|
135
|
+
relationField: 'product',
|
|
136
|
+
relationResource: 'products',
|
|
137
|
+
entityId: 12,
|
|
138
|
+
companyId: 3,
|
|
139
|
+
attachments: [{id: 1, file: {id: 1, fileName: 'Anexa'}}],
|
|
140
|
+
context: 'products',
|
|
141
|
+
title: 'Imagens anexas',
|
|
142
|
+
triggerLabel: 'Gerenciar imagens',
|
|
143
|
+
managerTitle: 'Gerenciador de imagens',
|
|
144
|
+
searchPlaceholder: 'Buscar imagem',
|
|
145
|
+
uploadButtonLabel: 'Enviar nova',
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await renderer.act(async () => {
|
|
151
|
+
findButtonByLabel(tree.root, 'Gerenciar imagens').props.onPress();
|
|
152
|
+
await flush();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(mockFileActions.getItems).toHaveBeenCalled();
|
|
156
|
+
|
|
157
|
+
const uploadButton = findButtonByLabel(tree.root, 'Enviar nova');
|
|
158
|
+
expect(uploadButton).toBeTruthy();
|
|
159
|
+
|
|
160
|
+
await renderer.act(async () => {
|
|
161
|
+
await uploadButton.props.onPress();
|
|
162
|
+
await flush();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(selectFile).toHaveBeenCalledWith('image/*');
|
|
166
|
+
expect(uploadFileToApi).toHaveBeenCalledWith({
|
|
167
|
+
file: expect.objectContaining({name: 'nova-imagem.png'}),
|
|
168
|
+
context: 'products',
|
|
169
|
+
peopleId: 3,
|
|
170
|
+
entityId: 12,
|
|
171
|
+
});
|
|
172
|
+
expect(mockRelationActions.save).toHaveBeenCalledWith({
|
|
173
|
+
product: '/products/12',
|
|
174
|
+
file: '/files/77',
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('permite que o pai gerencie upload anexado e remocao por contexto customizado', async () => {
|
|
179
|
+
const onChanged = jest.fn();
|
|
180
|
+
const onUploadFile = jest.fn().mockResolvedValue({id: 8, file: {id: 88}});
|
|
181
|
+
const onRemoveAttachment = jest.fn().mockResolvedValue({});
|
|
182
|
+
|
|
183
|
+
mockFileActions.getItems.mockResolvedValue({member: []});
|
|
184
|
+
selectFile.mockResolvedValue({
|
|
185
|
+
name: 'avatar.png',
|
|
186
|
+
type: 'image/png',
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
let tree;
|
|
190
|
+
|
|
191
|
+
await renderer.act(async () => {
|
|
192
|
+
tree = renderer.create(
|
|
193
|
+
mockReact.createElement(DefaultUpload, {
|
|
194
|
+
relationStoreName: 'people',
|
|
195
|
+
relationField: 'people',
|
|
196
|
+
relationResource: 'people',
|
|
197
|
+
entityId: 30,
|
|
198
|
+
companyId: 30,
|
|
199
|
+
attachments: [{file: {id: 91, fileName: 'avatar.png'}}],
|
|
200
|
+
context: 'people_media',
|
|
201
|
+
libraryContexts: ['people_media'],
|
|
202
|
+
title: 'avatar',
|
|
203
|
+
triggerLabel: 'Gerenciar avatar',
|
|
204
|
+
uploadButtonLabel: 'Enviar nova',
|
|
205
|
+
onChanged,
|
|
206
|
+
onUploadFile,
|
|
207
|
+
onRemoveAttachment,
|
|
208
|
+
uploadResultAlreadyAttached: true,
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await renderer.act(async () => {
|
|
214
|
+
findButtonByLabel(tree.root, 'Gerenciar avatar').props.onPress();
|
|
215
|
+
await flush();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
await renderer.act(async () => {
|
|
219
|
+
await findButtonByLabel(tree.root, 'Enviar nova').props.onPress();
|
|
220
|
+
await flush();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(onUploadFile).toHaveBeenCalledWith(
|
|
224
|
+
expect.objectContaining({
|
|
225
|
+
context: 'people_media',
|
|
226
|
+
entityId: 30,
|
|
227
|
+
file: expect.objectContaining({name: 'avatar.png'}),
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
expect(mockRelationActions.save).not.toHaveBeenCalled();
|
|
231
|
+
expect(onChanged).toHaveBeenCalled();
|
|
232
|
+
|
|
233
|
+
const removeButton = findButtonByAccessibilityLabel(tree.root, 'Remover');
|
|
234
|
+
expect(removeButton).toBeTruthy();
|
|
235
|
+
|
|
236
|
+
await renderer.act(async () => {
|
|
237
|
+
await removeButton.props.onPress();
|
|
238
|
+
await flush();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(onRemoveAttachment).toHaveBeenCalledWith(
|
|
242
|
+
expect.objectContaining({file: expect.objectContaining({id: 91})}),
|
|
243
|
+
);
|
|
244
|
+
expect(mockRelationActions.remove).not.toHaveBeenCalled();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('normaliza o identificador da relacao antes da remocao customizada', async () => {
|
|
248
|
+
const onRemoveAttachment = jest.fn().mockResolvedValue({});
|
|
249
|
+
|
|
250
|
+
let tree;
|
|
251
|
+
|
|
252
|
+
await renderer.act(async () => {
|
|
253
|
+
tree = renderer.create(
|
|
254
|
+
mockReact.createElement(DefaultUpload, {
|
|
255
|
+
relationStoreName: 'people',
|
|
256
|
+
relationField: 'people',
|
|
257
|
+
relationResource: 'people',
|
|
258
|
+
entityId: 30,
|
|
259
|
+
companyId: 30,
|
|
260
|
+
attachments: [{mediaId: '/people_media/44', file: {id: 91, fileName: 'avatar.png'}}],
|
|
261
|
+
context: 'people_media',
|
|
262
|
+
title: 'avatar',
|
|
263
|
+
triggerLabel: 'Gerenciar avatar',
|
|
264
|
+
onRemoveAttachment,
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
await renderer.act(async () => {
|
|
270
|
+
await findButtonByAccessibilityLabel(tree.root, 'Remover').props.onPress();
|
|
271
|
+
await flush();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(onRemoveAttachment).toHaveBeenCalledWith(
|
|
275
|
+
expect.objectContaining({
|
|
276
|
+
id: '44',
|
|
277
|
+
mediaId: '/people_media/44',
|
|
278
|
+
file: expect.objectContaining({id: 91}),
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('renderiza apenas o gatilho customizado quando o conteudo inline fica desativado', async () => {
|
|
284
|
+
let tree;
|
|
285
|
+
|
|
286
|
+
await renderer.act(async () => {
|
|
287
|
+
tree = renderer.create(
|
|
288
|
+
mockReact.createElement(DefaultUpload, {
|
|
289
|
+
relationStoreName: 'people',
|
|
290
|
+
relationField: 'people',
|
|
291
|
+
relationResource: 'people',
|
|
292
|
+
entityId: 30,
|
|
293
|
+
companyId: 30,
|
|
294
|
+
attachments: [{file: {id: 91, fileName: 'avatar.png'}}],
|
|
295
|
+
context: 'people_media',
|
|
296
|
+
title: 'avatar',
|
|
297
|
+
showInlineContent: false,
|
|
298
|
+
renderTrigger: ({openManager}) =>
|
|
299
|
+
mockReact.createElement(
|
|
300
|
+
'touchableopacity',
|
|
301
|
+
{onPress: openManager},
|
|
302
|
+
mockReact.createElement('text', null, 'Abrir gerenciador customizado'),
|
|
303
|
+
),
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(collectText(tree.root)).toContain('Abrir gerenciador customizado');
|
|
309
|
+
expect(collectText(tree.root)).not.toContain('avatar.png');
|
|
310
|
+
|
|
311
|
+
await renderer.act(async () => {
|
|
312
|
+
findButtonByLabel(tree.root, 'Abrir gerenciador customizado').props.onPress();
|
|
313
|
+
await flush();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(mockFileActions.getItems).toHaveBeenCalled();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('nao recarrega a biblioteca quando arrays equivalentes sao recriados', async () => {
|
|
320
|
+
mockFileActions.getItems.mockResolvedValue({member: []});
|
|
321
|
+
let tree;
|
|
322
|
+
const renderProps = () => ({
|
|
323
|
+
relationStoreName: 'people',
|
|
324
|
+
relationField: 'people',
|
|
325
|
+
relationResource: 'people',
|
|
326
|
+
entityId: 30,
|
|
327
|
+
companyId: 30,
|
|
328
|
+
context: 'company_certificate',
|
|
329
|
+
libraryContexts: ['company_certificate'],
|
|
330
|
+
knownFileIds: [],
|
|
331
|
+
showInlineContent: false,
|
|
332
|
+
renderTrigger: ({openManager}) =>
|
|
333
|
+
mockReact.createElement('touchableopacity', {onPress: openManager}, 'Abrir'),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
await renderer.act(async () => {
|
|
337
|
+
tree = renderer.create(mockReact.createElement(DefaultUpload, renderProps()));
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await renderer.act(async () => {
|
|
341
|
+
tree.root.findByType('touchableopacity').props.onPress();
|
|
342
|
+
await flush();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
await renderer.act(async () => {
|
|
346
|
+
tree.update(mockReact.createElement(DefaultUpload, renderProps()));
|
|
347
|
+
tree.update(mockReact.createElement(DefaultUpload, renderProps()));
|
|
348
|
+
await flush();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(mockFileActions.getItems).toHaveBeenCalledTimes(1);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** @jest-environment node */
|
|
2
|
+
|
|
3
|
+
/** app-community#433 — library lists all people_media files for the person */
|
|
4
|
+
const {
|
|
5
|
+
fetchLibraryFiles,
|
|
6
|
+
filesFromPeopleMediaRelations,
|
|
7
|
+
normalizeSeedLibraryFiles,
|
|
8
|
+
} = require('../../../../react/components/upload/defaultUploadLibrary');
|
|
9
|
+
|
|
10
|
+
describe('filesFromPeopleMediaRelations', () => {
|
|
11
|
+
it('extracts embedded file objects with id', () => {
|
|
12
|
+
const relations = [
|
|
13
|
+
{id: 1, file: {id: 10, fileName: 'a.png', '@id': '/files/10'}},
|
|
14
|
+
{id: 2, file: {id: 20, fileName: 'b.png'}},
|
|
15
|
+
{id: 3, file: null},
|
|
16
|
+
{id: 4},
|
|
17
|
+
];
|
|
18
|
+
const files = filesFromPeopleMediaRelations(relations);
|
|
19
|
+
expect(files).toHaveLength(2);
|
|
20
|
+
expect(files.map(f => f.id)).toEqual([10, 20]);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('normalizeSeedLibraryFiles', () => {
|
|
25
|
+
it('accepts people_media rows and plain file objects', () => {
|
|
26
|
+
const files = normalizeSeedLibraryFiles([
|
|
27
|
+
{id: 1, file: {id: 10, fileName: 'a.png'}},
|
|
28
|
+
{id: 20, fileName: 'b.png'},
|
|
29
|
+
'/files/30',
|
|
30
|
+
]);
|
|
31
|
+
expect(files.map(f => String(f.id)).sort()).toEqual(['10', '20', '30']);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('fetchLibraryFiles', () => {
|
|
36
|
+
it('returns empty when getItems is missing and no seed', async () => {
|
|
37
|
+
const result = await fetchLibraryFiles({fileActions: {}});
|
|
38
|
+
expect(result).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns seed files when getItems is missing', async () => {
|
|
42
|
+
const result = await fetchLibraryFiles({
|
|
43
|
+
fileActions: {},
|
|
44
|
+
additionalLibraryFiles: [
|
|
45
|
+
{id: 1, file: {id: 10, fileName: 'seed.png'}},
|
|
46
|
+
{id: 11, fileName: 'seed2.png'},
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
expect(result.map(f => String(f.id)).sort()).toEqual(['10', '11']);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('sends itemsPerPage and merges people_media relations + seed', async () => {
|
|
53
|
+
const getItems = jest.fn().mockResolvedValue({
|
|
54
|
+
member: [{id: 10, fileName: 'from-files.png', context: 'people_media'}],
|
|
55
|
+
});
|
|
56
|
+
const getPeopleMedia = jest.fn().mockResolvedValue([
|
|
57
|
+
{id: 1, file: {id: 10, fileName: 'from-files.png'}},
|
|
58
|
+
{id: 2, file: {id: 11, fileName: 'only-relation.png'}},
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const files = await fetchLibraryFiles({
|
|
62
|
+
fileActions: {getItems},
|
|
63
|
+
companyId: 103446,
|
|
64
|
+
fileType: 'image',
|
|
65
|
+
libraryContexts: ['people_media'],
|
|
66
|
+
peopleActions: {getPeopleMedia},
|
|
67
|
+
additionalLibraryFiles: [{id: 3, file: {id: 12, fileName: 'seeded.png'}}],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(getItems).toHaveBeenCalledWith(
|
|
71
|
+
expect.objectContaining({
|
|
72
|
+
context: 'people_media',
|
|
73
|
+
people: '/people/103446',
|
|
74
|
+
itemsPerPage: 500,
|
|
75
|
+
page: 1,
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
expect(getPeopleMedia).toHaveBeenCalledWith(
|
|
79
|
+
expect.objectContaining({
|
|
80
|
+
people: '/people/103446',
|
|
81
|
+
itemsPerPage: 100,
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
const ids = files.map(f => String(f.id)).sort();
|
|
85
|
+
expect(ids).toEqual(['10', '11', '12']);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('still returns /files results if people_media fetch fails', async () => {
|
|
89
|
+
const getItems = jest.fn().mockResolvedValue({
|
|
90
|
+
member: [{id: 5, fileName: 'ok.png'}],
|
|
91
|
+
});
|
|
92
|
+
const getPeopleMedia = jest.fn().mockRejectedValue(new Error('network'));
|
|
93
|
+
|
|
94
|
+
const files = await fetchLibraryFiles({
|
|
95
|
+
fileActions: {getItems},
|
|
96
|
+
companyId: 1,
|
|
97
|
+
libraryContexts: ['people_media'],
|
|
98
|
+
peopleActions: {getPeopleMedia},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(files).toHaveLength(1);
|
|
102
|
+
expect(files[0].id).toBe(5);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
|
|
8
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '../../../../../');
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const lib = require(join(root, 'src/react/components/upload/defaultUploadLibrary.js'));
|
|
11
|
+
|
|
12
|
+
test('people_media path never requires peopleIri to avoid GET /files', async () => {
|
|
13
|
+
const source = readFileSync(
|
|
14
|
+
join(root, 'src/react/components/upload/defaultUploadLibrary.js'),
|
|
15
|
+
'utf8',
|
|
16
|
+
);
|
|
17
|
+
assert.match(source, /if \(includesPeopleMedia\)/);
|
|
18
|
+
assert.doesNotMatch(
|
|
19
|
+
source,
|
|
20
|
+
/if \(includesPeopleMedia && peopleIri\)/,
|
|
21
|
+
);
|
|
22
|
+
// Without peopleIri still returns stubs only
|
|
23
|
+
const files = await lib.fetchLibraryFiles({
|
|
24
|
+
fileActions: {
|
|
25
|
+
getItems: async () => {
|
|
26
|
+
throw new Error('should not call getItems');
|
|
27
|
+
},
|
|
28
|
+
get: async () => {
|
|
29
|
+
throw new Error('should not call get');
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
companyId: null,
|
|
33
|
+
fileType: 'image',
|
|
34
|
+
libraryContexts: ['people_media'],
|
|
35
|
+
peopleActions: null,
|
|
36
|
+
knownFileIds: [8469],
|
|
37
|
+
});
|
|
38
|
+
assert.equal(files.length, 1);
|
|
39
|
+
assert.equal(files[0].id, 8469);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('inline attachments list can hide star/trash', () => {
|
|
43
|
+
const source = readFileSync(
|
|
44
|
+
join(root, 'src/react/components/upload/DefaultUploadAttachmentsList.js'),
|
|
45
|
+
'utf8',
|
|
46
|
+
);
|
|
47
|
+
assert.match(source, /showAttachmentActions/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('manager shows remove for attached library items', () => {
|
|
51
|
+
const source = readFileSync(
|
|
52
|
+
join(root, 'src/react/components/upload/DefaultUploadManagerModal.js'),
|
|
53
|
+
'utf8',
|
|
54
|
+
);
|
|
55
|
+
assert.match(source, /trash-can-outline/);
|
|
56
|
+
assert.match(source, /handleRemove/);
|
|
57
|
+
});
|