cozy-ui 127.10.0 → 127.11.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,19 @@
1
+ # [127.11.0](https://github.com/cozy/cozy-ui/compare/v127.10.1...v127.11.0) (2025-08-18)
2
+
3
+
4
+ ### Features
5
+
6
+ * **ContactsList:** Add ContactCell component to show a contact in table ([234bfe2](https://github.com/cozy/cozy-ui/commit/234bfe2))
7
+ * **ContactsList:** Add makeGroupLabelsAndCounts function ([b628571](https://github.com/cozy/cozy-ui/commit/b628571))
8
+ * **Providers:** Add Selection provider ([b9854b6](https://github.com/cozy/cozy-ui/commit/b9854b6))
9
+
10
+ ## [127.10.1](https://github.com/cozy/cozy-ui/compare/v127.10.0...v127.10.1) (2025-08-18)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * Reorganize virtualized grid list to render item content :bug: ([62cf105](https://github.com/cozy/cozy-ui/commit/62cf105))
16
+
1
17
  # [127.10.0](https://github.com/cozy/cozy-ui/compare/v127.9.0...v127.10.0) (2025-07-31)
2
18
 
3
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-ui",
3
- "version": "127.10.0",
3
+ "version": "127.11.0",
4
4
  "description": "Cozy apps UI SDK",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -0,0 +1,58 @@
1
+ import React, { useRef, useState } from 'react'
2
+
3
+ import ContactIdentity from './Contacts/ContactIdentity'
4
+ import { locales } from './locales/withContactsListLocales'
5
+ import ActionsMenu from '../ActionsMenu'
6
+ import Icon from '../Icon'
7
+ import IconButton from '../IconButton'
8
+ import DotsIcon from '../Icons/Dots'
9
+ import ListItemIcon from '../ListItemIcon'
10
+ import { useI18n, useExtendI18n } from '../providers/I18n'
11
+
12
+ const Cell = ({ row, column, cell, actions }) => {
13
+ const [showActions, setShowActions] = useState(false)
14
+ useExtendI18n(locales)
15
+ const { t } = useI18n()
16
+ const actionsRef = useRef()
17
+
18
+ if (column.id === 'fullname') {
19
+ return <ContactIdentity contact={row} noWrapper />
20
+ }
21
+
22
+ if (column.id === 'actions') {
23
+ if (!actions) {
24
+ return null
25
+ }
26
+
27
+ return (
28
+ <>
29
+ <ListItemIcon>
30
+ <IconButton
31
+ ref={actionsRef}
32
+ arial-label={t('menu')}
33
+ onClick={() => setShowActions(true)}
34
+ >
35
+ <Icon icon={DotsIcon} />
36
+ </IconButton>
37
+ </ListItemIcon>
38
+ {showActions && (
39
+ <ActionsMenu
40
+ open
41
+ ref={actionsRef}
42
+ docs={[row]}
43
+ actions={actions}
44
+ anchorOrigin={{
45
+ vertical: 'bottom',
46
+ horizontal: 'right'
47
+ }}
48
+ onClose={() => setShowActions(false)}
49
+ />
50
+ )}
51
+ </>
52
+ )
53
+ }
54
+
55
+ return cell
56
+ }
57
+
58
+ export default React.memo(Cell)
@@ -1,3 +1,5 @@
1
+ import get from 'lodash/get'
2
+
1
3
  export function buildLastNameFirst(contact) {
2
4
  const givenName =
3
5
  contact.name && contact.name.givenName
@@ -36,15 +38,39 @@ const makeHeader = (contact, t) => {
36
38
  return name[0] || t('empty')
37
39
  }
38
40
 
41
+ /**
42
+ * Build header for a contact (first letter of indexes.byFamilyNameGivenNameEmailCozyUrl)
43
+ * @param {object} contact
44
+ * @param {function} t translation function
45
+ * @returns {string} header
46
+ */
47
+ const makeHeaderForIndexedContacts = (contact, t) => {
48
+ if (contact.me) return t('me')
49
+ if (contact.cozyMetadata?.favorite) return t('favorite')
50
+
51
+ const index = get(contact, 'indexes.byFamilyNameGivenNameEmailCozyUrl', '')
52
+ const hasIndex = index !== null && index.length > 0
53
+
54
+ if (hasIndex) {
55
+ const firstLetterWithoutAccent = index[0]
56
+ .normalize('NFD')
57
+ .replace(/\p{Diacritic}/gu, '')
58
+ return firstLetterWithoutAccent
59
+ }
60
+
61
+ return t('empty')
62
+ }
63
+
39
64
  /**
40
65
  * @typedef {Object.<string, Object>} CategorizedContactsResult
41
66
  */
42
67
 
43
68
  /**
44
69
  * Categorize contacts by first letter of last name
70
+ * Expl.: all contacts with A as first letter will be in A category
45
71
  * @param {object[]} contacts io.cozy.contacts documents
46
72
  * @param {function} t translation function
47
- * @returns {CategorizedContactsResult}
73
+ * @returns {CategorizedContactsResult} Categorized contacts
48
74
  */
49
75
  export const categorizeContacts = (contacts, t) =>
50
76
  contacts.reduce((acc, contact) => {
@@ -81,3 +107,26 @@ export const sortHeaders = (categorized, t) => {
81
107
 
82
108
  return headersSorted.concat(notEmptyAndMyselfSorted)
83
109
  }
110
+
111
+ /**
112
+ * Counts how many contacts are categorized by first letter and store it in `groupCounts`
113
+ * Expl.: if there is 3 contacts in A and 2 in B, it will return [3,2]
114
+ * Also store first letters and store them in `groupLabels`
115
+ * @param {array} contacts - Array of io.cozy.contact documents
116
+ * @returns {object}
117
+ */
118
+ export const makeGroupLabelsAndCounts = (contacts, t) => {
119
+ return contacts.reduce(
120
+ (acc, contact) => {
121
+ const header = makeHeaderForIndexedContacts(contact, t)
122
+ if (!acc.groupLabels.includes(header)) {
123
+ acc.groupLabels.push(header)
124
+ }
125
+ const idx = acc.groupLabels.indexOf(header)
126
+ const val = acc.groupCounts[idx] || 0
127
+ acc.groupCounts[idx] = val + 1
128
+ return acc
129
+ },
130
+ { groupLabels: [], groupCounts: [] }
131
+ )
132
+ }
@@ -1,4 +1,11 @@
1
- import { sortLastNameFirst, sortHeaders } from './helpers'
1
+ import {
2
+ sortLastNameFirst,
3
+ sortHeaders,
4
+ makeGroupLabelsAndCounts
5
+ } from './helpers'
6
+
7
+ const t = x => x
8
+
2
9
  describe('Sort contacts', () => {
3
10
  describe('By Last Name', () => {
4
11
  it('should sort contact by last name', () => {
@@ -99,3 +106,45 @@ describe('sortHeaders', () => {
99
106
  expect(sortedHeaders).toEqual(['me', 'empty', 'B', 'F', 'H'])
100
107
  })
101
108
  })
109
+
110
+ describe('makeGroupLabelsAndCounts', () => {
111
+ it('should returns labels and counts', () => {
112
+ const contacts = [
113
+ { name: 'Alex', indexes: { byFamilyNameGivenNameEmailCozyUrl: 'A' } },
114
+ { name: 'Alan', indexes: { byFamilyNameGivenNameEmailCozyUrl: 'A' } },
115
+ { name: 'Cleo', indexes: { byFamilyNameGivenNameEmailCozyUrl: 'C' } },
116
+ { name: 'Cloé', indexes: { byFamilyNameGivenNameEmailCozyUrl: 'C' } },
117
+ { name: 'Clotilde', indexes: { byFamilyNameGivenNameEmailCozyUrl: 'C' } },
118
+ { name: 'Constant', indexes: { byFamilyNameGivenNameEmailCozyUrl: 'C' } },
119
+ {
120
+ name: 'Christophe',
121
+ indexes: { byFamilyNameGivenNameEmailCozyUrl: 'C' }
122
+ },
123
+ { name: 'Bernard', indexes: { byFamilyNameGivenNameEmailCozyUrl: 'B' } },
124
+ { name: 'Baptiste', indexes: { byFamilyNameGivenNameEmailCozyUrl: 'B' } },
125
+ { name: 'Xavier', indexes: { byFamilyNameGivenNameEmailCozyUrl: 'X' } },
126
+ { name: 'Zorro', indexes: { byFamilyNameGivenNameEmailCozyUrl: 'Z' } },
127
+ { name: '', indexes: { byFamilyNameGivenNameEmailCozyUrl: '' } },
128
+ { name: {}, indexes: { byFamilyNameGivenNameEmailCozyUrl: {} } },
129
+ { name: 'John', indexes: { byFamilyNameGivenNameEmailCozyUrl: null } },
130
+ {
131
+ name: 'Connor',
132
+ indexes: { byFamilyNameGivenNameEmailCozyUrl: undefined }
133
+ },
134
+ { name: 'Àlbert', indexes: { byFamilyNameGivenNameEmailCozyUrl: 'À' } },
135
+ {
136
+ name: 'Alice',
137
+ me: true,
138
+ indexes: { byFamilyNameGivenNameEmailCozyUrl: 'Alice' }
139
+ },
140
+ { name: 'Èllen', indexes: { byFamilyNameGivenNameEmailCozyUrl: 'È' } }
141
+ ]
142
+
143
+ const res = makeGroupLabelsAndCounts(contacts, t)
144
+
145
+ expect(res).toStrictEqual({
146
+ groupLabels: ['A', 'C', 'B', 'X', 'Z', 'empty', 'me', 'E'],
147
+ groupCounts: [3, 5, 2, 1, 1, 4, 1, 1]
148
+ })
149
+ })
150
+ })
@@ -1,4 +1,6 @@
1
1
  {
2
2
  "empty": "EMPTY",
3
- "me": "ME"
3
+ "me": "me",
4
+ "favorite": "favorites",
5
+ "menu": "Menu"
4
6
  }
@@ -1,4 +1,6 @@
1
1
  {
2
2
  "empty": "VIDE",
3
- "me": "MOI"
3
+ "me": "moi",
4
+ "favorite": "favoris",
5
+ "menu": "Menu"
4
6
  }
@@ -41,13 +41,13 @@ const VirtualizedGridListDnd = ({
41
41
  <div {...scrollerProps} ref={ref} />
42
42
  </DnDConfigWrapper>
43
43
  )),
44
- Item: ({ context, children, ...props }) => {
44
+ Item: ({ context, ...props }) => {
45
45
  const item = context?.items?.[props['data-index']]
46
46
  return (
47
47
  <GridItem
48
48
  item={item}
49
49
  context={context}
50
- renderItem={() => <>{children}</>}
50
+ renderItem={itemRenderer}
51
51
  {...componentProps.Item}
52
52
  />
53
53
  )
@@ -55,7 +55,6 @@ const VirtualizedGridListDnd = ({
55
55
  ...components
56
56
  }}
57
57
  context={_context}
58
- itemRenderer={itemRenderer}
59
58
  {...props}
60
59
  >
61
60
  {children}
@@ -2,7 +2,7 @@ import React, { forwardRef } from 'react'
2
2
  import { VirtuosoGrid } from 'react-virtuoso'
3
3
 
4
4
  const VirtualizedGridList = forwardRef(
5
- ({ items = [], itemRenderer, components, context, ...props }, ref) => {
5
+ ({ items = [], components, context, ...props }, ref) => {
6
6
  return (
7
7
  <VirtuosoGrid
8
8
  ref={ref}
@@ -10,7 +10,6 @@ const VirtualizedGridList = forwardRef(
10
10
  context={context}
11
11
  style={{ height: '100%' }}
12
12
  totalCount={items.length}
13
- itemContent={index => itemRenderer(items[index])}
14
13
  {...props}
15
14
  />
16
15
  )
@@ -0,0 +1,28 @@
1
+ ```jsx
2
+ import DemoProvider from 'cozy-ui/docs/components/DemoProvider'
3
+ import SelectionProvider, { useSelection } from 'cozy-ui/transpiled/react/providers/Selection'
4
+ import Button from 'cozy-ui/transpiled/react/Buttons'
5
+ import Icon from 'cozy-ui/transpiled/react/Icon'
6
+ import Typography from 'cozy-ui/transpiled/react/Typography'
7
+ import DeviceLaptopIcon from 'cozy-ui/transpiled/react/Icons/DeviceLaptop'
8
+
9
+ const Component = () => {
10
+ const { selectedItemsId, addSelectedItem, removeSelectedItem } = useSelection()
11
+
12
+ return (
13
+ <>
14
+ <Typography>selectedItemsId : {JSON.stringify(selectedItemsId)}</Typography>
15
+ <Button label="Add Item" onClick={() => addSelectedItem({ _id: '01' })} />
16
+ <Button label="Remove Item" onClick={() => removeSelectedItem({ _id: '01' })} />
17
+ </>
18
+ )
19
+ }
20
+
21
+ ;
22
+
23
+ <DemoProvider>
24
+ <SelectionProvider>
25
+ <Component />
26
+ </SelectionProvider>
27
+ </DemoProvider>
28
+ ```
@@ -0,0 +1,85 @@
1
+ import isEqual from 'lodash/isEqual'
2
+ import React, { createContext, useContext, useState } from 'react'
3
+
4
+ const SelectionContext = createContext()
5
+
6
+ export const useSelection = () => {
7
+ const context = useContext(SelectionContext)
8
+
9
+ if (!context) {
10
+ throw new Error('useSelection must be used within a SelectionProvider')
11
+ }
12
+ return context
13
+ }
14
+
15
+ /**
16
+ * This provider allows you to manage item selection
17
+ */
18
+ const SelectionProvider = ({ children }) => {
19
+ const [selectedItemsId, setSelectedItemsId] = useState([])
20
+
21
+ const isSelectedItem = item => {
22
+ return selectedItemsId.some(selectedItemId => selectedItemId === item._id)
23
+ }
24
+
25
+ const isSelectionEnabled = selectedItemsId.length > 0
26
+
27
+ const addSelectedItem = item => {
28
+ setSelectedItemsId(prev => [...prev, item._id])
29
+ }
30
+
31
+ const removeSelectedItem = item => {
32
+ setSelectedItemsId(prev => prev.filter(el => el !== item._id))
33
+ }
34
+
35
+ const toggleSelectedItem = item => {
36
+ if (isSelectedItem(item)) {
37
+ removeSelectedItem(item)
38
+ } else {
39
+ addSelectedItem(item)
40
+ }
41
+ }
42
+
43
+ const selectAll = items => {
44
+ const ids = items.map(item => item._id)
45
+ setSelectedItemsId(ids)
46
+ }
47
+
48
+ const unselectAll = () => {
49
+ setSelectedItemsId([])
50
+ }
51
+
52
+ const isSelectedAllItems = items => {
53
+ const a = selectedItemsId.sort()
54
+ const b = items.map(item => item._id).sort()
55
+ return isEqual(a, b)
56
+ }
57
+
58
+ const toggleSelectAllItems = items => {
59
+ if (isSelectedAllItems(items)) {
60
+ unselectAll()
61
+ } else {
62
+ selectAll(items)
63
+ }
64
+ }
65
+
66
+ return (
67
+ <SelectionContext.Provider
68
+ value={{
69
+ selectedItemsId,
70
+ addSelectedItem,
71
+ removeSelectedItem,
72
+ toggleSelectedItem,
73
+ selectAll,
74
+ toggleSelectAllItems,
75
+ isSelectedItem,
76
+ isSelectionEnabled,
77
+ isSelectedAllItems
78
+ }}
79
+ >
80
+ {children}
81
+ </SelectionContext.Provider>
82
+ )
83
+ }
84
+
85
+ export default React.memo(SelectionProvider)
@@ -0,0 +1,8 @@
1
+ declare var _default: React.MemoExoticComponent<({ row, column, cell, actions }: {
2
+ row: any;
3
+ column: any;
4
+ cell: any;
5
+ actions: any;
6
+ }) => any>;
7
+ export default _default;
8
+ import React from "react";
@@ -0,0 +1,68 @@
1
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
+ import React, { useRef, useState } from 'react';
3
+ import ContactIdentity from "cozy-ui/transpiled/react/ContactsList/Contacts/ContactIdentity";
4
+ import { locales } from "cozy-ui/transpiled/react/ContactsList/locales/withContactsListLocales";
5
+ import ActionsMenu from "cozy-ui/transpiled/react/ActionsMenu";
6
+ import Icon from "cozy-ui/transpiled/react/Icon";
7
+ import IconButton from "cozy-ui/transpiled/react/IconButton";
8
+ import DotsIcon from "cozy-ui/transpiled/react/Icons/Dots";
9
+ import ListItemIcon from "cozy-ui/transpiled/react/ListItemIcon";
10
+ import { useI18n, useExtendI18n } from "cozy-ui/transpiled/react/providers/I18n";
11
+
12
+ var Cell = function Cell(_ref) {
13
+ var row = _ref.row,
14
+ column = _ref.column,
15
+ cell = _ref.cell,
16
+ actions = _ref.actions;
17
+
18
+ var _useState = useState(false),
19
+ _useState2 = _slicedToArray(_useState, 2),
20
+ showActions = _useState2[0],
21
+ setShowActions = _useState2[1];
22
+
23
+ useExtendI18n(locales);
24
+
25
+ var _useI18n = useI18n(),
26
+ t = _useI18n.t;
27
+
28
+ var actionsRef = useRef();
29
+
30
+ if (column.id === 'fullname') {
31
+ return /*#__PURE__*/React.createElement(ContactIdentity, {
32
+ contact: row,
33
+ noWrapper: true
34
+ });
35
+ }
36
+
37
+ if (column.id === 'actions') {
38
+ if (!actions) {
39
+ return null;
40
+ }
41
+
42
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ListItemIcon, null, /*#__PURE__*/React.createElement(IconButton, {
43
+ ref: actionsRef,
44
+ "arial-label": t('menu'),
45
+ onClick: function onClick() {
46
+ return setShowActions(true);
47
+ }
48
+ }, /*#__PURE__*/React.createElement(Icon, {
49
+ icon: DotsIcon
50
+ }))), showActions && /*#__PURE__*/React.createElement(ActionsMenu, {
51
+ open: true,
52
+ ref: actionsRef,
53
+ docs: [row],
54
+ actions: actions,
55
+ anchorOrigin: {
56
+ vertical: 'bottom',
57
+ horizontal: 'right'
58
+ },
59
+ onClose: function onClose() {
60
+ return setShowActions(false);
61
+ }
62
+ }));
63
+ }
64
+
65
+ return cell;
66
+ };
67
+
68
+ export default /*#__PURE__*/React.memo(Cell);
@@ -3,6 +3,7 @@ export function sortLastNameFirst(contact: any, comparedContact: any): any;
3
3
  export function sortContacts(contacts: any): any;
4
4
  export function categorizeContacts(contacts: object[], t: Function): CategorizedContactsResult;
5
5
  export function sortHeaders(categorized: CategorizedContactsResult, t: Function): string[];
6
+ export function makeGroupLabelsAndCounts(contacts: array, t: any): object;
6
7
  export type CategorizedContactsResult = {
7
8
  [x: string]: Object;
8
9
  };
@@ -1,3 +1,4 @@
1
+ import get from 'lodash/get';
1
2
  export function buildLastNameFirst(contact) {
2
3
  var givenName = contact.name && contact.name.givenName ? contact.name.givenName.toUpperCase() : '';
3
4
  var familyName = contact.name && contact.name.familyName ? contact.name.familyName.toUpperCase() : '';
@@ -28,15 +29,39 @@ var makeHeader = function makeHeader(contact, t) {
28
29
  var name = buildLastNameFirst(contact);
29
30
  return name[0] || t('empty');
30
31
  };
32
+ /**
33
+ * Build header for a contact (first letter of indexes.byFamilyNameGivenNameEmailCozyUrl)
34
+ * @param {object} contact
35
+ * @param {function} t translation function
36
+ * @returns {string} header
37
+ */
38
+
39
+
40
+ var makeHeaderForIndexedContacts = function makeHeaderForIndexedContacts(contact, t) {
41
+ var _contact$cozyMetadata;
42
+
43
+ if (contact.me) return t('me');
44
+ if ((_contact$cozyMetadata = contact.cozyMetadata) !== null && _contact$cozyMetadata !== void 0 && _contact$cozyMetadata.favorite) return t('favorite');
45
+ var index = get(contact, 'indexes.byFamilyNameGivenNameEmailCozyUrl', '');
46
+ var hasIndex = index !== null && index.length > 0;
47
+
48
+ if (hasIndex) {
49
+ var firstLetterWithoutAccent = index[0].normalize('NFD').replace(/(?:[\^`\xA8\xAF\xB4\xB7\xB8\u02B0-\u034E\u0350-\u0357\u035D-\u0362\u0374\u0375\u037A\u0384\u0385\u0483-\u0487\u0559\u0591-\u05A1\u05A3-\u05BD\u05BF\u05C1\u05C2\u05C4\u064B-\u0652\u0657\u0658\u06DF\u06E0\u06E5\u06E6\u06EA-\u06EC\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F5\u0818\u0819\u0898-\u089F\u08C9-\u08D2\u08E3-\u08FE\u093C\u094D\u0951-\u0954\u0971\u09BC\u09CD\u0A3C\u0A4D\u0ABC\u0ACD\u0AFD-\u0AFF\u0B3C\u0B4D\u0B55\u0BCD\u0C3C\u0C4D\u0CBC\u0CCD\u0D3B\u0D3C\u0D4D\u0DCA\u0E47-\u0E4C\u0E4E\u0EBA\u0EC8-\u0ECC\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F82-\u0F84\u0F86\u0F87\u0FC6\u1037\u1039\u103A\u1063\u1064\u1069-\u106D\u1087-\u108D\u108F\u109A\u109B\u135D-\u135F\u1714\u1715\u17C9-\u17D3\u17DD\u1939-\u193B\u1A75-\u1A7C\u1A7F\u1AB0-\u1ABE\u1AC1-\u1ACB\u1B34\u1B44\u1B6B-\u1B73\u1BAA\u1BAB\u1C36\u1C37\u1C78-\u1C7D\u1CD0-\u1CE8\u1CED\u1CF4\u1CF7-\u1CF9\u1D2C-\u1D6A\u1DC4-\u1DCF\u1DF5-\u1DFF\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u2CEF-\u2CF1\u2E2F\u302A-\u302F\u3099-\u309C\u30FC\uA66F\uA67C\uA67D\uA67F\uA69C\uA69D\uA6F0\uA6F1\uA700-\uA721\uA788-\uA78A\uA7F8\uA7F9\uA8C4\uA8E0-\uA8F1\uA92B-\uA92E\uA953\uA9B3\uA9C0\uA9E5\uAA7B-\uAA7D\uAABF-\uAAC2\uAAF6\uAB5B-\uAB5F\uAB69-\uAB6B\uABEC\uABED\uFB1E\uFE20-\uFE2F\uFF3E\uFF40\uFF70\uFF9E\uFF9F\uFFE3]|\uD800\uDEE0|\uD801[\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDEE5\uDEE6]|\uD803[\uDD22-\uDD27\uDF46-\uDF50\uDF82-\uDF85]|\uD804[\uDC46\uDC70\uDCB9\uDCBA\uDD33\uDD34\uDD73\uDDC0\uDDCA-\uDDCC\uDE35\uDE36\uDEE9\uDEEA\uDF3C\uDF4D\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC42\uDC46\uDCC2\uDCC3\uDDBF\uDDC0\uDE3F\uDEB6\uDEB7\uDF2B]|\uD806[\uDC39\uDC3A\uDD3D\uDD3E\uDD43\uDDE0\uDE34\uDE47\uDE99]|\uD807[\uDC3F\uDD42\uDD44\uDD45\uDD97]|\uD81A[\uDEF0-\uDEF4\uDF30-\uDF36]|\uD81B[\uDF8F-\uDF9F\uDFF0\uDFF1]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD833[\uDF00-\uDF2D\uDF30-\uDF46]|\uD834[\uDD67-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD]|\uD838[\uDD30-\uDD36\uDEAE\uDEEC-\uDEEF]|\uD83A[\uDCD0-\uDCD6\uDD44-\uDD46\uDD48-\uDD4A])/g, '');
50
+ return firstLetterWithoutAccent;
51
+ }
52
+
53
+ return t('empty');
54
+ };
31
55
  /**
32
56
  * @typedef {Object.<string, Object>} CategorizedContactsResult
33
57
  */
34
58
 
35
59
  /**
36
60
  * Categorize contacts by first letter of last name
61
+ * Expl.: all contacts with A as first letter will be in A category
37
62
  * @param {object[]} contacts io.cozy.contacts documents
38
63
  * @param {function} t translation function
39
- * @returns {CategorizedContactsResult}
64
+ * @returns {CategorizedContactsResult} Categorized contacts
40
65
  */
41
66
 
42
67
 
@@ -77,4 +102,29 @@ export var sortHeaders = function sortHeaders(categorized, t) {
77
102
  }
78
103
 
79
104
  return headersSorted.concat(notEmptyAndMyselfSorted);
105
+ };
106
+ /**
107
+ * Counts how many contacts are categorized by first letter and store it in `groupCounts`
108
+ * Expl.: if there is 3 contacts in A and 2 in B, it will return [3,2]
109
+ * Also store first letters and store them in `groupLabels`
110
+ * @param {array} contacts - Array of io.cozy.contact documents
111
+ * @returns {object}
112
+ */
113
+
114
+ export var makeGroupLabelsAndCounts = function makeGroupLabelsAndCounts(contacts, t) {
115
+ return contacts.reduce(function (acc, contact) {
116
+ var header = makeHeaderForIndexedContacts(contact, t);
117
+
118
+ if (!acc.groupLabels.includes(header)) {
119
+ acc.groupLabels.push(header);
120
+ }
121
+
122
+ var idx = acc.groupLabels.indexOf(header);
123
+ var val = acc.groupCounts[idx] || 0;
124
+ acc.groupCounts[idx] = val + 1;
125
+ return acc;
126
+ }, {
127
+ groupLabels: [],
128
+ groupCounts: []
129
+ });
80
130
  };
@@ -1,10 +1,14 @@
1
1
  var en = {
2
2
  empty: "EMPTY",
3
- me: "ME"
3
+ me: "me",
4
+ favorite: "favorites",
5
+ menu: "Menu"
4
6
  };
5
7
  var fr = {
6
8
  empty: "VIDE",
7
- me: "MOI"
9
+ me: "moi",
10
+ favorite: "favoris",
11
+ menu: "Menu"
8
12
  };
9
13
  import withOnlyLocales from "cozy-ui/transpiled/react/providers/I18n/withOnlyLocales";
10
14
  export var locales = {
@@ -3,7 +3,7 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
3
3
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
4
4
  import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
5
5
  var _excluded = ["dragProps", "context", "itemRenderer", "children", "componentProps", "components"],
6
- _excluded2 = ["context", "children"];
6
+ _excluded2 = ["context"];
7
7
 
8
8
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
9
9
 
@@ -60,21 +60,17 @@ var VirtualizedGridListDnd = function VirtualizedGridListDnd(_ref) {
60
60
  var _context$items;
61
61
 
62
62
  var context = _ref3.context,
63
- children = _ref3.children,
64
63
  props = _objectWithoutProperties(_ref3, _excluded2);
65
64
 
66
65
  var item = context === null || context === void 0 ? void 0 : (_context$items = context.items) === null || _context$items === void 0 ? void 0 : _context$items[props['data-index']];
67
66
  return /*#__PURE__*/React.createElement(GridItem, _extends({
68
67
  item: item,
69
68
  context: context,
70
- renderItem: function renderItem() {
71
- return /*#__PURE__*/React.createElement(React.Fragment, null, children);
72
- }
69
+ renderItem: itemRenderer
73
70
  }, componentProps.Item));
74
71
  }
75
72
  }, components),
76
- context: _context,
77
- itemRenderer: itemRenderer
73
+ context: _context
78
74
  }, props), children));
79
75
  };
80
76
 
@@ -1,12 +1,11 @@
1
1
  import _extends from "@babel/runtime/helpers/extends";
2
2
  import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
3
- var _excluded = ["items", "itemRenderer", "components", "context"];
3
+ var _excluded = ["items", "components", "context"];
4
4
  import React, { forwardRef } from 'react';
5
5
  import { VirtuosoGrid } from 'react-virtuoso';
6
6
  var VirtualizedGridList = /*#__PURE__*/forwardRef(function (_ref, ref) {
7
7
  var _ref$items = _ref.items,
8
8
  items = _ref$items === void 0 ? [] : _ref$items,
9
- itemRenderer = _ref.itemRenderer,
10
9
  components = _ref.components,
11
10
  context = _ref.context,
12
11
  props = _objectWithoutProperties(_ref, _excluded);
@@ -18,10 +17,7 @@ var VirtualizedGridList = /*#__PURE__*/forwardRef(function (_ref, ref) {
18
17
  style: {
19
18
  height: '100%'
20
19
  },
21
- totalCount: items.length,
22
- itemContent: function itemContent(index) {
23
- return itemRenderer(items[index]);
24
- }
20
+ totalCount: items.length
25
21
  }, props));
26
22
  });
27
23
  VirtualizedGridList.displayName = 'VirtualizedGridList';
@@ -0,0 +1,6 @@
1
+ export function useSelection(): any;
2
+ declare var _default: React.MemoExoticComponent<({ children }: {
3
+ children: any;
4
+ }) => JSX.Element>;
5
+ export default _default;
6
+ import React from "react";
@@ -0,0 +1,99 @@
1
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
+ import isEqual from 'lodash/isEqual';
4
+ import React, { createContext, useContext, useState } from 'react';
5
+ var SelectionContext = /*#__PURE__*/createContext();
6
+ export var useSelection = function useSelection() {
7
+ var context = useContext(SelectionContext);
8
+
9
+ if (!context) {
10
+ throw new Error('useSelection must be used within a SelectionProvider');
11
+ }
12
+
13
+ return context;
14
+ };
15
+ /**
16
+ * This provider allows you to manage item selection
17
+ */
18
+
19
+ var SelectionProvider = function SelectionProvider(_ref) {
20
+ var children = _ref.children;
21
+
22
+ var _useState = useState([]),
23
+ _useState2 = _slicedToArray(_useState, 2),
24
+ selectedItemsId = _useState2[0],
25
+ setSelectedItemsId = _useState2[1];
26
+
27
+ var isSelectedItem = function isSelectedItem(item) {
28
+ return selectedItemsId.some(function (selectedItemId) {
29
+ return selectedItemId === item._id;
30
+ });
31
+ };
32
+
33
+ var isSelectionEnabled = selectedItemsId.length > 0;
34
+
35
+ var addSelectedItem = function addSelectedItem(item) {
36
+ setSelectedItemsId(function (prev) {
37
+ return [].concat(_toConsumableArray(prev), [item._id]);
38
+ });
39
+ };
40
+
41
+ var removeSelectedItem = function removeSelectedItem(item) {
42
+ setSelectedItemsId(function (prev) {
43
+ return prev.filter(function (el) {
44
+ return el !== item._id;
45
+ });
46
+ });
47
+ };
48
+
49
+ var toggleSelectedItem = function toggleSelectedItem(item) {
50
+ if (isSelectedItem(item)) {
51
+ removeSelectedItem(item);
52
+ } else {
53
+ addSelectedItem(item);
54
+ }
55
+ };
56
+
57
+ var selectAll = function selectAll(items) {
58
+ var ids = items.map(function (item) {
59
+ return item._id;
60
+ });
61
+ setSelectedItemsId(ids);
62
+ };
63
+
64
+ var unselectAll = function unselectAll() {
65
+ setSelectedItemsId([]);
66
+ };
67
+
68
+ var isSelectedAllItems = function isSelectedAllItems(items) {
69
+ var a = selectedItemsId.sort();
70
+ var b = items.map(function (item) {
71
+ return item._id;
72
+ }).sort();
73
+ return isEqual(a, b);
74
+ };
75
+
76
+ var toggleSelectAllItems = function toggleSelectAllItems(items) {
77
+ if (isSelectedAllItems(items)) {
78
+ unselectAll();
79
+ } else {
80
+ selectAll(items);
81
+ }
82
+ };
83
+
84
+ return /*#__PURE__*/React.createElement(SelectionContext.Provider, {
85
+ value: {
86
+ selectedItemsId: selectedItemsId,
87
+ addSelectedItem: addSelectedItem,
88
+ removeSelectedItem: removeSelectedItem,
89
+ toggleSelectedItem: toggleSelectedItem,
90
+ selectAll: selectAll,
91
+ toggleSelectAllItems: toggleSelectAllItems,
92
+ isSelectedItem: isSelectedItem,
93
+ isSelectionEnabled: isSelectionEnabled,
94
+ isSelectedAllItems: isSelectedAllItems
95
+ }
96
+ }, children);
97
+ };
98
+
99
+ export default /*#__PURE__*/React.memo(SelectionProvider);