@axinom/mosaic-ui 0.34.0-rc.31 → 0.34.0-rc.33

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axinom/mosaic-ui",
3
- "version": "0.34.0-rc.31",
3
+ "version": "0.34.0-rc.33",
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.31",
35
+ "@axinom/mosaic-core": "^0.4.7-rc.33",
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": "76aa11c0c081add5afb09b9885d02546206bd977"
105
+ "gitHead": "647ce65e0672bc19eade7956fad659ae752b9125"
106
106
  }
@@ -1,4 +1,5 @@
1
1
  import { mount, shallow } from 'enzyme';
2
+ import { noop } from 'lodash';
2
3
  import React from 'react';
3
4
  import { act } from 'react-dom/test-utils';
4
5
  import { actWithReturn } from '../../../helpers/testing';
@@ -165,4 +166,52 @@ describe('SelectionExplorer', () => {
165
166
  mode: 'SINGLE_ITEMS',
166
167
  });
167
168
  });
169
+
170
+ it('shows inline menu with link to details page when `generateItemLink` is set', () => {
171
+ const [provider] = getDataProvider();
172
+ const path = '/test';
173
+
174
+ const wrapper = shallow(
175
+ <SelectionExplorer
176
+ columns={mockListColumns}
177
+ dataProvider={provider}
178
+ stationKey="mock-key"
179
+ onSelection={noop}
180
+ generateItemLink={() => path}
181
+ />,
182
+ );
183
+
184
+ const actions =
185
+ wrapper.find(Explorer).prop('inlineMenuActions')?.({}) || [];
186
+ expect(actions[0].path).toBe(path);
187
+ expect(actions[0].openInNewTab).toBe(true);
188
+ });
189
+
190
+ it('adds details page inline menu action when `inlineMenuActions` and `generateItemLink` is defined', () => {
191
+ const [provider] = getDataProvider();
192
+ const path = '/test';
193
+ const inlineAction = {
194
+ label: 'Test Action',
195
+ onActionSelected: noop,
196
+ };
197
+
198
+ const wrapper = shallow(
199
+ <SelectionExplorer
200
+ columns={mockListColumns}
201
+ dataProvider={provider}
202
+ stationKey="mock-key"
203
+ onSelection={noop}
204
+ inlineMenuActions={() => [inlineAction]}
205
+ generateItemLink={() => path}
206
+ />,
207
+ );
208
+
209
+ const actions =
210
+ wrapper.find(Explorer).prop('inlineMenuActions')?.({}) || [];
211
+
212
+ expect(actions[0]).toBe(inlineAction);
213
+
214
+ expect(actions[1].path).toBe(path);
215
+ expect(actions[1].openInNewTab).toBe(true);
216
+ });
168
217
  });
@@ -1,15 +1,20 @@
1
1
  import { faker } from '@faker-js/faker';
2
2
  import { action } from '@storybook/addon-actions';
3
3
  import { Meta, StoryObj } from '@storybook/react';
4
+ import { noop } from 'lodash';
4
5
  import React, { useMemo } from 'react';
6
+ import { MemoryRouter } from 'react-router';
5
7
  import {
6
8
  createGroups,
7
9
  generateItemArray,
8
10
  randomDate,
9
11
  } from '../../../helpers/storybook';
10
12
  import { FilterType, FilterTypes } from '../../Filters/Filters.model';
