@axinom/mosaic-ui 0.34.0-rc.1 → 0.34.0-rc.11
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/dist/components/Explorer/Explorer.d.ts.map +1 -1
- package/dist/components/Filters/Filter/Filter.d.ts +2 -1
- package/dist/components/Filters/Filter/Filter.d.ts.map +1 -1
- package/dist/components/Filters/Filters.d.ts.map +1 -1
- package/dist/components/Filters/Filters.model.d.ts +2 -0
- package/dist/components/Filters/Filters.model.d.ts.map +1 -1
- package/dist/components/FormElements/FileUploadControl/FileUploadControl.d.ts.map +1 -1
- package/dist/components/FormElements/SingleLineText/SingleLineText.d.ts.map +1 -1
- package/dist/components/List/List.d.ts.map +1 -1
- package/dist/components/List/List.model.d.ts +2 -0
- package/dist/components/List/List.model.d.ts.map +1 -1
- package/dist/components/List/ListHeader/ListHeader.d.ts +7 -1
- package/dist/components/List/ListHeader/ListHeader.d.ts.map +1 -1
- package/dist/components/List/ListHeader/useResize.d.ts +18 -0
- package/dist/components/List/ListHeader/useResize.d.ts.map +1 -0
- package/dist/components/List/ListRow/ListRow.d.ts.map +1 -1
- package/dist/components/List/ListRow/ListRowLoader.d.ts +2 -2
- package/dist/components/List/ListRow/ListRowLoader.d.ts.map +1 -1
- package/dist/components/List/ListRow/Renderers/BooleanDotRenderer/BooleanDotRenderer.d.ts.map +1 -1
- package/dist/components/List/useColumnsSize.d.ts +21 -0
- package/dist/components/List/useColumnsSize.d.ts.map +1 -0
- package/dist/index.es.js +3 -3
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/DynamicDataList/DynamicListDataEntry/Renderers/createInputRenderer/createInputRenderer.spec.tsx +2 -2
- package/src/components/Explorer/Explorer.stories.tsx +16 -0
- package/src/components/Explorer/Explorer.tsx +33 -31
- package/src/components/Filters/Filter/Filter.spec.tsx +24 -1
- package/src/components/Filters/Filter/Filter.tsx +6 -0
- package/src/components/Filters/Filters.model.ts +3 -0
- package/src/components/Filters/Filters.stories.tsx +9 -0
- package/src/components/Filters/Filters.tsx +1 -0
- package/src/components/FormElements/FileUploadControl/FileUploadControl.spec.tsx +35 -0
- package/src/components/FormElements/FileUploadControl/FileUploadControl.tsx +2 -0
- package/src/components/FormElements/FormElementContainer/FormElementContainer.scss +2 -0
- package/src/components/FormElements/SingleLineText/SingleLineText.spec.tsx +5 -4
- package/src/components/FormElements/SingleLineText/SingleLineText.tsx +6 -1
- package/src/components/List/List.model.ts +3 -0
- package/src/components/List/List.scss +0 -2
- package/src/components/List/List.stories.tsx +1 -1
- package/src/components/List/List.tsx +17 -55
- package/src/components/List/ListHeader/ListHeader.scss +23 -10
- package/src/components/List/ListHeader/ListHeader.spec.tsx +56 -0
- package/src/components/List/ListHeader/ListHeader.tsx +43 -9
- package/src/components/List/ListHeader/useResize.ts +108 -0
- package/src/components/List/ListRow/ListRow.scss +8 -12
- package/src/components/List/ListRow/ListRow.spec.tsx +5 -21
- package/src/components/List/ListRow/ListRow.tsx +16 -32
- package/src/components/List/ListRow/ListRowLoader.tsx +14 -4
- package/src/components/List/ListRow/Renderers/BooleanDotRenderer/BooleanDotRenderer.scss +10 -8
- package/src/components/List/ListRow/Renderers/BooleanDotRenderer/BooleanDotRenderer.tsx +3 -1
- package/src/components/List/useColumnsSize.ts +120 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axinom/mosaic-ui",
|
|
3
|
-
"version": "0.34.0-rc.
|
|
3
|
+
"version": "0.34.0-rc.11",
|
|
4
4
|
"description": "UI components for building Axinom Mosaic applications",
|
|
5
5
|
"author": "Axinom",
|
|
6
6
|
"license": "PROPRIETARY",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"build-storybook": "storybook build"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@axinom/mosaic-core": "^0.4.7-rc.
|
|
35
|
+
"@axinom/mosaic-core": "^0.4.7-rc.11",
|
|
36
36
|
"@faker-js/faker": "^7.4.0",
|
|
37
37
|
"@popperjs/core": "^2.9.2",
|
|
38
38
|
"clsx": "^1.1.0",
|
|
@@ -102,5 +102,5 @@
|
|
|
102
102
|
"publishConfig": {
|
|
103
103
|
"access": "public"
|
|
104
104
|
},
|
|
105
|
-
"gitHead": "
|
|
105
|
+
"gitHead": "a0c164baba1be782cb9f7012c088f51fa8f5ed7b"
|
|
106
106
|
}
|
|
@@ -49,7 +49,7 @@ describe('createInputRenderer', () => {
|
|
|
49
49
|
|
|
50
50
|
const input = wrapper.find('input');
|
|
51
51
|
|
|
52
|
-
expect(input.
|
|
52
|
+
expect(input.getDOMNode<HTMLInputElement>().value).toBe(mockValue);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
it(`emits 'onValueChange' with the new value when 'input' value has changed`, () => {
|
|
@@ -59,7 +59,7 @@ describe('createInputRenderer', () => {
|
|
|
59
59
|
|
|
60
60
|
const input = wrapper.find('input');
|
|
61
61
|
|
|
62
|
-
expect(input.
|
|
62
|
+
expect(input.getDOMNode<HTMLInputElement>().value).toBe('');
|
|
63
63
|
|
|
64
64
|
act(() => {
|
|
65
65
|
input.simulate('change', { target: { value: mockValueUpdated } });
|
|
@@ -238,6 +238,22 @@ initializeUi({
|
|
|
238
238
|
action('showNotification')(args);
|
|
239
239
|
return faker.random.numeric();
|
|
240
240
|
},
|
|
241
|
+
addIndicator: (args) => {
|
|
242
|
+
action('addIndicator')(args);
|
|
243
|
+
return Math.random();
|
|
244
|
+
},
|
|
245
|
+
removeIndicator: (args) => {
|
|
246
|
+
action('removeIndicator')(args);
|
|
247
|
+
},
|
|
248
|
+
showSaveIndicator: () => {
|
|
249
|
+
action('showSaveIndicator')();
|
|
250
|
+
},
|
|
251
|
+
hideSaveIndicator: () => {
|
|
252
|
+
action('hideSaveIndicator')();
|
|
253
|
+
},
|
|
254
|
+
on: (event, callback) => {
|
|
255
|
+
action('on')(event, callback);
|
|
256
|
+
},
|
|
241
257
|
});
|
|
242
258
|
|
|
243
259
|
export const ActionErrors: StoryObj<ExplorerStoryType> = {
|
|
@@ -433,37 +433,39 @@ export const Explorer = React.forwardRef(function Explorer<T extends Data>(
|
|
|
433
433
|
setActiveFilters(args);
|
|
434
434
|
}}
|
|
435
435
|
/>
|
|
436
|
-
<
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
onSortChanged(sortData)
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
436
|
+
<div>
|
|
437
|
+
<List<T>
|
|
438
|
+
columns={columns}
|
|
439
|
+
data={data}
|
|
440
|
+
isLoading={isLoading}
|
|
441
|
+
isError={Boolean(stationMessage?.type === 'error')}
|
|
442
|
+
handleRetry={false}
|
|
443
|
+
minimumWidth={minimumWidth}
|
|
444
|
+
columnGap={columnGap}
|
|
445
|
+
rowGap={rowGap}
|
|
446
|
+
headerRowHeight={headerRowHeight}
|
|
447
|
+
listRowHeight={listRowHeight}
|
|
448
|
+
listRowActionSize={listRowActionSize}
|
|
449
|
+
headerRowActionSize={headerRowActionSize}
|
|
450
|
+
horizontalTextAlign={horizontalTextAlign}
|
|
451
|
+
verticalTextAlign={verticalTextAlign}
|
|
452
|
+
keyProperty={keyProperty}
|
|
453
|
+
showActionButton={Boolean(generateItemLink) || Boolean(onItemClicked)} // or hard code to `true`?
|
|
454
|
+
selectionMode={mode}
|
|
455
|
+
enableSelectAll={enableSelectAll}
|
|
456
|
+
loadingTriggerOffset={loadingTriggerOffset}
|
|
457
|
+
defaultSortOrder={sortOrder}
|
|
458
|
+
generateItemLink={generateItemLink}
|
|
459
|
+
onItemClicked={onItemClickedHandler}
|
|
460
|
+
onItemSelected={itemSelectedHandler}
|
|
461
|
+
onRequestMoreData={onRequestMoreData}
|
|
462
|
+
onSortChanged={(sortData: SortData<T>) => {
|
|
463
|
+
onSortChanged(sortData);
|
|
464
|
+
setSortOrder(sortData);
|
|
465
|
+
}}
|
|
466
|
+
inlineMenuActions={inlineMenuActions && inlineMenuActionsHandler}
|
|
467
|
+
/>
|
|
468
|
+
</div>
|
|
467
469
|
</div>
|
|
468
470
|
);
|
|
469
471
|
});
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { mount, shallow } from 'enzyme';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Button } from '../../Buttons';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
CustomFilterProps,
|
|
6
|
+
FilterType,
|
|
7
|
+
FilterTypes,
|
|
8
|
+
SelectedValueRenderer,
|
|
9
|
+
} from '../Filters.model';
|
|
5
10
|
import { FreeTextFilter } from '../SelectionTypes/FreeTextFilter/FreeTextFilter';
|
|
6
11
|
import { OptionsFilter } from '../SelectionTypes/OptionsFilter/OptionsFilter';
|
|
7
12
|
import { Filter, FilterProps } from './Filter';
|
|
@@ -148,6 +153,24 @@ describe('Filter', () => {
|
|
|
148
153
|
expect(wrapper.find('.selectedValue').exists()).toBe(false);
|
|
149
154
|
});
|
|
150
155
|
|
|
156
|
+
it(`'selectedValueRenderer' value is displayed when the prop is set`, () => {
|
|
157
|
+
const renderedValue = `${sampleText} renderer`;
|
|
158
|
+
const selectedValueRenderer: SelectedValueRenderer = (value) => {
|
|
159
|
+
return `${value} renderer`;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const wrapper = mount(
|
|
163
|
+
<Filter {...defaultProps} options={freeTextFilter} value={sampleText} />,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(wrapper.find('.selectedValue').text()).toEqual(sampleText);
|
|
167
|
+
|
|
168
|
+
wrapper.setProps({ selectedValueRenderer });
|
|
169
|
+
wrapper.update();
|
|
170
|
+
|
|
171
|
+
expect(wrapper.find('.selectedValue').text()).toEqual(renderedValue);
|
|
172
|
+
});
|
|
173
|
+
|
|
151
174
|
it('Raises onFilterChange with prop name and new undefined value when filter is closed', () => {
|
|
152
175
|
const mockValue = 'test-value';
|
|
153
176
|
const wrapper = mount(
|
|
@@ -26,6 +26,7 @@ export interface FilterProps<T extends Data> {
|
|
|
26
26
|
index?: number;
|
|
27
27
|
isActive: boolean;
|
|
28
28
|
className?: string;
|
|
29
|
+
selectedValueRenderer?: (value: FilterValue) => string;
|
|
29
30
|
onFilterChange: (prop: keyof T, value: FilterValue, index: number) => void;
|
|
30
31
|
onValidate?: (currentValue: FilterValue) => string | null | undefined;
|
|
31
32
|
onFilterClicked: () => void;
|
|
@@ -37,6 +38,7 @@ export const Filter = <T extends Data>({
|
|
|
37
38
|
index = -1,
|
|
38
39
|
isActive,
|
|
39
40
|
className = '',
|
|
41
|
+
selectedValueRenderer,
|
|
40
42
|
onFilterChange,
|
|
41
43
|
onFilterClicked,
|
|
42
44
|
onValidate,
|
|
@@ -72,6 +74,10 @@ export const Filter = <T extends Data>({
|
|
|
72
74
|
};
|
|
73
75
|
|
|
74
76
|
const renderValue = (value: unknown): ReactNode => {
|
|
77
|
+
if (selectedValueRenderer !== undefined) {
|
|
78
|
+
return selectedValueRenderer(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
switch (options.type) {
|
|
76
82
|
case FilterTypes.Date:
|
|
77
83
|
return formatDate(String(value));
|
|
@@ -15,6 +15,7 @@ export interface FilterConfig<T extends Data> {
|
|
|
15
15
|
label: string;
|
|
16
16
|
property: keyof T;
|
|
17
17
|
type: FilterTypes;
|
|
18
|
+
selectedValueRenderer?: SelectedValueRenderer;
|
|
18
19
|
onValidate?: FilterValidatorFunction<T>;
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -105,3 +106,5 @@ export type FilterValidatorFunction<T> = (
|
|
|
105
106
|
value: FilterValue,
|
|
106
107
|
allValues: FilterValues<T>,
|
|
107
108
|
) => FilterValidationResult;
|
|
109
|
+
|
|
110
|
+
export type SelectedValueRenderer = (value: FilterValue) => string;
|
|
@@ -153,6 +153,15 @@ const customFilter: FilterType<{ custom: string }> = {
|
|
|
153
153
|
label: 'Custom Filter (multi select)',
|
|
154
154
|
property: 'custom',
|
|
155
155
|
type: FilterTypes.Custom,
|
|
156
|
+
selectedValueRenderer: (value: unknown) => {
|
|
157
|
+
const items = value as string[];
|
|
158
|
+
|
|
159
|
+
return String(
|
|
160
|
+
`${items.length} item${items.length === 1 ? '' : 's'}: ${items
|
|
161
|
+
.join(', ')
|
|
162
|
+
.replace(/([a-zA-Z])(\d)/g, '$1 $2')}`,
|
|
163
|
+
);
|
|
164
|
+
},
|
|
156
165
|
component: CustomFilterComponent,
|
|
157
166
|
};
|
|
158
167
|
|
|
@@ -134,6 +134,7 @@ export const Filters = <T extends Data>({
|
|
|
134
134
|
index={index}
|
|
135
135
|
isActive={index === activeFilterIndex}
|
|
136
136
|
onFilterChange={filtersChangedHandler}
|
|
137
|
+
selectedValueRenderer={filter.selectedValueRenderer}
|
|
137
138
|
onValidate={(currentValue) =>
|
|
138
139
|
filter.onValidate &&
|
|
139
140
|
filter.onValidate(currentValue, activeFilters.current)
|
|
@@ -203,6 +203,41 @@ describe('FileUploadControl', () => {
|
|
|
203
203
|
);
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
+
it('allows files with the same name to be uploaded multiple times', () => {
|
|
207
|
+
const spy = jest.fn();
|
|
208
|
+
|
|
209
|
+
const wrapper = mount(
|
|
210
|
+
<FileUploadControl name={'test-name'} onFileSelected={spy} />,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const fileInput = wrapper.find('input[type="file"]');
|
|
214
|
+
|
|
215
|
+
// Simulate first file upload
|
|
216
|
+
fileInput.simulate('change', { target: { files: mockFileList } });
|
|
217
|
+
|
|
218
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
219
|
+
expect(spy).toHaveBeenCalledWith({
|
|
220
|
+
file: mockFile,
|
|
221
|
+
uploadCompleted: expect.any(Function),
|
|
222
|
+
uploadProgress: expect.any(Function),
|
|
223
|
+
uploadStarted: expect.any(Function),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Reset spy
|
|
227
|
+
spy.mockReset();
|
|
228
|
+
|
|
229
|
+
// Simulate second file upload with the same file
|
|
230
|
+
fileInput.simulate('change', { target: { files: mockFileList } });
|
|
231
|
+
|
|
232
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
233
|
+
expect(spy).toHaveBeenCalledWith({
|
|
234
|
+
file: mockFile,
|
|
235
|
+
uploadCompleted: expect.any(Function),
|
|
236
|
+
uploadProgress: expect.any(Function),
|
|
237
|
+
uploadStarted: expect.any(Function),
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
206
241
|
describe('MIME Types', () => {
|
|
207
242
|
const mockTypes = 'image/jpeg,image/png';
|
|
208
243
|
const mockFile = new File([new ArrayBuffer(1)], 'image.png', {
|
|
@@ -169,6 +169,8 @@ export const FileUploadControl: React.FC<FileUploadProps> = ({
|
|
|
169
169
|
onChange={(event) => {
|
|
170
170
|
const file = event.target.files?.item(0);
|
|
171
171
|
file && fileSelected(file);
|
|
172
|
+
// Reset the value of the input field to ensure onChange fires even with same file
|
|
173
|
+
event.target.value = '';
|
|
172
174
|
}}
|
|
173
175
|
/>
|
|
174
176
|
</>
|
|
@@ -24,18 +24,20 @@ describe('SingleLineText', () => {
|
|
|
24
24
|
const spy = jest.fn();
|
|
25
25
|
const mockValue = 'test-value';
|
|
26
26
|
const mockValueUpdated = 'updated-test-value';
|
|
27
|
-
const wrapper =
|
|
27
|
+
const wrapper = mount(
|
|
28
28
|
<SingleLineText name={'test-name'} value={mockValue} onChange={spy} />,
|
|
29
29
|
);
|
|
30
30
|
|
|
31
31
|
const input = wrapper.find('input');
|
|
32
32
|
|
|
33
|
-
expect(input.
|
|
33
|
+
expect(input.getDOMNode<HTMLInputElement>().value).toEqual(mockValue);
|
|
34
34
|
|
|
35
35
|
input.simulate('change', { target: { value: mockValueUpdated } });
|
|
36
36
|
|
|
37
37
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
38
|
-
expect(spy).toHaveBeenCalledWith(
|
|
38
|
+
expect(spy).toHaveBeenCalledWith(
|
|
39
|
+
expect.objectContaining({ target: { value: mockValueUpdated } }),
|
|
40
|
+
);
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
it('uses optional props when passed in', () => {
|
|
@@ -48,7 +50,6 @@ describe('SingleLineText', () => {
|
|
|
48
50
|
name: 'test-name',
|
|
49
51
|
placeholder: 'test-placeholder',
|
|
50
52
|
type: 'number',
|
|
51
|
-
value: '',
|
|
52
53
|
} as Record<string, unknown>;
|
|
53
54
|
|
|
54
55
|
const wrapper = shallow(
|
|
@@ -57,6 +57,12 @@ export const SingleLineText: React.FC<SingleLineTextProps> = ({
|
|
|
57
57
|
const DUMMY_PWD = '0000000000';
|
|
58
58
|
const isPasswordField = type === 'password' ? true : false;
|
|
59
59
|
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (innerRef.current) {
|
|
62
|
+
innerRef.current.value = String(value || '');
|
|
63
|
+
}
|
|
64
|
+
}, [innerRef, value]);
|
|
65
|
+
|
|
60
66
|
useEffect(() => {
|
|
61
67
|
if (isPasswordField && isSet) {
|
|
62
68
|
executeIfRefAvailable(innerRef, (input) => {
|
|
@@ -99,7 +105,6 @@ export const SingleLineText: React.FC<SingleLineTextProps> = ({
|
|
|
99
105
|
name={name}
|
|
100
106
|
type={type}
|
|
101
107
|
ref={innerRef}
|
|
102
|
-
value={value}
|
|
103
108
|
defaultValue={defaultValue}
|
|
104
109
|
disabled={disabled}
|
|
105
110
|
placeholder={disabled ? undefined : placeholder}
|
|
@@ -46,6 +46,9 @@ export interface Column<T extends Data> {
|
|
|
46
46
|
|
|
47
47
|
/** Specify the horizontal text alignment of the column */
|
|
48
48
|
horizontalColumnAlign?: 'left' | 'center' | 'right';
|
|
49
|
+
|
|
50
|
+
/** If set to true, the column will not be resizable */
|
|
51
|
+
disableResizing?: boolean;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
export interface ColumnMap {
|
|
@@ -248,7 +248,7 @@ export const NotSortableColumn: StoryObj<StoryListType> = {
|
|
|
248
248
|
args: {
|
|
249
249
|
columns: [
|
|
250
250
|
defaultColumns[0],
|
|
251
|
-
{ ...defaultColumns[1], sortable: false },
|
|
251
|
+
{ ...defaultColumns[1], sortable: false, disableResizing: true },
|
|
252
252
|
...defaultColumns.slice(2),
|
|
253
253
|
],
|
|
254
254
|
},
|
|
@@ -24,6 +24,7 @@ import classes from './List.scss';
|
|
|
24
24
|
import { ListHeader } from './ListHeader/ListHeader';
|
|
25
25
|
import { ListRow } from './ListRow/ListRow';
|
|
26
26
|
import { ListRowLoader } from './ListRow/ListRowLoader';
|
|
27
|
+
import { useColumnsSize } from './useColumnsSize';
|
|
27
28
|
|
|
28
29
|
export interface ListProps<T extends Data> {
|
|
29
30
|
/**
|
|
@@ -104,53 +105,6 @@ export interface ListProps<T extends Data> {
|
|
|
104
105
|
inlineMenuActions?: (data: T) => ActionData[];
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
/**
|
|
108
|
-
* Generates a combined string of all columns.columnSize values, to be used as CSS value
|
|
109
|
-
* @param columns The list of columns that should be used
|
|
110
|
-
* @returns a string of all column sizes of the array, combined
|
|
111
|
-
*/
|
|
112
|
-
const getColumnsSizeDefinition = function <T extends Data>(
|
|
113
|
-
columns: Column<T>[],
|
|
114
|
-
showActionButton: boolean,
|
|
115
|
-
selectMode: ListSelectMode,
|
|
116
|
-
showInlineMenu: boolean,
|
|
117
|
-
): string {
|
|
118
|
-
const columnSizeDefinition = columns.map((column) => column.size ?? '1fr');
|
|
119
|
-
|
|
120
|
-
const hasActionsColumn =
|
|
121
|
-
selectMode !== ListSelectMode.None || showActionButton || showInlineMenu;
|
|
122
|
-
|
|
123
|
-
if (hasActionsColumn) {
|
|
124
|
-
columnSizeDefinition.push(
|
|
125
|
-
getActionsColumnSizePx(showInlineMenu, showActionButton),
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return columnSizeDefinition.join(' ');
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const getActionsColumnSizePx = (...enableActions: boolean[]): string => {
|
|
133
|
-
const enabledActionsCount = enableActions.filter(
|
|
134
|
-
(actionEnabled) => actionEnabled,
|
|
135
|
-
).length;
|
|
136
|
-
|
|
137
|
-
const defaultActionSizePx = 50;
|
|
138
|
-
const defaultActionsRawGapPx = 8;
|
|
139
|
-
const calculateMultiActionsColumnSizePx = (actionsCount: number): number => {
|
|
140
|
-
return (
|
|
141
|
-
defaultActionSizePx * actionsCount +
|
|
142
|
-
defaultActionsRawGapPx * (actionsCount - 1)
|
|
143
|
-
);
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const sizePx =
|
|
147
|
-
enabledActionsCount > 1
|
|
148
|
-
? calculateMultiActionsColumnSizePx(enabledActionsCount)
|
|
149
|
-
: defaultActionSizePx;
|
|
150
|
-
|
|
151
|
-
return `${sizePx}px`;
|
|
152
|
-
};
|
|
153
|
-
|
|
154
108
|
const noItemsMessage = (
|
|
155
109
|
itemsCount: number,
|
|
156
110
|
isLoading: boolean,
|
|
@@ -206,7 +160,7 @@ export const List = <T extends Data>({
|
|
|
206
160
|
isError = false,
|
|
207
161
|
errorMsg = 'There was an error.',
|
|
208
162
|
handleRetry = true,
|
|
209
|
-
minimumWidth = '
|
|
163
|
+
minimumWidth = 'fit-content',
|
|
210
164
|
columnGap = '5px',
|
|
211
165
|
rowGap = '0px',
|
|
212
166
|
headerRowHeight = '44px',
|
|
@@ -243,14 +197,19 @@ export const List = <T extends Data>({
|
|
|
243
197
|
});
|
|
244
198
|
}, [data]);
|
|
245
199
|
|
|
246
|
-
const columnSizes =
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
200
|
+
const { columnSizes, resetColumnSizes, setColumnSizes, hasActionColumn } =
|
|
201
|
+
useColumnsSize(
|
|
202
|
+
columns,
|
|
203
|
+
Boolean(showActionButton),
|
|
204
|
+
selectionMode,
|
|
205
|
+
Boolean(inlineMenuActions),
|
|
206
|
+
);
|
|
252
207
|
|
|
253
|
-
const customStyles = {
|
|
208
|
+
const customStyles = {
|
|
209
|
+
gridRowGap: rowGap,
|
|
210
|
+
minWidth: minimumWidth,
|
|
211
|
+
width: '100%',
|
|
212
|
+
};
|
|
254
213
|
|
|
255
214
|
const itemSelectionHandler = useCallback(
|
|
256
215
|
(items: ListItem<T>[]) => {
|
|
@@ -327,6 +286,9 @@ export const List = <T extends Data>({
|
|
|
327
286
|
isCheckboxDisabled={listItems.length === 0}
|
|
328
287
|
onCheckboxToggled={headerCheckboxHandler}
|
|
329
288
|
onSortChanged={sortChangedHandler}
|
|
289
|
+
onResetColumnSizes={resetColumnSizes}
|
|
290
|
+
onColumnSizesChanged={setColumnSizes}
|
|
291
|
+
hasActionColumn={hasActionColumn}
|
|
330
292
|
/>
|
|
331
293
|
{/* Rows */}
|
|
332
294
|
{listItems.map((item: ListItem<T>, index) => (
|
|
@@ -3,14 +3,9 @@
|
|
|
3
3
|
.container {
|
|
4
4
|
padding-left: 5px;
|
|
5
5
|
display: grid;
|
|
6
|
-
grid-template-columns: repeat(auto-fit, minmax(20px, 1fr));
|
|
7
|
-
grid-auto-rows: 44px;
|
|
8
|
-
column-gap: 5px;
|
|
9
|
-
justify-items: left;
|
|
10
|
-
align-items: center;
|
|
11
6
|
position: sticky;
|
|
12
7
|
top: 0;
|
|
13
|
-
z-index: 1;
|
|
8
|
+
//z-index: 1;
|
|
14
9
|
|
|
15
10
|
background-color: var(
|
|
16
11
|
--explorer-header-background-color,
|
|
@@ -22,11 +17,29 @@
|
|
|
22
17
|
|
|
23
18
|
.columnLabel {
|
|
24
19
|
box-sizing: border-box;
|
|
25
|
-
width:
|
|
20
|
+
width: 100%;
|
|
26
21
|
height: 100%;
|
|
27
22
|
display: grid;
|
|
28
|
-
grid-template-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
grid-template-columns: 1fr auto;
|
|
24
|
+
position: relative;
|
|
25
|
+
|
|
26
|
+
.resizeHandle {
|
|
27
|
+
cursor: col-resize;
|
|
28
|
+
width: 7px;
|
|
29
|
+
height: 100%;
|
|
30
|
+
|
|
31
|
+
z-index: 1;
|
|
32
|
+
border-right: var(--explorer-list-row-border, 1px solid #dddddd);
|
|
33
|
+
position: absolute;
|
|
34
|
+
right: 0;
|
|
35
|
+
|
|
36
|
+
&:hover:not(.resizeHandleDisabled) {
|
|
37
|
+
border-width: 3px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&.resizeHandleDisabled {
|
|
41
|
+
cursor: default;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
31
44
|
}
|
|
32
45
|
}
|
|
@@ -40,6 +40,9 @@ const mockProps: ListHeaderProps<TestListHeaderData> = {
|
|
|
40
40
|
actionSize: '50px',
|
|
41
41
|
horizontalTextAlign: 'left',
|
|
42
42
|
verticalTextAlign: 'center',
|
|
43
|
+
hasActionColumn: true,
|
|
44
|
+
onResetColumnSizes: jest.fn(),
|
|
45
|
+
onColumnSizesChanged: jest.fn(),
|
|
43
46
|
};
|
|
44
47
|
|
|
45
48
|
describe('ListHeader', () => {
|
|
@@ -155,4 +158,57 @@ describe('ListHeader', () => {
|
|
|
155
158
|
});
|
|
156
159
|
|
|
157
160
|
it.todo('reacts meaningfully when the columns are empty');
|
|
161
|
+
|
|
162
|
+
describe('Column Resizing', () => {
|
|
163
|
+
it('calls onResetColumnSizes when the reset button is clicked', () => {
|
|
164
|
+
const spy = jest.fn();
|
|
165
|
+
const wrapper = shallow(
|
|
166
|
+
<ListHeader {...mockProps} onResetColumnSizes={spy} />,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const resetButton = wrapper.find('.resizeHandle').first();
|
|
170
|
+
resetButton.simulate('doubleClick');
|
|
171
|
+
|
|
172
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('calls onResetColumnSizes when the reset button is clicked also on non-resizable column', () => {
|
|
176
|
+
const spy = jest.fn();
|
|
177
|
+
const wrapper = shallow(
|
|
178
|
+
<ListHeader
|
|
179
|
+
{...mockProps}
|
|
180
|
+
columns={[
|
|
181
|
+
{ ...mockListColumns[0], disableResizing: true },
|
|
182
|
+
...mockListColumns.slice(1),
|
|
183
|
+
]}
|
|
184
|
+
onResetColumnSizes={spy}
|
|
185
|
+
/>,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const resetButton = wrapper.find('.resizeHandle').first();
|
|
189
|
+
resetButton.simulate('doubleClick');
|
|
190
|
+
|
|
191
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('calls onColumnSizesChanged when the column is resized', () => {
|
|
195
|
+
const spy = jest.fn();
|
|
196
|
+
const wrapper = mount(
|
|
197
|
+
<ListHeader {...mockProps} onColumnSizesChanged={spy} />,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const resizeHandle = wrapper.find('.resizeHandle').first();
|
|
201
|
+
resizeHandle.invoke('onMouseDown')!({
|
|
202
|
+
preventDefault: jest.fn(),
|
|
203
|
+
} as any);
|
|
204
|
+
|
|
205
|
+
window.dispatchEvent(
|
|
206
|
+
new Event('mousemove', {
|
|
207
|
+
clientX: 100,
|
|
208
|
+
} as any),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
158
214
|
});
|