cozy-ui 139.0.1 → 139.1.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ # [139.1.0](https://github.com/cozy/cozy-ui/compare/v139.0.2...v139.1.0) (2026-06-03)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **Table:** No need colSpan + 1 anymore, checkbox has been removed ([4329e35](https://github.com/cozy/cozy-ui/commit/4329e35))
7
+
8
+
9
+ ### Features
10
+
11
+ * **Table:** Add `noWrap` column prop to enable ellipsis on cells content ([5ebd5c0](https://github.com/cozy/cozy-ui/commit/5ebd5c0))
12
+ * **Table:** Apply tableLayout fixed for ellipsis support ([00663fa](https://github.com/cozy/cozy-ui/commit/00663fa))
13
+
14
+ ## [139.0.2](https://github.com/cozy/cozy-ui/compare/v139.0.1...v139.0.2) (2026-06-02)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **useSubmitWithLoader:** Avoid double submit ([c65b431](https://github.com/cozy/cozy-ui/commit/c65b431))
20
+
1
21
  ## [139.0.1](https://github.com/cozy/cozy-ui/compare/v139.0.0...v139.0.1) (2026-06-01)
2
22
 
3
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-ui",
3
- "version": "139.0.1",
3
+ "version": "139.1.0",
4
4
  "description": "Cozy apps UI SDK",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -514,6 +514,13 @@ export const makeLightOverrides = theme => ({
514
514
  paddingRight: 16
515
515
  }
516
516
  },
517
+ MuiTable: {
518
+ root: {
519
+ tableLayout: 'fixed',
520
+ width: '100%',
521
+ minWidth: 'max-content'
522
+ }
523
+ },
517
524
  MuiTableHead: {
518
525
  root: {
519
526
  backgroundColor: theme.palette.background.paper,
@@ -41,7 +41,8 @@ const columns = [
41
41
  {
42
42
  id: 'name',
43
43
  disablePadding: false,
44
- label: 'Dessert'
44
+ label: 'Dessert',
45
+ noWrap: false
45
46
  },
46
47
  {
47
48
  id: 'calories',
@@ -75,7 +76,7 @@ const columns = [
75
76
  }
76
77
  ]
77
78
 
78
- const initialVariants = [{ grouped: false}]
79
+ const initialVariants = [{ grouped: false }]
79
80
 
80
81
  // Very basic usage only works when Dessert is sorted "asc"
81
82
  // Ideally you have to create a logic to create groups with sorted data
@@ -89,62 +90,69 @@ const ExampleTable = ({ variant, ...props }) => {
89
90
  }
90
91
 
91
92
  return (
92
- <div>
93
- <Button
93
+ <VirtualizedTable
94
+ {...props}
95
+ rows={rows}
96
+ columns={columns}
97
+ groups={variant.grouped ? makeGroups : undefined}
98
+ selectedItems={selectedItemsId}
99
+ isSelectedItem={row => isSelectedItem(row.id)}
100
+ onSortChange={onSortChange}
101
+ componentsProps={{
102
+ rowContent: {
103
+ onClick: (row, column) => {
104
+ row.id !== 1
105
+ ? toggleSelectedItem(row.id)
106
+ : undefined
107
+ },
108
+ onDoubleClick: (row, column) => {
109
+ row.id !== 1
110
+ ? console.info(`double click on cell. Row ${row['id']}, Column ${column['id']}`)
111
+ : undefined
112
+ },
113
+ onLongPress: (row, column) => {
114
+ row.id !== 1
115
+ ? console.info(`long press on cell. Row ${row['id']}, Column ${column['id']}`)
116
+ : undefined
117
+ }
118
+ }
119
+ }}
120
+ />
121
+ )
122
+ }
123
+
124
+ const SelectButton = () => {
125
+ const { toggleSelectAllItems } = useSelection()
126
+
127
+ return (
128
+ <Button
94
129
  className="u-mt-1 u-mb-1"
95
130
  variant="ghost"
96
131
  label="Select all"
97
132
  onClick={() => toggleSelectAllItems(rows.map(item => item.id !== 1 ? item.id : undefined))}
98
- />
99
- <div style={{ border: "1px solid var(--borderMainColor)", height: 400, width: "100%" }}>
100
- <VirtualizedTable
101
- {...props}
102
- rows={rows}
103
- columns={columns}
104
- groups={variant.grouped ? makeGroups : undefined}
105
- selectedItems={selectedItemsId}
106
- isSelectedItem={row => isSelectedItem(row.id)}
107
- onSortChange={onSortChange}
108
- componentsProps={{
109
- rowContent: {
110
- onClick: (row, column) => {
111
- row.id !== 1
112
- ? toggleSelectedItem(row.id)
113
- : undefined
114
- },
115
- onDoubleClick: (row, column) => {
116
- row.id !== 1
117
- ? console.info(`double click on cell. Row ${row['id']}, Column ${column['id']}`)
118
- : undefined
119
- },
120
- onLongPress: (row, column) => {
121
- row.id !== 1
122
- ? console.info(`long press on cell. Row ${row['id']}, Column ${column['id']}`)
123
- : undefined
124
- }
125
- }
126
- }}
127
- />
128
- </div>
129
- </div>
133
+ />
130
134
  )
131
135
  }
132
136
 
137
+ ;
138
+
133
139
  <Variants initialVariants={initialVariants} screenshotAllVariants>
134
140
  {variant => (
135
141
  <>
136
- <Typography variant="h4">Not sorted table</Typography>
137
- <div className="u-mt-half" style={{ border: "1px solid var(--borderMainColor)", height: 400, width: "100%", marginBottom: "6rem" }}>
138
- <SelectionProvider>
142
+ <SelectionProvider>
143
+ <Typography className="u-mt-1" variant="h4">Not sorted table</Typography>
144
+ <SelectButton />
145
+ <div style={{ border: "1px solid var(--borderMainColor)", height: 400, width: "100%" }}>
139
146
  <ExampleTable variant={variant} />
140
- </SelectionProvider>
141
- </div>
142
- <Typography className="u-mt-1" variant="h4">Sorted table</Typography>
143
- <div className="u-mt-half" style={{ border: "1px solid var(--borderMainColor)", height: 400, width: "100%" }}>
144
- <SelectionProvider>
147
+ </div>
148
+ </SelectionProvider>
149
+ <SelectionProvider>
150
+ <Typography className="u-mt-1" variant="h4">Sorted table</Typography>
151
+ <SelectButton />
152
+ <div style={{ border: "1px solid var(--borderMainColor)", height: 400, width: "100%" }}>
145
153
  <ExampleTable variant={variant} defaultOrder={{by: columns[0].id, direction: 'asc'}} />
146
- </SelectionProvider>
147
- </div>
154
+ </div>
155
+ </SelectionProvider>
148
156
  </>
149
157
  )}
150
158
  </Variants>
@@ -175,10 +183,18 @@ const rows = [
175
183
  createData(7, 'Jelly Bean', 375, 0.0, 94, 0.0),
176
184
  createData(8, 'KitKat', 518, 26.0, 65, 7.0),
177
185
  createData(9, 'Oreo', 437, 18.0, 63, 4.0),
186
+ createData(
187
+ 10,
188
+ 'Ice cream with a very long list of ingredient to see how the table can handle this kind of item, and this is the end',
189
+ 237,
190
+ 9.0,
191
+ 37,
192
+ 4.3
193
+ )
178
194
  ]
179
195
 
180
196
  const columns = [
181
- { id: 'name', disablePadding: false, label: 'Dessert' },
197
+ { id: 'name', disablePadding: false, label: 'Dessert', noWrap: true },
182
198
  { id: 'calories', disablePadding: false, width: 80, label: 'Calories', textAlign: 'left' },
183
199
  { id: 'fat', disablePadding: false, width: 85, label: 'Fat (g)', textAlign: 'right' },
184
200
  { id: 'carbs', disablePadding: false, width: 115, label: 'Carbs (g)', textAlign: 'right' },
@@ -199,36 +215,32 @@ const DndExample = () => {
199
215
  }
200
216
 
201
217
  return (
202
- <div style={{ border: "1px solid var(--borderMainColor)", height: 400, width: "100%" }}>
203
- <VirtualizedTableDnd
204
- rows={rows}
205
- columns={columns}
206
- dragProps={dragProps}
207
- selectedItems={selectedItemsId}
208
- isSelectedItem={row => isSelectedItem(row._id)}
209
- componentsProps={{
210
- rowContent: {
211
- onClick: (row, column) => toggleSelectedItem(row._id),
212
- onDoubleClick: (row, column) => {
213
- console.info(`double click on cell. Row ${row['_id']}, Column ${column['id']}`)
214
- },
215
- onLongPress: (row, column) => { console.info(`long press on cell. Row ${row['_id']}, Column ${column['id']}`) },
218
+ <VirtualizedTableDnd
219
+ rows={rows}
220
+ columns={columns}
221
+ dragProps={dragProps}
222
+ selectedItems={selectedItemsId}
223
+ isSelectedItem={row => isSelectedItem(row._id)}
224
+ componentsProps={{
225
+ rowContent: {
226
+ onClick: (row, column) => toggleSelectedItem(row._id),
227
+ onDoubleClick: (row, column) => {
228
+ console.info(`double click on cell. Row ${row['_id']}, Column ${column['id']}`)
216
229
  },
217
- }}
218
- />
219
- </div>
230
+ onLongPress: (row, column) => { console.info(`long press on cell. Row ${row['_id']}, Column ${column['id']}`) },
231
+ },
232
+ }}
233
+ />
220
234
  )
221
235
  }
222
236
 
223
237
  ;
224
238
 
225
- <>
226
- <div className="u-mt-half" style={{ border: "1px solid var(--borderMainColor)", height: 400, width: "100%" }}>
227
- <SelectionProvider>
228
- <DndProvider backend={HTML5Backend}>
229
- <DndExample />
230
- </DndProvider>
231
- </SelectionProvider>
232
- </div>
233
- </>
239
+ <SelectionProvider>
240
+ <DndProvider backend={HTML5Backend}>
241
+ <div className="u-mt-half" style={{ border: "1px solid var(--borderMainColor)", height: 400, width: "100%" }}>
242
+ <DndExample />
243
+ </div>
244
+ </DndProvider>
245
+ </SelectionProvider>
234
246
  ```
@@ -67,6 +67,7 @@ const Cell = ({
67
67
  key={column.id}
68
68
  ref={longPressRef}
69
69
  classes={classes}
70
+ className={column.noWrap ? 'u-ellipsis' : undefined}
70
71
  align={column.textAlign ?? 'left'}
71
72
  padding={column.disablePadding ? 'none' : 'normal'}
72
73
  onClick={handleClick}
@@ -24,6 +24,7 @@ const TableHeadCell = ({
24
24
  <TableCell
25
25
  key={column.id}
26
26
  classes={classes}
27
+ className={column.noWrap ? 'u-ellipsis' : undefined}
27
28
  align={column.textAlign ?? 'left'}
28
29
  padding={column.disablePadding ? 'none' : 'normal'}
29
30
  sortDirection={orderBy === column.id ? orderDirection : false}
@@ -77,7 +77,7 @@ const VirtualizedTable = forwardRef(
77
77
  )}
78
78
  {...(isGroupedTable && {
79
79
  groupContent: index => (
80
- <TableCell colSpan={columns.length + 1} size="small">
80
+ <TableCell colSpan={columns.length} size="small">
81
81
  {groupLabels[index]}
82
82
  </TableCell>
83
83
  )
@@ -93,7 +93,6 @@ const VirtualizedTable = forwardRef(
93
93
  {componentsProps?.rowContent?.children}
94
94
  </RowContent>
95
95
  )}
96
- rowSpan={2}
97
96
  />
98
97
  )
99
98
  }
@@ -9,6 +9,8 @@ export const useSubmitWithLoader = () => {
9
9
  const { t } = useI18n()
10
10
 
11
11
  const onSubmit = async ({ submit, success, error }) => {
12
+ if (isLoading) return
13
+
12
14
  setIsLoading(true)
13
15
 
14
16
  try {
@@ -0,0 +1,74 @@
1
+ import { renderHook, act } from '@testing-library/react-hooks'
2
+ import React from 'react'
3
+ import I18n from 'twake-i18n'
4
+
5
+ import { useSubmitWithLoader } from './index'
6
+ import AlertProvider from '../../providers/Alert'
7
+
8
+ jest.mock('../../Snackbar', () => ({
9
+ __esModule: true,
10
+ default: ({ children }) => children
11
+ }))
12
+
13
+ const renderUseSubmitWithLoader = () =>
14
+ renderHook(() => useSubmitWithLoader(), {
15
+ wrapper: ({ children }) => (
16
+ <I18n lang="en" dictRequire={() => ({})}>
17
+ <AlertProvider>{children}</AlertProvider>
18
+ </I18n>
19
+ )
20
+ })
21
+
22
+ describe('useSubmitWithLoader', () => {
23
+ it('should not execute submit when isLoading is true', async () => {
24
+ const { result } = renderUseSubmitWithLoader()
25
+ const submit = jest.fn()
26
+
27
+ act(() => {
28
+ result.current.onSubmit({ submit, success: { message: 'ok' }, error: {} })
29
+ })
30
+ expect(result.current.isLoading).toBe(true)
31
+
32
+ await act(async () => {
33
+ await result.current.onSubmit({
34
+ submit,
35
+ success: { message: 'ok' },
36
+ error: {}
37
+ })
38
+ })
39
+
40
+ expect(submit).toHaveBeenCalledTimes(1)
41
+ })
42
+
43
+ it('should call success.action when submit succeeds', async () => {
44
+ const { result } = renderUseSubmitWithLoader()
45
+ const successAction = jest.fn()
46
+ const submit = jest.fn().mockResolvedValue(undefined)
47
+
48
+ await act(async () => {
49
+ await result.current.onSubmit({
50
+ submit,
51
+ success: { message: 'Success', action: successAction },
52
+ error: {}
53
+ })
54
+ })
55
+
56
+ expect(successAction).toHaveBeenCalled()
57
+ })
58
+
59
+ it('should call error.action when submit throws', async () => {
60
+ const { result } = renderUseSubmitWithLoader()
61
+ const errorAction = jest.fn()
62
+ const submit = jest.fn().mockRejectedValue(new Error('fail'))
63
+
64
+ await act(async () => {
65
+ await result.current.onSubmit({
66
+ submit,
67
+ success: {},
68
+ error: { message: () => 'Error', action: errorAction }
69
+ })
70
+ })
71
+
72
+ expect(errorAction).toHaveBeenCalled()
73
+ })
74
+ })
@@ -1407,6 +1407,25 @@ import Stepper from 'cozy-ui/transpiled/react/Stepper'
1407
1407
  import Table from 'cozy-ui/transpiled/react/Table'
1408
1408
  ```
1409
1409
 
1410
+ **Example:**
1411
+
1412
+ ```jsx
1413
+ import { useState } from 'react'
1414
+ import VirtualizedTableDnd from 'cozy-ui/transpiled/react/Table/Virtualized/Dnd'
1415
+ import Typography from 'cozy-ui/transpiled/react/Typography'
1416
+ import SelectionProvider, { useSelection } from 'cozy-ui/transpiled/react/providers/Selection'
1417
+ import { DndProvider } from 'react-dnd'
1418
+ import { HTML5Backend } from 'react-dnd-html5-backend'
1419
+
1420
+ <SelectionProvider>
1421
+ <DndProvider backend={HTML5Backend}>
1422
+ <div className="u-mt-half" style={{ border: "1px solid var(--borderMainColor)", height: 400, width: "100%" }}>
1423
+ <DndExample />
1424
+ </div>
1425
+ </DndProvider>
1426
+ </SelectionProvider>
1427
+ ```
1428
+
1410
1429
 
1411
1430
  ### Thumbnail
1412
1431
 
@@ -586,6 +586,13 @@ export function makeDarkOverrides(theme: any): {
586
586
  paddingRight: number;
587
587
  };
588
588
  };
589
+ MuiTable: {
590
+ root: {
591
+ tableLayout: string;
592
+ width: string;
593
+ minWidth: string;
594
+ };
595
+ };
589
596
  MuiTableHead: {
590
597
  root: {
591
598
  backgroundColor: any;
@@ -586,6 +586,13 @@ export function makeLightOverrides(theme: any): {
586
586
  paddingRight: number;
587
587
  };
588
588
  };
589
+ MuiTable: {
590
+ root: {
591
+ tableLayout: string;
592
+ width: string;
593
+ minWidth: string;
594
+ };
595
+ };
589
596
  MuiTableHead: {
590
597
  root: {
591
598
  backgroundColor: any;
@@ -488,6 +488,13 @@ export var makeLightOverrides = function makeLightOverrides(theme) {
488
488
  paddingRight: 16
489
489
  }
490
490
  },
491
+ MuiTable: {
492
+ root: {
493
+ tableLayout: 'fixed',
494
+ width: '100%',
495
+ minWidth: 'max-content'
496
+ }
497
+ },
491
498
  MuiTableHead: {
492
499
  root: {
493
500
  backgroundColor: theme.palette.background.paper,
@@ -84,6 +84,7 @@ var Cell = function Cell(_ref4) {
84
84
  key: column.id,
85
85
  ref: longPressRef,
86
86
  classes: classes,
87
+ className: column.noWrap ? 'u-ellipsis' : undefined,
87
88
  align: (_column$textAlign = column.textAlign) !== null && _column$textAlign !== void 0 ? _column$textAlign : 'left',
88
89
  padding: column.disablePadding ? 'none' : 'normal',
89
90
  onClick: handleClick,
@@ -29,6 +29,7 @@ var TableHeadCell = function TableHeadCell(_ref3) {
29
29
  return /*#__PURE__*/React.createElement(TableCell, {
30
30
  key: column.id,
31
31
  classes: classes,
32
+ className: column.noWrap ? 'u-ellipsis' : undefined,
32
33
  align: (_column$textAlign = column.textAlign) !== null && _column$textAlign !== void 0 ? _column$textAlign : 'left',
33
34
  padding: column.disablePadding ? 'none' : 'normal',
34
35
  sortDirection: orderBy === column.id ? orderDirection : false
@@ -92,7 +92,7 @@ var VirtualizedTable = /*#__PURE__*/forwardRef(function (_ref, ref) {
92
92
  }, isGroupedTable && {
93
93
  groupContent: function groupContent(index) {
94
94
  return /*#__PURE__*/React.createElement(TableCell, {
95
- colSpan: columns.length + 1,
95
+ colSpan: columns.length,
96
96
  size: "small"
97
97
  }, groupLabels[index]);
98
98
  }
@@ -106,8 +106,7 @@ var VirtualizedTable = /*#__PURE__*/forwardRef(function (_ref, ref) {
106
106
  columns: columns,
107
107
  context: _context
108
108
  }), componentsProps === null || componentsProps === void 0 ? void 0 : (_componentsProps$rowC = componentsProps.rowContent) === null || _componentsProps$rowC === void 0 ? void 0 : _componentsProps$rowC.children);
109
- },
110
- rowSpan: 2
109
+ }
111
110
  }));
112
111
  });
113
112
  VirtualizedTable.displayName = 'VirtualizedTable';
@@ -25,23 +25,32 @@ export var useSubmitWithLoader = function useSubmitWithLoader() {
25
25
  switch (_context.prev = _context.next) {
26
26
  case 0:
27
27
  submit = _ref.submit, success = _ref.success, error = _ref.error;
28
+
29
+ if (!isLoading) {
30
+ _context.next = 3;
31
+ break;
32
+ }
33
+
34
+ return _context.abrupt("return");
35
+
36
+ case 3:
28
37
  setIsLoading(true);
29
- _context.prev = 2;
30
- _context.next = 5;
38
+ _context.prev = 4;
39
+ _context.next = 7;
31
40
  return submit();
32
41
 
33
- case 5:
42
+ case 7:
34
43
  showAlert({
35
44
  severity: 'success',
36
45
  message: (success === null || success === void 0 ? void 0 : success.message) || t('useSubmitWithLoader.success')
37
46
  });
38
47
  success === null || success === void 0 ? void 0 : (_success$action = success.action) === null || _success$action === void 0 ? void 0 : _success$action.call(success);
39
- _context.next = 13;
48
+ _context.next = 15;
40
49
  break;
41
50
 
42
- case 9:
43
- _context.prev = 9;
44
- _context.t0 = _context["catch"](2);
51
+ case 11:
52
+ _context.prev = 11;
53
+ _context.t0 = _context["catch"](4);
45
54
  showAlert({
46
55
  severity: 'error',
47
56
  message: (error === null || error === void 0 ? void 0 : (_error$message = error.message) === null || _error$message === void 0 ? void 0 : _error$message.call(error, _context.t0)) || t('useSubmitWithLoader.error', {
@@ -50,17 +59,17 @@ export var useSubmitWithLoader = function useSubmitWithLoader() {
50
59
  });
51
60
  error === null || error === void 0 ? void 0 : (_error$action = error.action) === null || _error$action === void 0 ? void 0 : _error$action.call(error);
52
61
 
53
- case 13:
54
- _context.prev = 13;
62
+ case 15:
63
+ _context.prev = 15;
55
64
  setIsLoading(false);
56
- return _context.finish(13);
65
+ return _context.finish(15);
57
66
 
58
- case 16:
67
+ case 18:
59
68
  case "end":
60
69
  return _context.stop();
61
70
  }
62
71
  }
63
- }, _callee, null, [[2, 9, 13, 16]]);
72
+ }, _callee, null, [[4, 11, 15, 18]]);
64
73
  }));
65
74
 
66
75
  return function onSubmit(_x) {