11
- import { ExplorerDataProvider } from '../Explorer.model';
12
- import { ExplorerStoryData } from '../ExplorerStoryType';
13
+ import { IconName } from '../../Icons';
14
+ import {
15
+ ExplorerDataProvider,
16
+ ExplorerDataProviderConfiguration,
17
+ } from '../Explorer.model';
13
18
  import {
14
19
  createInMemoryDataProvider,
15
20
  findAnywhereInStringCaseInsensitive,
@@ -77,7 +82,7 @@ export default meta;
77
82
  const generateData = (
78
83
  amount: number,
79
84
  { startIndex = 1, usePrefix = true } = {},
80
- ): ExplorerStoryData[] =>
85
+ ): SelectExplorerStoryData[] =>
81
86
  generateItemArray(amount, (i) => {
82
87
  const index = i + startIndex;
83
88
  return {
@@ -92,6 +97,49 @@ const generateData = (
92
97
  };
93
98
  });
94
99
 
100
+ const generateDataProvider =
101
+ (): ExplorerDataProvider<SelectExplorerStoryData> => {
102
+ const pageSize = 20;
103
+
104
+ const actualProvider = createInMemoryDataProvider<SelectExplorerStoryData>(
105
+ generateData(100, {
106
+ usePrefix: false,
107
+ }),
108
+ {
109
+ filterFunctions: {
110
+ title: findAnywhereInStringCaseInsensitive,
111
+ desc: findAnywhereInStringCaseInsensitive,
112
+ },
113
+ },
114
+ );
115
+ return {
116
+ loadData: async (
117
+ config: ExplorerDataProviderConfiguration<SelectExplorerStoryData>,
118
+ ) => {
119
+ // Report the call
120
+ action('loadData')(config);
121
+
122
+ const currentPage = (config.pagingInformation as number) ?? 0;
123
+
124
+ // Use the in memory provider to do the heavy lifting
125
+ const { totalCount, filteredCount, data } =
126
+ await actualProvider.loadData(config);
127
+
128
+ // Apply paging to the results
129
+ return {
130
+ totalCount: totalCount,
131
+ filteredCount: filteredCount,
132
+ data: data.slice(
133
+ pageSize * currentPage,
134
+ pageSize * (currentPage + 1),
135
+ ),
136
+ pagingInformation: currentPage + 1,
137
+ hasMoreData: currentPage < 9,
138
+ };
139
+ },
140
+ };
141
+ };
142
+
95
143
  const freeTextFilter: FilterType<SelectExplorerStoryData> = {
96
144
  label: 'Title',
97
145
  property: 'title',
@@ -114,8 +162,7 @@ const optionFilter: FilterType<SelectExplorerStoryData> = {
114
162
  ],
115
163
  };
116
164
 
117
- export const SelectExplorer: StoryObj<SelectionExplorerStoryType> = {
118
- name: 'Selection Explorer',
165
+ export const Default: StoryObj<SelectionExplorerStoryType> = {
119
166
  args: {
120
167
  title: 'Selection Explorer',
121
168
  columns: [
@@ -141,46 +188,7 @@ export const SelectExplorer: StoryObj<SelectionExplorerStoryType> = {
141
188
  render: (args) =>
142
189
  React.createElement(() => {
143
190
  const dataProvider: ExplorerDataProvider<SelectExplorerStoryData> =
144
- useMemo(() => {
145
- const pageSize = 20;
146
-
147
- const actualProvider =
148
- createInMemoryDataProvider<SelectExplorerStoryData>(
149
- generateData(100, {
150
- usePrefix: false,
151
- }),
152
- {
153
- filterFunctions: {
154
- title: findAnywhereInStringCaseInsensitive,
155
- desc: findAnywhereInStringCaseInsensitive,
156
- },
157
- },
158
- );
159
- return {
160
- loadData: async (config) => {
161
- // Report the call
162
- action('loadData')(config);
163
-
164
- const currentPage = (config.pagingInformation as number) ?? 0;
165
-
166
- // Use the in memory provider to do the heavy lifting
167
- const { totalCount, filteredCount, data } =
168
- await actualProvider.loadData(config);
169
-
170
- // Apply paging to the results
171
- return {
172
- totalCount: totalCount,
173
- filteredCount: filteredCount,
174
- data: data.slice(
175
- pageSize * currentPage,
176
- pageSize * (currentPage + 1),
177
- ),
178
- pagingInformation: currentPage + 1,
179
- hasMoreData: currentPage < 9,
180
- };
181
- },
182
- };
183
- }, []);
191
+ useMemo(generateDataProvider, []);
184
192
 
185
193
  return (
186
194
  <SelectionExplorer<SelectExplorerStoryData>
@@ -190,3 +198,61 @@ export const SelectExplorer: StoryObj<SelectionExplorerStoryType> = {
190
198
  );
191
199
  }),
192
200
  };
201
+
202
+ export const WithOpenDetailsInlineAction: StoryObj<SelectionExplorerStoryType> =
203
+ {
204
+ args: {
205
+ ...Default.args,
206
+ stationKey: 'SelectionStoryBookExplorer_WithOpenDetailsInlineAction',
207
+ generateItemLink: () => `#`,
208
+ },
209
+ render: (args) =>
210
+ React.createElement(() => {
211
+ const dataProvider: ExplorerDataProvider<SelectExplorerStoryData> =
212
+ useMemo(generateDataProvider, []);
213
+
214
+ return (
215
+ <MemoryRouter>
216
+ <SelectionExplorer<SelectExplorerStoryData>
217
+ {...args}
218
+ dataProvider={dataProvider}
219
+ />
220
+ </MemoryRouter>
221
+ );
222
+ }),
223
+ };
224
+
225
+ export const WithInlineActions: StoryObj<SelectionExplorerStoryType> = {
226
+ args: {
227
+ ...Default.args,
228
+ stationKey: 'SelectionStoryBookExplorer_WithInlineActions',
229
+ inlineMenuActions: () => {
230
+ return [
231
+ {
232
+ label: 'Navigation Action',
233
+ icon: IconName.NavigateRight,
234
+ path: '#',
235
+ },
236
+ {
237
+ label: 'Context Action',
238
+ icon: IconName.Ellipsis,
239
+ onActionSelected: noop,
240
+ },
241
+ ];
242
+ },
243
+ },
244
+ render: (args) =>
245
+ React.createElement(() => {
246
+ const dataProvider: ExplorerDataProvider<SelectExplorerStoryData> =
247
+ useMemo(generateDataProvider, []);
248
+
249
+ return (
250
+ <MemoryRouter>
251
+ <SelectionExplorer<SelectExplorerStoryData>
252
+ {...args}
253
+ dataProvider={dataProvider}
254
+ />
255
+ </MemoryRouter>
256
+ );
257
+ }),
258
+ };
@@ -1,6 +1,8 @@
1
+ import { LocationDescriptor } from 'history';
1
2
  import React, { ForwardedRef } from 'react';
2
3
  import { noop } from '../../../helpers/utils';
3
4
  import { Data } from '../../../types/data';
5
+ import { ActionData } from '../../Actions/Actions.models';
4
6
  import { IconName } from '../../Icons';
5
7
  import { ListSelectMode } from '../../List';
6
8
  import { Explorer, ExplorerProps } from '../Explorer';
@@ -23,6 +25,9 @@ export interface SelectionExplorerProps<T extends Data>
23
25
  * The selected item (or items) will be passed as argument to the callback.
24
26
  */
25
27
  onSelection?: (selection: ItemSelection<T>) => void;
28
+
29
+ /** When set, this function is used to generate inline menu link that navigates user to details page for each item. */
30
+ generateItemLink?: (data: T) => LocationDescriptor<unknown>;
26
31
  }
27
32
 
28
33
  /**
@@ -38,14 +43,14 @@ export interface SelectionExplorerProps<T extends Data>
38
43
  export const SelectionExplorer = React.forwardRef(function SelectionExplorer<
39
44
  T extends Data,
40
45
  >(
41
- props: SelectionExplorerProps<T>,
46
+ { generateItemLink, inlineMenuActions, ...rest }: SelectionExplorerProps<T>,
42
47
  ref: ForwardedRef<ExplorerDataProviderConnection<T>>,
43
48
  ): JSX.Element {
44
49
  const {
45
50
  allowBulkSelect = false,
46
51
  onSelection = noop,
47
52
  modalMode = true,
48
- } = props;
53
+ } = rest;
49
54
 
50
55
  const onItemClickedHandler = (item: T, mode?: ListSelectMode): void => {
51
56
  // only if the list is not in Multi mode is this executed
@@ -54,10 +59,25 @@ export const SelectionExplorer = React.forwardRef(function SelectionExplorer<
54
59
  }
55
60
  };
56
61
 
62
+ let selectionExplorerActions = inlineMenuActions;
63
+
64
+ if (generateItemLink) {
65
+ selectionExplorerActions = (data: T): ActionData[] => {
66
+ return [
67
+ ...(inlineMenuActions?.(data) ?? []),
68
+ {
69
+ label: 'Open Details',
70
+ path: generateItemLink(data),
71
+ openInNewTab: true,
72
+ },
73
+ ];
74
+ };
75
+ }
76
+
57
77
  return (
58
78
  <Explorer<T>
79
+ {...rest}
59
80
  ref={ref}
60
- {...props}
61
81
  modalMode={modalMode}
62
82
  bulkActions={[
63
83
  ...(allowBulkSelect
@@ -75,6 +95,7 @@ export const SelectionExplorer = React.forwardRef(function SelectionExplorer<
75
95
  ]}
76
96
  selectionMode={ListSelectMode.Single}
77
97
  onItemClicked={onItemClickedHandler}
98
+ inlineMenuActions={selectionExplorerActions}
78
99
  />
79
100
  );
80
101
  });
@@ -19,7 +19,6 @@
19
19
  font-size: var(--label-font-size, $label-font-size);
20
20
  color: var(--input-color, $input-color);
21
21
  max-width: 100%;
22
- overflow: auto;
23
22
  }
24
23
 
25
24
  .label {
@@ -50,8 +50,11 @@
50
50
  margin-left: auto;
51
51
  }
52
52
 
53
- .link {
53
+ .content {
54
54
  display: contents;
55
+ }
56
+
57
+ .link {
55
58
  color: inherit;
56
59
  text-decoration: none;
57
60
  }
@@ -229,7 +229,7 @@ describe('ListRow', () => {
229
229
  it('calls the "onItemClicked" event with data', () => {
230
230
  const spy = jest.fn();
231
231
  const wrapper = shallow(<ListRow {...mockProps} onItemClicked={spy} />);
232
- wrapper.simulate('click');
232
+ wrapper.find('.content').simulate('click');
233
233
  expect(spy).toHaveBeenCalledTimes(1);
234
234
  expect(spy).toHaveBeenCalledWith(mockExplorerData);
235
235
  });
@@ -258,9 +258,14 @@ describe('ListRow', () => {
258
258
  });
259
259
 
260
260
  it('raises checkBoxHandler', () => {
261
- const spy = jest.fn();
261
+ const spy = jest.fn().mockImplementation(() => null);
262
262
  const wrapper = mount(
263
- <ListRow {...mockProps} onItemSelected={spy} showItemCheckbox={true} />,
263
+ <ListRow
264
+ {...mockProps}
265
+ onItemSelected={spy}
266
+ showItemCheckbox={true}
267
+ selectionMode={ListSelectMode.Multi}
268
+ />,
264
269
  );
265
270
 
266
271
  const chk = wrapper.find(ListCheckBox);
@@ -366,7 +371,7 @@ describe('ListRow', () => {
366
371
  />,
367
372
  );
368
373
 
369
- const row = wrapper.find('.columnsRoot');
374
+ const row = wrapper.find('.content');
370
375
 
371
376
  row.simulate('click');
372
377
 
@@ -388,7 +393,7 @@ describe('ListRow', () => {
388
393
  />,
389
394
  );
390
395
 
391
- const row = wrapper.find('.columnsRoot');
396
+ const row = wrapper.find('.content');
392
397
 
393
398
  row.simulate('click');
394
399
 
@@ -411,7 +416,7 @@ describe('ListRow', () => {
411
416
  />,
412
417
  );
413
418
 
414
- const row = wrapper.find('.columnsRoot');
419
+ const row = wrapper.find('.content');
415
420
 
416
421
  row.simulate('click');
417
422
 
@@ -230,32 +230,37 @@ export const ListRow = <T extends Data>({
230
230
  [classes.disabled]: isRowDisabled,
231
231
  })}
232
232
  style={customRootStyles}
233
- onClick={() => {
234
- onItemClickedHandler(data);
235
- }}
236
233
  ref={isTrigger ? elementRef : null}
237
234
  data-test-id="list-entry"
238
235
  >
239
236
  {/* Items */}
240
237
  {isLinkable ? (
241
- <Link to={onItemClicked as string} className={classes.link}>
238
+ <Link
239
+ to={onItemClicked as string}
240
+ className={clsx(classes.content, classes.link)}
241
+ >
242
242
  {generateCells}
243
243
  </Link>
244
244
  ) : (
245
- <>{generateCells}</>
245
+ <div
246
+ className={classes.content}
247
+ onClick={() => onItemClickedHandler(data)}
248
+ >
249
+ {generateCells}
250
+ </div>
246
251
  )}
247
- {(inlineMenuActions || showActionButton) &&
248
- selectionMode === ListSelectMode.None && (
249
- <div className={classes.actions}>
250
- {inlineActionMenuData && inlineActionMenuData.length > 0 && (
251
- <InlineMenu
252
- actions={inlineActionMenuData}
253
- showArrow={false}
254
- placement="bottom-end"
255
- onButtonClicked={(e) => e.stopPropagation()}
256
- />
257
- )}
258
-
252
+ <div className={classes.actions}>
253
+ {inlineMenuActions &&
254
+ inlineActionMenuData &&
255
+ inlineActionMenuData.length > 0 && (
256
+ <InlineMenu
257
+ actions={inlineActionMenuData}
258
+ showArrow={false}
259
+ placement="bottom-end"
260
+ />
261
+ )}
262
+ {showActionButton && selectionMode === ListSelectMode.None && (
263
+ <>
259
264
  {showActionButton &&
260
265
  (typeof onItemClicked !== 'function' ? (
261
266
  <Button
@@ -273,27 +278,29 @@ export const ListRow = <T extends Data>({
273
278
  dataTestId="list-entry-action"
274
279
  />
275
280
  ))}
276
- </div>
281
+ </>
277
282
  )}
278
283
 
279
- {showCheckMark && (
280
- <Button
281
- icon={IconName.Checkmark}
282
- height={actionSize}
283
- width={actionSize}
284
- dataTestId="list-entry-select-button"
285
- className={clsx(classes.selectionCheckMark)}
286
- />
287
- )}
288
- {showItemCheckbox && (
289
- <ListCheckBox
290
- height={actionSize}
291
- width={actionSize}
292
- onCheckBoxToggled={onItemSelected}
293
- isChecked={itemSelected}
294
- isDisabled={isRowDisabled}
295
- />
296
- )}
284
+ {showCheckMark && (
285
+ <Button
286
+ icon={IconName.Checkmark}
287
+ height={actionSize}
288
+ width={actionSize}
289
+ dataTestId="list-entry-select-button"
290
+ className={classes.selectionCheckMark}
291
+ onButtonClicked={() => onItemClickedHandler(data)}
292
+ />
293
+ )}
294
+ {showItemCheckbox && (
295
+ <ListCheckBox
296
+ height={actionSize}
297
+ width={actionSize}
298
+ onCheckBoxToggled={() => onItemClickedHandler(data)}
299
+ isChecked={itemSelected}
300
+ isDisabled={isRowDisabled}
301
+ />
302
+ )}
303
+ </div>
297
304
  </div>
298
305
  );
299
306