cozy-harvest-lib 8.2.0 → 8.3.1

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
@@ -3,6 +3,39 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [8.3.1](https://github.com/cozy/cozy-libs/compare/cozy-harvest-lib@8.3.0...cozy-harvest-lib@8.3.1) (2022-04-15)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * Avoid to have not sanitized fields as identifier fields ([c6ed9ec](https://github.com/cozy/cozy-libs/commit/c6ed9ec194685e5eeb9f81e5c620d17beabf0289))
12
+
13
+
14
+
15
+
16
+
17
+ # [8.3.0](https://github.com/cozy/cozy-libs/compare/cozy-harvest-lib@8.2.1...cozy-harvest-lib@8.3.0) (2022-04-15)
18
+
19
+
20
+ ### Features
21
+
22
+ * Implement ViewerModal as Route ([bdb6e88](https://github.com/cozy/cozy-libs/commit/bdb6e88e4c70217654f3b72828dee964aa2a4d1b))
23
+
24
+
25
+
26
+
27
+
28
+ ## [8.2.1](https://github.com/cozy/cozy-libs/compare/cozy-harvest-lib@8.2.0...cozy-harvest-lib@8.2.1) (2022-04-13)
29
+
30
+
31
+ ### Bug Fixes
32
+
33
+ * Get correct bi mapping for bnp_es and cic_es ([f304f89](https://github.com/cozy/cozy-libs/commit/f304f8978f1857f3f1d1e74c0c19385ab96dde24))
34
+
35
+
36
+
37
+
38
+
6
39
  # [8.2.0](https://github.com/cozy/cozy-libs/compare/cozy-harvest-lib@8.1.1...cozy-harvest-lib@8.2.0) (2022-04-13)
7
40
 
8
41
 
@@ -191,7 +191,7 @@ export var AccountForm = /*#__PURE__*/function (_PureComponent) {
191
191
  var _this$props = this.props,
192
192
  account = _this$props.account,
193
193
  konnector = _this$props.konnector;
194
- var identifier = manifest.getIdentifier(konnector.fields);
194
+ var identifier = manifest.getIdentifier(manifest.sanitizeFields(konnector.fields));
195
195
 
196
196
  if (account && account.auth[identifier] && account.auth[identifier] !== values[identifier]) {
197
197
  this.showConfirmationModal();
@@ -272,7 +272,7 @@ export var AccountForm = /*#__PURE__*/function (_PureComponent) {
272
272
  var isReadOnlyIdentifier = Boolean(get(account, 'relationships.vaultCipher')) && this.props.readOnlyIdentifier;
273
273
 
274
274
  if (isReadOnlyIdentifier) {
275
- var identifier = manifest.getIdentifier(fields);
275
+ var identifier = manifest.getIdentifier(sanitizedFields);
276
276
  sanitizedFields[identifier].type = 'hidden';
277
277
  }
278
278
 
@@ -308,7 +308,7 @@ export var AccountForm = /*#__PURE__*/function (_PureComponent) {
308
308
  className: "u-mb-1",
309
309
  onClick: onBack,
310
310
  konnector: konnector,
311
- identifier: get(account, "auth.".concat(manifest.getIdentifier(konnector.fields)))
311
+ identifier: get(account, "auth.".concat(manifest.getIdentifier(sanitizedFields)))
312
312
  }), isRunnable({
313
313
  win: window,
314
314
  konnector: konnector
@@ -51,6 +51,19 @@ var fixtures = {
51
51
  }
52
52
  }
53
53
  },
54
+ konnectorWithAdvancedField: {
55
+ fields: {
56
+ advancedFields: {
57
+ folderPath: {
58
+ advanced: true,
59
+ isRequired: false
60
+ }
61
+ },
62
+ username: {
63
+ type: 'text'
64
+ }
65
+ }
66
+ },
54
67
  account: {
55
68
  auth: {
56
69
  username: 'Toto',
@@ -434,6 +447,40 @@ describe('AccountForm', function () {
434
447
  var hiddenInput = wrapper.find('input[type="hidden"][name="username"]');
435
448
  expect(hiddenInput).toHaveLength(1);
436
449
  });
450
+ it('should render a read-only identifier field even with advancedFields field in the manifest', function () {
451
+ var accountWithCipher = _objectSpread(_objectSpread({}, fixtures.account), {}, {
452
+ relationships: {
453
+ vaultCipher: {
454
+ _id: 'fake-cipher-id',
455
+ _type: 'com.bitwarden.ciphers',
456
+ _protocol: 'bitwarden'
457
+ }
458
+ }
459
+ });
460
+
461
+ var flowState = {};
462
+ var wrapper = mount( /*#__PURE__*/React.createElement(I18n, {
463
+ lang: "en",
464
+ dictRequire: function dictRequire() {}
465
+ }, /*#__PURE__*/React.createElement(AccountForm, {
466
+ flowState: flowState,
467
+ t: t,
468
+ konnector: fixtures.konnectorWithAdvancedField,
469
+ onSubmit: onSubmit,
470
+ account: accountWithCipher,
471
+ readOnlyIdentifier: true,
472
+ fieldOptions: {}
473
+ })), {
474
+ context: {
475
+ t: t
476
+ },
477
+ childContextTypes: {
478
+ t: PropTypes.func
479
+ }
480
+ });
481
+ var hiddenInput = wrapper.find('input[type="hidden"][name="username"]');
482
+ expect(hiddenInput).toHaveLength(1);
483
+ });
437
484
  });
438
485
  describe('fieldOptions', function () {
439
486
  var setup = function setup(fieldOptions) {
@@ -4,6 +4,7 @@ import { Switch, Route, Redirect } from 'react-router';
4
4
  import { withStyles } from '@material-ui/core/styles';
5
5
  import Dialog from 'cozy-ui/transpiled/react/Dialog';
6
6
  import { DialogCloseButton, useCozyDialog } from 'cozy-ui/transpiled/react/CozyDialogs';
7
+ import { useVaultUnlockContext, VaultUnlockProvider, VaultUnlockPlaceholder } from 'cozy-keys-lib';
7
8
  import KonnectorAccounts from './KonnectorAccounts';
8
9
  import AccountModal from './AccountModal';
9
10
  import NewAccountModal from './NewAccountModal';
@@ -14,7 +15,7 @@ import HarvestVaultProvider from './HarvestVaultProvider';
14
15
  import { MountPointProvider } from './MountPointContext';
15
16
  import DialogContext from './DialogContext';
16
17
  import { DatacardOptions } from './Datacards/DatacardOptionsContext';
17
- import { useVaultUnlockContext, VaultUnlockProvider, VaultUnlockPlaceholder } from 'cozy-keys-lib';
18
+ import { ViewerModal } from '../datacards/ViewerModal';
18
19
  /**
19
20
  * Dialog will not be centered vertically since we need the modal to "stay in place"
20
21
  * when changing tabs. Since tabs content's height is not the same between the data
@@ -98,6 +99,12 @@ var Routes = function Routes(_ref) {
98
99
  accounts: accountsAndTriggers
99
100
  });
100
101
  }
102
+ }), /*#__PURE__*/React.createElement(Route, {
103
+ path: "".concat(konnectorRoot, "/viewer/:accountId/:folderToSaveId/:fileIndex"),
104
+ exact: true,
105
+ render: function render(routeComponentProps) {
106
+ return /*#__PURE__*/React.createElement(ViewerModal, routeComponentProps);
107
+ }
101
108
  }), /*#__PURE__*/React.createElement(Route, {
102
109
  path: "".concat(konnectorRoot, "/new"),
103
110
  exact: true,
@@ -1,10 +1,7 @@
1
- import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
1
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
- import React, { useState, useMemo } from 'react';
2
+ import React, { useContext, useState } from 'react';
4
3
  import PropTypes from 'prop-types';
5
4
  import get from 'lodash/get';
6
- import sortBy from 'lodash/sortBy';
7
- import uniq from 'lodash/uniq';
8
5
  import keyBy from 'lodash/keyBy';
9
6
  import 'leaflet/dist/leaflet.css';
10
7
  import Skeleton from '@material-ui/lab/Skeleton';
@@ -12,13 +9,11 @@ import List from '@material-ui/core/List';
12
9
  import ListItem from '@material-ui/core/ListItem';
13
10
  import ListItemIcon from '@material-ui/core/ListItemIcon';
14
11
  import Slide from '@material-ui/core/Slide';
15
- import Modal from '@material-ui/core/Modal';
16
12
  import ListItemText from 'cozy-ui/transpiled/react/ListItemText';
13
+ import { RealTimeQueries } from 'cozy-client';
17
14
  import palette from 'cozy-ui/transpiled/react/palette';
18
15
  import { Media, Bd, Img } from 'cozy-ui/transpiled/react/Media';
19
16
  import Circle from 'cozy-ui/transpiled/react/Circle';
20
- import Portal from 'cozy-ui/transpiled/react/Portal';
21
- import Viewer from 'cozy-ui/transpiled/react/Viewer';
22
17
  import Card from 'cozy-ui/transpiled/react/Card';
23
18
  import Icon from 'cozy-ui/transpiled/react/Icon';
24
19
  import FileIcon from 'cozy-ui/transpiled/react/Icons/File';
@@ -26,8 +21,9 @@ import Typography from 'cozy-ui/transpiled/react/Typography';
26
21
  import { useI18n } from 'cozy-ui/transpiled/react/I18n';
27
22
  import AppLinkCard, { AppLinkButton } from '../components/cards/AppLinkCard';
28
23
  import appLinksProps from '../components/KonnectorConfiguration/DataTab/appLinksProps';
29
- import CozyClient, { Q, queryConnect, isQueryLoading, hasQueryBeenLoaded, RealTimeQueries } from 'cozy-client';
24
+ import { MountPointContext } from '../components/MountPointContext';
30
25
  import { getFileIcon } from './mime-utils';
26
+ import { useDataCardFiles } from './useDataCardFiles';
31
27
 
32
28
  var LoadingFileListItem = function LoadingFileListItem(_ref) {
33
29
  var divider = _ref.divider;
@@ -85,10 +81,14 @@ var FileCard = function FileCard(_ref4) {
85
81
  var files = _ref4.files,
86
82
  loading = _ref4.loading,
87
83
  konnector = _ref4.konnector,
88
- trigger = _ref4.trigger;
84
+ trigger = _ref4.trigger,
85
+ accountId = _ref4.accountId;
89
86
 
90
87
  var _useI18n2 = useI18n(),
91
- t = _useI18n2.t; // Remember files that were there initially so that we do not
88
+ t = _useI18n2.t;
89
+
90
+ var _useContext = useContext(MountPointContext),
91
+ pushHistory = _useContext.pushHistory; // Remember files that were there initially so that we do not
92
92
  // animate their ListItem.
93
93
  // Only files coming from realtime and that are added to files
94
94
  // while the component is mounted will be animated.
@@ -102,19 +102,6 @@ var FileCard = function FileCard(_ref4) {
102
102
  _useState2 = _slicedToArray(_useState, 1),
103
103
  initialFilesById = _useState2[0];
104
104
 
105
- var _useState3 = useState(null),
106
- _useState4 = _slicedToArray(_useState3, 2),
107
- viewerIndex = _useState4[0],
108
- setViewerIndex = _useState4[1];
109
-
110
- var handleCloseViewer = function handleCloseViewer() {
111
- return setViewerIndex(null);
112
- };
113
-
114
- var handleFileChange = function handleFileChange(file, newIndex) {
115
- return setViewerIndex(newIndex);
116
- };
117
-
118
105
  return /*#__PURE__*/React.createElement(Card, {
119
106
  className: "u-ph-0 u-pb-0 u-ov-hidden"
120
107
  }, /*#__PURE__*/React.createElement("div", {
@@ -149,21 +136,12 @@ var FileCard = function FileCard(_ref4) {
149
136
  key: file._id
150
137
  }, /*#__PURE__*/React.createElement(FileListItem, {
151
138
  onClick: function onClick() {
152
- return setViewerIndex(i);
139
+ pushHistory("/viewer/".concat(accountId, "/").concat(get(trigger, 'message.folder_to_save'), "/").concat(i));
153
140
  },
154
141
  file: file,
155
142
  divider: i !== files.length - 1
156
143
  }));
157
- })), viewerIndex !== null && /*#__PURE__*/React.createElement(Portal, {
158
- into: "body"
159
- }, /*#__PURE__*/React.createElement(Modal, {
160
- open: true
161
- }, /*#__PURE__*/React.createElement(Viewer, {
162
- files: files,
163
- currentIndex: viewerIndex,
164
- onCloseRequest: handleCloseViewer,
165
- onChangeRequest: handleFileChange
166
- }))), /*#__PURE__*/React.createElement("div", {
144
+ })), /*#__PURE__*/React.createElement("div", {
167
145
  className: "u-ta-right u-mv-half u-mh-1"
168
146
  }, /*#__PURE__*/React.createElement(AppLinkButton, {
169
147
  slug: "drive",
@@ -171,63 +149,27 @@ var FileCard = function FileCard(_ref4) {
171
149
  })));
172
150
  };
173
151
 
174
- var makeFolderToSaveQueryFromProps = function makeFolderToSaveQueryFromProps(_ref5) {
175
- var trigger = _ref5.trigger;
176
- return {
177
- query: Q('io.cozy.files').where({
178
- dir_id: trigger.message.folder_to_save,
179
- trashed: false
180
- }).indexFields(['dir_id', 'cozyMetadata.createdAt']).sortBy([{
181
- dir_id: 'desc'
182
- }, {
183
- 'cozyMetadata.createdAt': 'desc'
184
- }]).limitBy(5),
185
- as: "fileDataCard_io.cozy.files/".concat(trigger.message.folder_to_save, "/io.cozy.files"),
186
- fetchPolicy: CozyClient.fetchPolicies.olderThan(30 * 1000)
187
- };
188
- };
152
+ var FileDataCard = function FileDataCard(_ref5) {
153
+ var accountId = _ref5.accountId,
154
+ konnector = _ref5.konnector,
155
+ trigger = _ref5.trigger;
189
156
 
190
- var makeSourceAccountQueryFromProps = function makeSourceAccountQueryFromProps(_ref6) {
191
- var accountId = _ref6.accountId;
192
- return {
193
- query: Q('io.cozy.files').where({
194
- 'cozyMetadata.sourceAccount': accountId,
195
- trashed: false
196
- }).indexFields(['cozyMetadata.sourceAccount', 'cozyMetadata.createdAt']).sortBy([{
197
- 'cozyMetadata.sourceAccount': 'desc'
198
- }, {
199
- 'cozyMetadata.createdAt': 'desc'
200
- }]).limitBy(5),
201
- as: "fileDataCard_io.cozy.accounts/".concat(accountId, "/io.cozy.files"),
202
- fetchPolicy: CozyClient.fetchPolicies.olderThan(30 * 1000)
203
- };
204
- };
157
+ var _useDataCardFiles = useDataCardFiles(accountId, trigger.message.folder_to_save),
158
+ data = _useDataCardFiles.data,
159
+ fetchStatus = _useDataCardFiles.fetchStatus;
205
160
 
206
- var FileDataCard = function FileDataCard(_ref7) {
207
- var folderToSaveFiles = _ref7.folderToSaveFiles,
208
- sourceAccountFiles = _ref7.sourceAccountFiles,
209
- konnector = _ref7.konnector,
210
- trigger = _ref7.trigger;
211
- var files1 = folderToSaveFiles.data;
212
- var files2 = sourceAccountFiles.data;
213
- var noFiles = hasQueryBeenLoaded(folderToSaveFiles) && files1.length === 0 && hasQueryBeenLoaded(sourceAccountFiles) && files2.length === 0;
214
- var isLoading = isQueryLoading(folderToSaveFiles) || isQueryLoading(sourceAccountFiles);
215
- var files = useMemo(function () {
216
- return sortBy(uniq([].concat(_toConsumableArray(files1), _toConsumableArray(files2)), function (x) {
217
- return x._id;
218
- }), function (x) {
219
- return get(x, 'cozyMetadata.createdAt');
220
- }).reverse().slice(0, 5);
221
- }, [files1, files2]);
161
+ var isLoading = fetchStatus === 'loading';
162
+ var noFiles = fetchStatus === 'empty' || fetchStatus === 'failed';
222
163
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(RealTimeQueries, {
223
164
  doctype: "io.cozy.files"
224
165
  }), noFiles ? /*#__PURE__*/React.createElement(AppLinkCard, appLinksProps.drive({
225
166
  trigger: trigger
226
167
  })) : /*#__PURE__*/React.createElement(FileCard, {
227
- files: files,
168
+ files: data,
228
169
  loading: isLoading,
229
170
  konnector: konnector,
230
- trigger: trigger
171
+ trigger: trigger,
172
+ accountId: accountId
231
173
  }));
232
174
  };
233
175
 
@@ -236,11 +178,4 @@ FileDataCard.propTypes = {
236
178
  accountId: PropTypes.string.isRequired,
237
179
  trigger: PropTypes.object.isRequired
238
180
  };
239
- export default queryConnect({
240
- folderToSaveFiles: function folderToSaveFiles(props) {
241
- return makeFolderToSaveQueryFromProps(props);
242
- },
243
- sourceAccountFiles: function sourceAccountFiles(props) {
244
- return makeSourceAccountQueryFromProps(props);
245
- }
246
- })(FileDataCard);
181
+ export default FileDataCard;
@@ -0,0 +1,40 @@
1
+ import React, { useContext } from 'react';
2
+ import Overlay from 'cozy-ui/transpiled/react/Overlay';
3
+ import Viewer from 'cozy-ui/transpiled/react/Viewer';
4
+ import { MountPointContext } from '../components/MountPointContext';
5
+ import { useDataCardFiles } from './useDataCardFiles';
6
+ export var ViewerModal = function ViewerModal(_ref) {
7
+ var _ref$match$params = _ref.match.params,
8
+ accountId = _ref$match$params.accountId,
9
+ folderToSaveId = _ref$match$params.folderToSaveId,
10
+ fileIndex = _ref$match$params.fileIndex;
11
+
12
+ var _useContext = useContext(MountPointContext),
13
+ pushHistory = _useContext.pushHistory,
14
+ replaceHistory = _useContext.replaceHistory;
15
+
16
+ var _useDataCardFiles = useDataCardFiles(accountId, folderToSaveId),
17
+ data = _useDataCardFiles.data,
18
+ fetchStatus = _useDataCardFiles.fetchStatus;
19
+
20
+ var handleCloseViewer = function handleCloseViewer() {
21
+ return replaceHistory("/accounts");
22
+ };
23
+
24
+ var handleFileChange = function handleFileChange(_file, newIndex) {
25
+ return pushHistory("/viewer/".concat(accountId, "/").concat(folderToSaveId, "/").concat(newIndex));
26
+ };
27
+
28
+ if (fetchStatus === 'empty' || fetchStatus === 'failed' || fetchStatus === 'loaded' && fileIndex > data.length) {
29
+ handleCloseViewer();
30
+ return null;
31
+ }
32
+
33
+ if (fetchStatus === 'loading') return /*#__PURE__*/React.createElement(Overlay, null);
34
+ return /*#__PURE__*/React.createElement(Overlay, null, /*#__PURE__*/React.createElement(Viewer, {
35
+ files: data,
36
+ currentIndex: Number(fileIndex),
37
+ onCloseRequest: handleCloseViewer,
38
+ onChangeRequest: handleFileChange
39
+ }));
40
+ };
@@ -0,0 +1,63 @@
1
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
+ import { useMemo } from 'react';
3
+ import get from 'lodash/get';
4
+ import sortBy from 'lodash/sortBy';
5
+ import uniq from 'lodash/uniq';
6
+ import CozyClient, { Q, useQuery, hasQueryBeenLoaded } from 'cozy-client';
7
+
8
+ var useFolderToSaveFiles = function useFolderToSaveFiles(folderToSaveId) {
9
+ return useQuery(Q('io.cozy.files').where({
10
+ dir_id: folderToSaveId,
11
+ trashed: false
12
+ }).indexFields(['dir_id', 'cozyMetadata.createdAt']).sortBy([{
13
+ dir_id: 'desc'
14
+ }, {
15
+ 'cozyMetadata.createdAt': 'desc'
16
+ }]).limitBy(5), {
17
+ as: "fileDataCard_io.cozy.files/".concat(folderToSaveId, "/io.cozy.files"),
18
+ fetchPolicy: CozyClient.fetchPolicies.olderThan(30 * 1000)
19
+ });
20
+ };
21
+
22
+ var useSourceAccountFiles = function useSourceAccountFiles(accountId) {
23
+ return useQuery(Q('io.cozy.files').where({
24
+ 'cozyMetadata.sourceAccount': accountId,
25
+ trashed: false
26
+ }).indexFields(['cozyMetadata.sourceAccount', 'cozyMetadata.createdAt']).sortBy([{
27
+ 'cozyMetadata.sourceAccount': 'desc'
28
+ }, {
29
+ 'cozyMetadata.createdAt': 'desc'
30
+ }]).limitBy(5), {
31
+ as: "fileDataCard_io.cozy.accounts/".concat(accountId, "/io.cozy.files"),
32
+ fetchPolicy: CozyClient.fetchPolicies.olderThan(30 * 1000)
33
+ });
34
+ };
35
+
36
+ var getResponse = function getResponse(folderToSaveFiles, sourceAccountFiles) {
37
+ var loaded = Boolean(hasQueryBeenLoaded(folderToSaveFiles) && hasQueryBeenLoaded(sourceAccountFiles));
38
+ if (folderToSaveFiles.fetchStatus === 'failed' && sourceAccountFiles === 'failed') return {
39
+ fetchStatus: 'failed'
40
+ };
41
+ if (loaded && folderToSaveFiles.data.length === 0 && sourceAccountFiles.data.length === 0) return {
42
+ fetchStatus: 'empty'
43
+ };
44
+ if (loaded) return {
45
+ data: sortBy(uniq([].concat(_toConsumableArray(folderToSaveFiles.data), _toConsumableArray(sourceAccountFiles.data)), function (x) {
46
+ return x._id;
47
+ }), function (x) {
48
+ return get(x, 'cozyMetadata.createdAt');
49
+ }).reverse().slice(0, 5),
50
+ fetchStatus: 'loaded'
51
+ };
52
+ return {
53
+ fetchStatus: 'loading'
54
+ };
55
+ };
56
+
57
+ export var useDataCardFiles = function useDataCardFiles(accountId, folderToSaveId) {
58
+ var folderToSaveFiles = useFolderToSaveFiles(folderToSaveId);
59
+ var sourceAccountFiles = useSourceAccountFiles(accountId);
60
+ return useMemo(function () {
61
+ return getResponse(folderToSaveFiles, sourceAccountFiles);
62
+ }, [folderToSaveFiles, sourceAccountFiles]);
63
+ };
@@ -0,0 +1,242 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+
3
+ 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; }
4
+
5
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
6
+
7
+ import { renderHook } from '@testing-library/react-hooks';
8
+ import { useDataCardFiles } from './useDataCardFiles';
9
+ var mockUseQuery = jest.fn();
10
+ var mockFile1 = {
11
+ cozyMetadata: {
12
+ createdAt: '1970-01-01T00:00:11Z'
13
+ }
14
+ };
15
+ var mockFile2 = {
16
+ cozyMetadata: {
17
+ createdAt: '1970-01-01T00:00:10Z'
18
+ }
19
+ };
20
+ var mockFile3 = {
21
+ cozyMetadata: {
22
+ createdAt: '1970-01-01T00:00:09Z'
23
+ }
24
+ };
25
+ var mockFile4 = {
26
+ cozyMetadata: {
27
+ createdAt: '1970-01-01T00:00:08Z'
28
+ }
29
+ };
30
+ var mockFile5 = {
31
+ cozyMetadata: {
32
+ createdAt: '1970-01-01T00:00:07Z'
33
+ }
34
+ };
35
+ var mockFile6 = {
36
+ cozyMetadata: {
37
+ createdAt: '1970-01-01T00:00:06Z'
38
+ }
39
+ };
40
+ var mockFile7 = {
41
+ cozyMetadata: {
42
+ createdAt: '1970-01-01T00:00:05Z'
43
+ }
44
+ };
45
+ var mockFile8 = {
46
+ cozyMetadata: {
47
+ createdAt: '1970-01-01T00:00:04Z'
48
+ }
49
+ };
50
+ var mockFile9 = {
51
+ cozyMetadata: {
52
+ createdAt: '1970-01-01T00:00:03Z'
53
+ }
54
+ };
55
+ var mockFile10 = {
56
+ cozyMetadata: {
57
+ createdAt: '1970-01-01T00:00:02Z'
58
+ }
59
+ };
60
+ var mockFile11 = {
61
+ cozyMetadata: {
62
+ createdAt: '1970-01-01T00:00:01Z'
63
+ }
64
+ };
65
+ var mockFile12 = {
66
+ cozyMetadata: {
67
+ createdAt: '1970-01-01T00:00:00Z'
68
+ }
69
+ };
70
+ jest.mock('cozy-client', function () {
71
+ return _objectSpread(_objectSpread({}, jest.requireActual('cozy-client')), {}, {
72
+ useQuery: function useQuery() {
73
+ return mockUseQuery();
74
+ }
75
+ });
76
+ });
77
+ afterEach(function () {
78
+ mockUseQuery.mockClear();
79
+ });
80
+ it('handles files pending', function () {
81
+ mockUseQuery.mockReturnValueOnce({
82
+ fetchStatus: 'pending',
83
+ data: null
84
+ });
85
+ mockUseQuery.mockReturnValueOnce({
86
+ fetchStatus: 'pending',
87
+ data: null
88
+ });
89
+
90
+ var _renderHook = renderHook(function () {
91
+ return useDataCardFiles('1', '2');
92
+ }),
93
+ result = _renderHook.result;
94
+
95
+ expect(result.current).toStrictEqual({
96
+ fetchStatus: 'loading'
97
+ });
98
+ });
99
+ it('handles files loading', function () {
100
+ mockUseQuery.mockReturnValueOnce({
101
+ fetchStatus: 'loading',
102
+ data: null
103
+ });
104
+ mockUseQuery.mockReturnValueOnce({
105
+ fetchStatus: 'loading',
106
+ data: null
107
+ });
108
+
109
+ var _renderHook2 = renderHook(function () {
110
+ return useDataCardFiles('1', '2');
111
+ }),
112
+ result = _renderHook2.result;
113
+
114
+ expect(result.current).toStrictEqual({
115
+ fetchStatus: 'loading'
116
+ });
117
+ });
118
+ it('handles files loading with partial data', function () {
119
+ mockUseQuery.mockReturnValueOnce({
120
+ fetchStatus: 'loading',
121
+ data: [mockFile1]
122
+ });
123
+ mockUseQuery.mockReturnValueOnce({
124
+ fetchStatus: 'loading',
125
+ data: [mockFile2]
126
+ });
127
+
128
+ var _renderHook3 = renderHook(function () {
129
+ return useDataCardFiles('1', '2');
130
+ }),
131
+ result = _renderHook3.result;
132
+
133
+ expect(result.current).toStrictEqual({
134
+ fetchStatus: 'loading'
135
+ });
136
+ });
137
+ it('handles files loading with more partial data', function () {
138
+ mockUseQuery.mockReturnValueOnce({
139
+ fetchStatus: 'loading',
140
+ data: [mockFile1, mockFile2]
141
+ });
142
+ mockUseQuery.mockReturnValueOnce({
143
+ fetchStatus: 'loading',
144
+ data: [mockFile3, mockFile4]
145
+ });
146
+
147
+ var _renderHook4 = renderHook(function () {
148
+ return useDataCardFiles('1', '2');
149
+ }),
150
+ result = _renderHook4.result;
151
+
152
+ expect(result.current).toStrictEqual({
153
+ fetchStatus: 'loading'
154
+ });
155
+ });
156
+ it('handles files loading with empty data', function () {
157
+ mockUseQuery.mockReturnValueOnce({
158
+ fetchStatus: 'loaded',
159
+ data: [],
160
+ lastFetch: 1
161
+ });
162
+ mockUseQuery.mockReturnValueOnce({
163
+ fetchStatus: 'loaded',
164
+ data: [],
165
+ lastFetch: 1
166
+ });
167
+
168
+ var _renderHook5 = renderHook(function () {
169
+ return useDataCardFiles('1', '2');
170
+ }),
171
+ result = _renderHook5.result;
172
+
173
+ expect(result.current).toStrictEqual({
174
+ fetchStatus: 'empty'
175
+ });
176
+ });
177
+ it('handles files loaded and return in correct order', function () {
178
+ mockUseQuery.mockReturnValueOnce({
179
+ fetchStatus: 'loaded',
180
+ data: [mockFile2, mockFile1],
181
+ lastFetch: 1
182
+ });
183
+ mockUseQuery.mockReturnValueOnce({
184
+ fetchStatus: 'loaded',
185
+ data: [mockFile3, mockFile4],
186
+ lastFetch: 1
187
+ });
188
+
189
+ var _renderHook6 = renderHook(function () {
190
+ return useDataCardFiles('1', '2');
191
+ }),
192
+ result = _renderHook6.result;
193
+
194
+ expect(result.current).toStrictEqual({
195
+ data: [mockFile1, mockFile2, mockFile3, mockFile4],
196
+ fetchStatus: 'loaded'
197
+ });
198
+ });
199
+ it('handles files loaded with identical double return', function () {
200
+ mockUseQuery.mockReturnValueOnce({
201
+ fetchStatus: 'loaded',
202
+ data: [mockFile1, mockFile2, mockFile3, mockFile4],
203
+ lastFetch: 1
204
+ });
205
+ mockUseQuery.mockReturnValueOnce({
206
+ fetchStatus: 'loaded',
207
+ data: [mockFile3, mockFile2, mockFile3, mockFile4],
208
+ lastFetch: 1
209
+ });
210
+
211
+ var _renderHook7 = renderHook(function () {
212
+ return useDataCardFiles('1', '2');
213
+ }),
214
+ result = _renderHook7.result;
215
+
216
+ expect(result.current).toStrictEqual({
217
+ data: [mockFile1, mockFile2, mockFile3, mockFile4],
218
+ fetchStatus: 'loaded'
219
+ });
220
+ });
221
+ it('handles files loaded with more than 5 result', function () {
222
+ mockUseQuery.mockReturnValueOnce({
223
+ fetchStatus: 'loaded',
224
+ data: [mockFile1, mockFile2, mockFile3, mockFile4, mockFile5, mockFile6],
225
+ lastFetch: 1
226
+ });
227
+ mockUseQuery.mockReturnValueOnce({
228
+ fetchStatus: 'loaded',
229
+ data: [mockFile7, mockFile8, mockFile9, mockFile10, mockFile11, mockFile12],
230
+ lastFetch: 1
231
+ });
232
+
233
+ var _renderHook8 = renderHook(function () {
234
+ return useDataCardFiles('1', '2');
235
+ }),
236
+ result = _renderHook8.result;
237
+
238
+ expect(result.current).toStrictEqual({
239
+ data: [mockFile1, mockFile2, mockFile3, mockFile4, mockFile5],
240
+ fetchStatus: 'loaded'
241
+ });
242
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-harvest-lib",
3
- "version": "8.2.0",
3
+ "version": "8.3.1",
4
4
  "description": "Provides logic, modules and components for Cozy's harvest applications.",
5
5
  "main": "dist/index.js",
6
6
  "author": "Cozy",
@@ -28,7 +28,7 @@
28
28
  "dependencies": {
29
29
  "@cozy/minilog": "^1.0.0",
30
30
  "@sentry/browser": "^6.0.1",
31
- "cozy-bi-auth": "0.0.24",
31
+ "cozy-bi-auth": "0.0.25",
32
32
  "cozy-doctypes": "^1.83.7",
33
33
  "cozy-logger": "^1.9.0",
34
34
  "date-fns": "^1.30.1",
@@ -85,5 +85,5 @@
85
85
  "react-router-dom": "^5.0.1"
86
86
  },
87
87
  "sideEffects": false,
88
- "gitHead": "89a33de8f811a4e1a1525f48caddd5368672dba2"
88
+ "gitHead": "aea5ca9cbfb60d5b3bb8318939087952adbe8522"
89
89
  }
@@ -132,7 +132,7 @@ export class AccountForm extends PureComponent {
132
132
  handleSubmit(values, form) {
133
133
  const { account, konnector } = this.props
134
134
 
135
- const identifier = manifest.getIdentifier(konnector.fields)
135
+ const identifier = manifest.getIdentifier(manifest.sanitizeFields(konnector.fields))
136
136
  if (
137
137
  account &&
138
138
  account.auth[identifier] &&
@@ -228,7 +228,7 @@ export class AccountForm extends PureComponent {
228
228
  this.props.readOnlyIdentifier
229
229
 
230
230
  if (isReadOnlyIdentifier) {
231
- const identifier = manifest.getIdentifier(fields)
231
+ const identifier = manifest.getIdentifier(sanitizedFields)
232
232
  sanitizedFields[identifier].type = 'hidden'
233
233
  }
234
234
 
@@ -268,7 +268,7 @@ export class AccountForm extends PureComponent {
268
268
  konnector={konnector}
269
269
  identifier={get(
270
270
  account,
271
- `auth.${manifest.getIdentifier(konnector.fields)}`
271
+ `auth.${manifest.getIdentifier(sanitizedFields)}`
272
272
  )}
273
273
  />
274
274
  )}
@@ -49,6 +49,19 @@ const fixtures = {
49
49
  }
50
50
  }
51
51
  },
52
+ konnectorWithAdvancedField: {
53
+ fields: {
54
+ advancedFields: {
55
+ folderPath: {
56
+ advanced: true,
57
+ isRequired: false
58
+ }
59
+ },
60
+ username: {
61
+ type: 'text'
62
+ }
63
+ }
64
+ },
52
65
  account: {
53
66
  auth: {
54
67
  username: 'Toto',
@@ -386,6 +399,42 @@ describe('AccountForm', () => {
386
399
 
387
400
  const hiddenInput = wrapper.find('input[type="hidden"][name="username"]')
388
401
 
402
+ expect(hiddenInput).toHaveLength(1)
403
+ })
404
+ it('should render a read-only identifier field even with advancedFields field in the manifest', () => {
405
+ const accountWithCipher = {
406
+ ...fixtures.account,
407
+ relationships: {
408
+ vaultCipher: {
409
+ _id: 'fake-cipher-id',
410
+ _type: 'com.bitwarden.ciphers',
411
+ _protocol: 'bitwarden'
412
+ }
413
+ }
414
+ }
415
+ const flowState = {}
416
+ const wrapper = mount(
417
+ <I18n lang="en" dictRequire={() => {}}>
418
+ <AccountForm
419
+ flowState={flowState}
420
+ t={t}
421
+ konnector={fixtures.konnectorWithAdvancedField}
422
+ onSubmit={onSubmit}
423
+ account={accountWithCipher}
424
+ readOnlyIdentifier={true}
425
+ fieldOptions={{}}
426
+ />
427
+ </I18n>,
428
+ {
429
+ context: { t },
430
+ childContextTypes: {
431
+ t: PropTypes.func
432
+ }
433
+ }
434
+ )
435
+
436
+ const hiddenInput = wrapper.find('input[type="hidden"][name="username"]')
437
+
389
438
  expect(hiddenInput).toHaveLength(1)
390
439
  })
391
440
  })
@@ -7,6 +7,11 @@ import {
7
7
  DialogCloseButton,
8
8
  useCozyDialog
9
9
  } from 'cozy-ui/transpiled/react/CozyDialogs'
10
+ import {
11
+ useVaultUnlockContext,
12
+ VaultUnlockProvider,
13
+ VaultUnlockPlaceholder
14
+ } from 'cozy-keys-lib'
10
15
 
11
16
  import KonnectorAccounts from './KonnectorAccounts'
12
17
  import AccountModal from './AccountModal'
@@ -18,11 +23,8 @@ import HarvestVaultProvider from './HarvestVaultProvider'
18
23
  import { MountPointProvider } from './MountPointContext'
19
24
  import DialogContext from './DialogContext'
20
25
  import { DatacardOptions } from './Datacards/DatacardOptionsContext'
21
- import {
22
- useVaultUnlockContext,
23
- VaultUnlockProvider,
24
- VaultUnlockPlaceholder
25
- } from 'cozy-keys-lib'
26
+
27
+ import { ViewerModal } from '../datacards/ViewerModal'
26
28
 
27
29
  /**
28
30
  * Dialog will not be centered vertically since we need the modal to "stay in place"
@@ -101,6 +103,13 @@ const Routes = ({ konnectorRoot, konnector, onDismiss, datacardOptions }) => {
101
103
  />
102
104
  )}
103
105
  />
106
+ <Route
107
+ path={`${konnectorRoot}/viewer/:accountId/:folderToSaveId/:fileIndex`}
108
+ exact
109
+ render={routeComponentProps => (
110
+ <ViewerModal {...routeComponentProps} />
111
+ )}
112
+ />
104
113
  <Route
105
114
  path={`${konnectorRoot}/new`}
106
115
  exact
@@ -1,8 +1,6 @@
1
- import React, { useState, useMemo } from 'react'
1
+ import React, { useContext, useState } from 'react'
2
2
  import PropTypes from 'prop-types'
3
3
  import get from 'lodash/get'
4
- import sortBy from 'lodash/sortBy'
5
- import uniq from 'lodash/uniq'
6
4
  import keyBy from 'lodash/keyBy'
7
5
 
8
6
  import 'leaflet/dist/leaflet.css'
@@ -12,15 +10,13 @@ import List from '@material-ui/core/List'
12
10
  import ListItem from '@material-ui/core/ListItem'
13
11
  import ListItemIcon from '@material-ui/core/ListItemIcon'
14
12
  import Slide from '@material-ui/core/Slide'
15
- import Modal from '@material-ui/core/Modal'
16
13
 
17
14
  import ListItemText from 'cozy-ui/transpiled/react/ListItemText'
18
15
 
16
+ import { RealTimeQueries } from 'cozy-client'
19
17
  import palette from 'cozy-ui/transpiled/react/palette'
20
18
  import { Media, Bd, Img } from 'cozy-ui/transpiled/react/Media'
21
19
  import Circle from 'cozy-ui/transpiled/react/Circle'
22
- import Portal from 'cozy-ui/transpiled/react/Portal'
23
- import Viewer from 'cozy-ui/transpiled/react/Viewer'
24
20
  import Card from 'cozy-ui/transpiled/react/Card'
25
21
  import Icon from 'cozy-ui/transpiled/react/Icon'
26
22
  import FileIcon from 'cozy-ui/transpiled/react/Icons/File'
@@ -29,16 +25,9 @@ import { useI18n } from 'cozy-ui/transpiled/react/I18n'
29
25
 
30
26
  import AppLinkCard, { AppLinkButton } from '../components/cards/AppLinkCard'
31
27
  import appLinksProps from '../components/KonnectorConfiguration/DataTab/appLinksProps'
32
-
33
- import CozyClient, {
34
- Q,
35
- queryConnect,
36
- isQueryLoading,
37
- hasQueryBeenLoaded,
38
- RealTimeQueries
39
- } from 'cozy-client'
40
-
28
+ import { MountPointContext } from '../components/MountPointContext'
41
29
  import { getFileIcon } from './mime-utils'
30
+ import { useDataCardFiles } from './useDataCardFiles'
42
31
 
43
32
  const LoadingFileListItem = ({ divider }) => {
44
33
  return (
@@ -87,17 +76,15 @@ const TransitionWrapper = ({ children }) => {
87
76
  )
88
77
  }
89
78
 
90
- const FileCard = ({ files, loading, konnector, trigger }) => {
79
+ const FileCard = ({ files, loading, konnector, trigger, accountId }) => {
91
80
  const { t } = useI18n()
81
+ const { pushHistory } = useContext(MountPointContext)
92
82
 
93
83
  // Remember files that were there initially so that we do not
94
84
  // animate their ListItem.
95
85
  // Only files coming from realtime and that are added to files
96
86
  // while the component is mounted will be animated.
97
87
  const [initialFilesById] = useState(() => keyBy(files, x => x._id))
98
- const [viewerIndex, setViewerIndex] = useState(null)
99
- const handleCloseViewer = () => setViewerIndex(null)
100
- const handleFileChange = (file, newIndex) => setViewerIndex(newIndex)
101
88
 
102
89
  return (
103
90
  <Card className="u-ph-0 u-pb-0 u-ov-hidden">
@@ -138,7 +125,14 @@ const FileCard = ({ files, loading, konnector, trigger }) => {
138
125
  return (
139
126
  <ItemWrapper key={file._id}>
140
127
  <FileListItem
141
- onClick={() => setViewerIndex(i)}
128
+ onClick={() => {
129
+ pushHistory(
130
+ `/viewer/${accountId}/${get(
131
+ trigger,
132
+ 'message.folder_to_save'
133
+ )}/${i}`
134
+ )
135
+ }}
142
136
  file={file}
143
137
  divider={i !== files.length - 1}
144
138
  />
@@ -147,18 +141,6 @@ const FileCard = ({ files, loading, konnector, trigger }) => {
147
141
  })
148
142
  )}
149
143
  </List>
150
- {viewerIndex !== null && (
151
- <Portal into="body">
152
- <Modal open={true}>
153
- <Viewer
154
- files={files}
155
- currentIndex={viewerIndex}
156
- onCloseRequest={handleCloseViewer}
157
- onChangeRequest={handleFileChange}
158
- />
159
- </Modal>
160
- </Portal>
161
- )}
162
144
  <div className="u-ta-right u-mv-half u-mh-1">
163
145
  <AppLinkButton
164
146
  slug="drive"
@@ -169,60 +151,14 @@ const FileCard = ({ files, loading, konnector, trigger }) => {
169
151
  )
170
152
  }
171
153
 
172
- const makeFolderToSaveQueryFromProps = ({ trigger }) => ({
173
- query: Q('io.cozy.files')
174
- .where({
175
- dir_id: trigger.message.folder_to_save,
176
- trashed: false
177
- })
178
- .indexFields(['dir_id', 'cozyMetadata.createdAt'])
179
- .sortBy([{ dir_id: 'desc' }, { 'cozyMetadata.createdAt': 'desc' }])
180
- .limitBy(5),
181
- as: `fileDataCard_io.cozy.files/${trigger.message.folder_to_save}/io.cozy.files`,
182
- fetchPolicy: CozyClient.fetchPolicies.olderThan(30 * 1000)
183
- })
184
-
185
- const makeSourceAccountQueryFromProps = ({ accountId }) => ({
186
- query: Q('io.cozy.files')
187
- .where({
188
- 'cozyMetadata.sourceAccount': accountId,
189
- trashed: false
190
- })
191
- .indexFields(['cozyMetadata.sourceAccount', 'cozyMetadata.createdAt'])
192
- .sortBy([
193
- { 'cozyMetadata.sourceAccount': 'desc' },
194
- { 'cozyMetadata.createdAt': 'desc' }
195
- ])
196
- .limitBy(5),
197
- as: `fileDataCard_io.cozy.accounts/${accountId}/io.cozy.files`,
198
- fetchPolicy: CozyClient.fetchPolicies.olderThan(30 * 1000)
199
- })
200
-
201
- const FileDataCard = ({
202
- folderToSaveFiles,
203
- sourceAccountFiles,
204
- konnector,
205
- trigger
206
- }) => {
207
- const { data: files1 } = folderToSaveFiles
208
- const { data: files2 } = sourceAccountFiles
209
-
210
- const noFiles =
211
- hasQueryBeenLoaded(folderToSaveFiles) &&
212
- files1.length === 0 &&
213
- hasQueryBeenLoaded(sourceAccountFiles) &&
214
- files2.length === 0
215
- const isLoading =
216
- isQueryLoading(folderToSaveFiles) || isQueryLoading(sourceAccountFiles)
154
+ const FileDataCard = ({ accountId, konnector, trigger }) => {
155
+ const { data, fetchStatus } = useDataCardFiles(
156
+ accountId,
157
+ trigger.message.folder_to_save
158
+ )
159
+ const isLoading = fetchStatus === 'loading'
160
+ const noFiles = fetchStatus === 'empty' || fetchStatus === 'failed'
217
161
 
218
- const files = useMemo(() => {
219
- return sortBy(
220
- uniq([...files1, ...files2], x => x._id),
221
- x => get(x, 'cozyMetadata.createdAt')
222
- )
223
- .reverse()
224
- .slice(0, 5)
225
- }, [files1, files2])
226
162
  return (
227
163
  <>
228
164
  <RealTimeQueries doctype="io.cozy.files" />
@@ -230,10 +166,11 @@ const FileDataCard = ({
230
166
  <AppLinkCard {...appLinksProps.drive({ trigger })} />
231
167
  ) : (
232
168
  <FileCard
233
- files={files}
169
+ files={data}
234
170
  loading={isLoading}
235
171
  konnector={konnector}
236
172
  trigger={trigger}
173
+ accountId={accountId}
237
174
  />
238
175
  )}
239
176
  </>
@@ -246,7 +183,4 @@ FileDataCard.propTypes = {
246
183
  trigger: PropTypes.object.isRequired
247
184
  }
248
185
 
249
- export default queryConnect({
250
- folderToSaveFiles: props => makeFolderToSaveQueryFromProps(props),
251
- sourceAccountFiles: props => makeSourceAccountQueryFromProps(props)
252
- })(FileDataCard)
186
+ export default FileDataCard
@@ -0,0 +1,43 @@
1
+ import React, { useContext } from 'react'
2
+
3
+ import Overlay from 'cozy-ui/transpiled/react/Overlay'
4
+ import Viewer from 'cozy-ui/transpiled/react/Viewer'
5
+
6
+ import { MountPointContext } from '../components/MountPointContext'
7
+ import { useDataCardFiles } from './useDataCardFiles'
8
+
9
+ export const ViewerModal = ({
10
+ match: {
11
+ params: { accountId, folderToSaveId, fileIndex }
12
+ }
13
+ }) => {
14
+ const { pushHistory, replaceHistory } = useContext(MountPointContext)
15
+ const { data, fetchStatus } = useDataCardFiles(accountId, folderToSaveId)
16
+
17
+ const handleCloseViewer = () => replaceHistory(`/accounts`)
18
+ const handleFileChange = (_file, newIndex) =>
19
+ pushHistory(`/viewer/${accountId}/${folderToSaveId}/${newIndex}`)
20
+
21
+ if (
22
+ fetchStatus === 'empty' ||
23
+ fetchStatus === 'failed' ||
24
+ (fetchStatus === 'loaded' && fileIndex > data.length)
25
+ ) {
26
+ handleCloseViewer()
27
+
28
+ return null
29
+ }
30
+
31
+ if (fetchStatus === 'loading') return <Overlay />
32
+
33
+ return (
34
+ <Overlay>
35
+ <Viewer
36
+ files={data}
37
+ currentIndex={Number(fileIndex)}
38
+ onCloseRequest={handleCloseViewer}
39
+ onChangeRequest={handleFileChange}
40
+ />
41
+ </Overlay>
42
+ )
43
+ }
@@ -0,0 +1,84 @@
1
+ import { useMemo } from 'react'
2
+ import get from 'lodash/get'
3
+ import sortBy from 'lodash/sortBy'
4
+ import uniq from 'lodash/uniq'
5
+
6
+ import CozyClient, { Q, useQuery, hasQueryBeenLoaded } from 'cozy-client'
7
+
8
+ const useFolderToSaveFiles = folderToSaveId =>
9
+ useQuery(
10
+ Q('io.cozy.files')
11
+ .where({
12
+ dir_id: folderToSaveId,
13
+ trashed: false
14
+ })
15
+ .indexFields(['dir_id', 'cozyMetadata.createdAt'])
16
+ .sortBy([{ dir_id: 'desc' }, { 'cozyMetadata.createdAt': 'desc' }])
17
+ .limitBy(5),
18
+ {
19
+ as: `fileDataCard_io.cozy.files/${folderToSaveId}/io.cozy.files`,
20
+ fetchPolicy: CozyClient.fetchPolicies.olderThan(30 * 1000)
21
+ }
22
+ )
23
+
24
+ const useSourceAccountFiles = accountId =>
25
+ useQuery(
26
+ Q('io.cozy.files')
27
+ .where({ 'cozyMetadata.sourceAccount': accountId, trashed: false })
28
+ .indexFields(['cozyMetadata.sourceAccount', 'cozyMetadata.createdAt'])
29
+ .sortBy([
30
+ { 'cozyMetadata.sourceAccount': 'desc' },
31
+ { 'cozyMetadata.createdAt': 'desc' }
32
+ ])
33
+ .limitBy(5),
34
+ {
35
+ as: `fileDataCard_io.cozy.accounts/${accountId}/io.cozy.files`,
36
+ fetchPolicy: CozyClient.fetchPolicies.olderThan(30 * 1000)
37
+ }
38
+ )
39
+
40
+ const getResponse = (folderToSaveFiles, sourceAccountFiles) => {
41
+ const loaded = Boolean(
42
+ hasQueryBeenLoaded(folderToSaveFiles) &&
43
+ hasQueryBeenLoaded(sourceAccountFiles)
44
+ )
45
+
46
+ if (
47
+ folderToSaveFiles.fetchStatus === 'failed' &&
48
+ sourceAccountFiles === 'failed'
49
+ )
50
+ return { fetchStatus: 'failed' }
51
+
52
+ if (
53
+ loaded &&
54
+ folderToSaveFiles.data.length === 0 &&
55
+ sourceAccountFiles.data.length === 0
56
+ )
57
+ return { fetchStatus: 'empty' }
58
+
59
+ if (loaded)
60
+ return {
61
+ data: sortBy(
62
+ uniq(
63
+ [...folderToSaveFiles.data, ...sourceAccountFiles.data],
64
+ x => x._id
65
+ ),
66
+ x => get(x, 'cozyMetadata.createdAt')
67
+ )
68
+ .reverse()
69
+ .slice(0, 5),
70
+ fetchStatus: 'loaded'
71
+ }
72
+
73
+ return { fetchStatus: 'loading' }
74
+ }
75
+
76
+ export const useDataCardFiles = (accountId, folderToSaveId) => {
77
+ const folderToSaveFiles = useFolderToSaveFiles(folderToSaveId)
78
+ const sourceAccountFiles = useSourceAccountFiles(accountId)
79
+
80
+ return useMemo(
81
+ () => getResponse(folderToSaveFiles, sourceAccountFiles),
82
+ [folderToSaveFiles, sourceAccountFiles]
83
+ )
84
+ }
@@ -0,0 +1,135 @@
1
+ import { renderHook } from '@testing-library/react-hooks'
2
+
3
+ import { useDataCardFiles } from './useDataCardFiles'
4
+
5
+ const mockUseQuery = jest.fn()
6
+ const mockFile1 = { cozyMetadata: { createdAt: '1970-01-01T00:00:11Z' } }
7
+ const mockFile2 = { cozyMetadata: { createdAt: '1970-01-01T00:00:10Z' } }
8
+ const mockFile3 = { cozyMetadata: { createdAt: '1970-01-01T00:00:09Z' } }
9
+ const mockFile4 = { cozyMetadata: { createdAt: '1970-01-01T00:00:08Z' } }
10
+ const mockFile5 = { cozyMetadata: { createdAt: '1970-01-01T00:00:07Z' } }
11
+ const mockFile6 = { cozyMetadata: { createdAt: '1970-01-01T00:00:06Z' } }
12
+ const mockFile7 = { cozyMetadata: { createdAt: '1970-01-01T00:00:05Z' } }
13
+ const mockFile8 = { cozyMetadata: { createdAt: '1970-01-01T00:00:04Z' } }
14
+ const mockFile9 = { cozyMetadata: { createdAt: '1970-01-01T00:00:03Z' } }
15
+ const mockFile10 = { cozyMetadata: { createdAt: '1970-01-01T00:00:02Z' } }
16
+ const mockFile11 = { cozyMetadata: { createdAt: '1970-01-01T00:00:01Z' } }
17
+ const mockFile12 = { cozyMetadata: { createdAt: '1970-01-01T00:00:00Z' } }
18
+
19
+ jest.mock('cozy-client', () => ({
20
+ ...jest.requireActual('cozy-client'),
21
+ useQuery: () => mockUseQuery()
22
+ }))
23
+
24
+ afterEach(() => {
25
+ mockUseQuery.mockClear()
26
+ })
27
+
28
+ it('handles files pending', () => {
29
+ mockUseQuery.mockReturnValueOnce({ fetchStatus: 'pending', data: null })
30
+ mockUseQuery.mockReturnValueOnce({ fetchStatus: 'pending', data: null })
31
+ const { result } = renderHook(() => useDataCardFiles('1', '2'))
32
+ expect(result.current).toStrictEqual({ fetchStatus: 'loading' })
33
+ })
34
+
35
+ it('handles files loading', () => {
36
+ mockUseQuery.mockReturnValueOnce({ fetchStatus: 'loading', data: null })
37
+ mockUseQuery.mockReturnValueOnce({ fetchStatus: 'loading', data: null })
38
+ const { result } = renderHook(() => useDataCardFiles('1', '2'))
39
+ expect(result.current).toStrictEqual({ fetchStatus: 'loading' })
40
+ })
41
+
42
+ it('handles files loading with partial data', () => {
43
+ mockUseQuery.mockReturnValueOnce({
44
+ fetchStatus: 'loading',
45
+ data: [mockFile1]
46
+ })
47
+ mockUseQuery.mockReturnValueOnce({
48
+ fetchStatus: 'loading',
49
+ data: [mockFile2]
50
+ })
51
+ const { result } = renderHook(() => useDataCardFiles('1', '2'))
52
+ expect(result.current).toStrictEqual({ fetchStatus: 'loading' })
53
+ })
54
+
55
+ it('handles files loading with more partial data', () => {
56
+ mockUseQuery.mockReturnValueOnce({
57
+ fetchStatus: 'loading',
58
+ data: [mockFile1, mockFile2]
59
+ })
60
+ mockUseQuery.mockReturnValueOnce({
61
+ fetchStatus: 'loading',
62
+ data: [mockFile3, mockFile4]
63
+ })
64
+ const { result } = renderHook(() => useDataCardFiles('1', '2'))
65
+ expect(result.current).toStrictEqual({ fetchStatus: 'loading' })
66
+ })
67
+
68
+ it('handles files loading with empty data', () => {
69
+ mockUseQuery.mockReturnValueOnce({
70
+ fetchStatus: 'loaded',
71
+ data: [],
72
+ lastFetch: 1
73
+ })
74
+ mockUseQuery.mockReturnValueOnce({
75
+ fetchStatus: 'loaded',
76
+ data: [],
77
+ lastFetch: 1
78
+ })
79
+ const { result } = renderHook(() => useDataCardFiles('1', '2'))
80
+ expect(result.current).toStrictEqual({ fetchStatus: 'empty' })
81
+ })
82
+
83
+ it('handles files loaded and return in correct order', () => {
84
+ mockUseQuery.mockReturnValueOnce({
85
+ fetchStatus: 'loaded',
86
+ data: [mockFile2, mockFile1],
87
+ lastFetch: 1
88
+ })
89
+ mockUseQuery.mockReturnValueOnce({
90
+ fetchStatus: 'loaded',
91
+ data: [mockFile3, mockFile4],
92
+ lastFetch: 1
93
+ })
94
+ const { result } = renderHook(() => useDataCardFiles('1', '2'))
95
+ expect(result.current).toStrictEqual({
96
+ data: [mockFile1, mockFile2, mockFile3, mockFile4],
97
+ fetchStatus: 'loaded'
98
+ })
99
+ })
100
+
101
+ it('handles files loaded with identical double return', () => {
102
+ mockUseQuery.mockReturnValueOnce({
103
+ fetchStatus: 'loaded',
104
+ data: [mockFile1, mockFile2, mockFile3, mockFile4],
105
+ lastFetch: 1
106
+ })
107
+ mockUseQuery.mockReturnValueOnce({
108
+ fetchStatus: 'loaded',
109
+ data: [mockFile3, mockFile2, mockFile3, mockFile4],
110
+ lastFetch: 1
111
+ })
112
+ const { result } = renderHook(() => useDataCardFiles('1', '2'))
113
+ expect(result.current).toStrictEqual({
114
+ data: [mockFile1, mockFile2, mockFile3, mockFile4],
115
+ fetchStatus: 'loaded'
116
+ })
117
+ })
118
+
119
+ it('handles files loaded with more than 5 result', () => {
120
+ mockUseQuery.mockReturnValueOnce({
121
+ fetchStatus: 'loaded',
122
+ data: [mockFile1, mockFile2, mockFile3, mockFile4, mockFile5, mockFile6],
123
+ lastFetch: 1
124
+ })
125
+ mockUseQuery.mockReturnValueOnce({
126
+ fetchStatus: 'loaded',
127
+ data: [mockFile7, mockFile8, mockFile9, mockFile10, mockFile11, mockFile12],
128
+ lastFetch: 1
129
+ })
130
+ const { result } = renderHook(() => useDataCardFiles('1', '2'))
131
+ expect(result.current).toStrictEqual({
132
+ data: [mockFile1, mockFile2, mockFile3, mockFile4, mockFile5],
133
+ fetchStatus: 'loaded'
134
+ })
135
+ })