@openneuro/app 4.11.0-alpha.0 → 4.12.0-alpha.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.
@@ -1,11 +1,7 @@
1
1
  import React from 'react'
2
2
  import { render, screen, fireEvent } from '@testing-library/react'
3
3
  import { MockedProvider } from '@apollo/client/testing'
4
- import FileTree, {
5
- sortByFilename,
6
- sortByName,
7
- unescapePath,
8
- } from '../file-tree.jsx'
4
+ import FileTree, { unescapePath, fileTreeLevels } from '../file-tree'
9
5
 
10
6
  // official Jest workaround for mocking methods not implemented in JSDOM
11
7
  window.matchMedia =
@@ -18,15 +14,110 @@ window.matchMedia =
18
14
  }
19
15
  }
20
16
 
21
- /* eslint-disable */
22
- jest.mock('react-spring', () => ({
23
- useSpring: jest.fn().mockImplementation(() => [{ mockProp: 1 }, jest.fn()]),
24
- animated: {
25
- path: () => <path data-testid="ANIMATED-COMPONENT" />,
26
- div: () => <div data-testid="ANIMATED-COMPONENT" />,
17
+ const datasetFiles = [
18
+ {
19
+ id: 'b42624c6aea63fc5e3f6f3e712d9e44adc4dbfdc',
20
+ key: '834b88a80109f1b38e0ab85999090170889469ce',
21
+ filename: 'CHANGES',
22
+ size: 59,
23
+ directory: false,
24
+ annexed: false,
27
25
  },
28
- }))
29
- /* eslint-enable */
26
+ {
27
+ id: '63888a199a5ce37377b1cd708cda59577dad218f',
28
+ key: 'fa84e5f958ec72d42b3e196e592f6db9f7104b19',
29
+ filename: 'README',
30
+ size: 709,
31
+ directory: false,
32
+ annexed: false,
33
+ },
34
+ {
35
+ id: 'a2922b427c5c997e77ce058e0ce57ffd17123a7f',
36
+ key: '0b1856b91c11f67098ce60114417a62dd55730a5',
37
+ filename: 'dataset_description.json',
38
+ size: 172,
39
+ directory: false,
40
+ annexed: false,
41
+ },
42
+ {
43
+ id: 'a6378eea201d9cad639e0bee328e03132d30489d',
44
+ key: null,
45
+ filename: 'sub-01',
46
+ size: 0,
47
+ directory: true,
48
+ annexed: false,
49
+ },
50
+ ]
51
+
52
+ const anatDir = {
53
+ id: '9703f3f6b4137c2b86a3a712eb34d78bdec9fd72',
54
+ key: null,
55
+ filename: 'sub-01:anat',
56
+ size: 0,
57
+ directory: true,
58
+ annexed: false,
59
+ }
60
+
61
+ const niftiFile = {
62
+ id: '69fd3617b27125c433ea5f8e0e2052c31828c253',
63
+ key: 'MD5E-s311112--bc8bbbacfd2ff823c2047ead1afec9b3.nii.gz',
64
+ filename: 'sub-01:anat:sub-01_T1w.nii.gz',
65
+ size: 311112,
66
+ directory: false,
67
+ annexed: true,
68
+ }
69
+
70
+ describe('fileTreeLevels()', () => {
71
+ it('handles top level files correctly', () => {
72
+ const { childFiles, currentFiles } = fileTreeLevels('', datasetFiles)
73
+ expect(currentFiles).toEqual(
74
+ expect.arrayContaining([
75
+ currentFiles.find(
76
+ f => f.id === 'a2922b427c5c997e77ce058e0ce57ffd17123a7f',
77
+ ),
78
+ ]),
79
+ )
80
+ expect(childFiles).toEqual({})
81
+ })
82
+ it('passes childFiles to the next level', () => {
83
+ const levelOneFiles = [...datasetFiles, anatDir]
84
+ const { childFiles, currentFiles } = fileTreeLevels('', levelOneFiles)
85
+ expect(currentFiles).toEqual(
86
+ expect.arrayContaining([
87
+ currentFiles.find(
88
+ f => f.id === 'a2922b427c5c997e77ce058e0ce57ffd17123a7f',
89
+ ),
90
+ ]),
91
+ )
92
+ expect(childFiles['sub-01']).toEqual(expect.arrayContaining([anatDir]))
93
+ })
94
+ it('passes two level deep childFiles to the next level', () => {
95
+ const levelTwoFiles = [...datasetFiles, anatDir, niftiFile]
96
+ const { childFiles, currentFiles } = fileTreeLevels('', levelTwoFiles)
97
+ expect(currentFiles).toEqual(
98
+ expect.arrayContaining([
99
+ currentFiles.find(
100
+ f => f.id === 'a2922b427c5c997e77ce058e0ce57ffd17123a7f',
101
+ ),
102
+ ]),
103
+ )
104
+ expect(childFiles['sub-01']).toEqual(expect.arrayContaining([niftiFile]))
105
+ })
106
+ it('passes two level deep childFiles to the next level', () => {
107
+ const levelTwoFiles = [anatDir, niftiFile]
108
+ const { childFiles, currentFiles } = fileTreeLevels('sub-01', levelTwoFiles)
109
+ expect(currentFiles).toEqual(
110
+ expect.arrayContaining([
111
+ currentFiles.find(
112
+ f => f.id === '9703f3f6b4137c2b86a3a712eb34d78bdec9fd72',
113
+ ),
114
+ ]),
115
+ )
116
+ expect(childFiles['sub-01:anat']).toEqual(
117
+ expect.arrayContaining([niftiFile]),
118
+ )
119
+ })
120
+ })
30
121
 
31
122
  describe('FileTree component', () => {
32
123
  it('renders with default props', () => {
@@ -56,26 +147,6 @@ describe('FileTree component', () => {
56
147
  'fa-folder',
57
148
  )
58
149
  })
59
- describe('sortByFilename()', () => {
60
- it('sorts the expected filename properties', () => {
61
- expect(
62
- sortByFilename(
63
- { name: 'abc', filename: 'xyz' },
64
- { name: 'xyz', filename: 'abc' },
65
- ),
66
- ).toBe(1)
67
- })
68
- })
69
- describe('sortByName()', () => {
70
- it('sorts the expected name properties', () => {
71
- expect(
72
- sortByName(
73
- { name: 'abc', filename: 'xyz' },
74
- { name: 'xyz', filename: 'abc' },
75
- ),
76
- ).toBe(-1)
77
- })
78
- })
79
150
  describe('unescapePath()', () => {
80
151
  it('does not alter an already escaped path', () => {
81
152
  expect(unescapePath('sub-01/anat')).toBe('sub-01/anat')
@@ -1,15 +1,14 @@
1
1
  import React, { useState, useContext, useEffect } from 'react'
2
2
  import PropTypes from 'prop-types'
3
3
  import DatasetQueryContext from '../../datalad/dataset/dataset-query-context.js'
4
- import FileTreeLoading from './file-tree-loading.jsx'
5
4
  import { gql } from '@apollo/client'
6
5
  import { AccordionTab } from '@openneuro/components/accordion'
7
6
 
8
7
  export const DRAFT_FILES_QUERY = gql`
9
- query dataset($datasetId: ID!, $filePrefix: String!) {
8
+ query dataset($datasetId: ID!, $tree: String!) {
10
9
  dataset(id: $datasetId) {
11
10
  draft {
12
- files(prefix: $filePrefix) {
11
+ files(tree: $tree) {
13
12
  id
14
13
  key
15
14
  filename
@@ -23,9 +22,9 @@ export const DRAFT_FILES_QUERY = gql`
23
22
  `
24
23
 
25
24
  export const SNAPSHOT_FILES_QUERY = gql`
26
- query snapshot($datasetId: ID!, $snapshotTag: String!, $filePrefix: String!) {
25
+ query snapshot($datasetId: ID!, $snapshotTag: String!, $tree: String!) {
27
26
  snapshot(datasetId: $datasetId, tag: $snapshotTag) {
28
- files(prefix: $filePrefix) {
27
+ files(tree: $tree) {
29
28
  id
30
29
  key
31
30
  filename
@@ -37,24 +36,30 @@ export const SNAPSHOT_FILES_QUERY = gql`
37
36
  }
38
37
  `
39
38
 
39
+ /**
40
+ * Prepend paths to the tree object returned to get absolute filenames
41
+ */
42
+ export const nestFiles = path => file => ({
43
+ ...file,
44
+ filename: `${path}:${file.filename}`,
45
+ })
46
+
47
+ /**
48
+ * Merge cached dataset files with newly received data
49
+ */
40
50
  export const mergeNewFiles =
41
51
  (directory, snapshotTag) =>
42
52
  (past, { fetchMoreResult }) => {
43
53
  // Deep clone the old dataset object
54
+ const path = directory.filename
44
55
  const newDatasetObj = JSON.parse(JSON.stringify(past))
45
- const mergeNewFileFilter = f => f.id !== directory.id
46
- // Remove ourselves from the array
47
- if (snapshotTag) {
48
- newDatasetObj.snapshot.files =
49
- newDatasetObj.snapshot.files.filter(mergeNewFileFilter)
50
- newDatasetObj.snapshot.files.push(...fetchMoreResult.snapshot.files)
51
- } else {
52
- newDatasetObj.dataset.draft.files =
53
- newDatasetObj.dataset.draft.files.filter(mergeNewFileFilter)
54
- newDatasetObj.dataset.draft.files.push(
55
- ...fetchMoreResult.dataset.draft.files,
56
- )
57
- }
56
+ const newFiles = snapshotTag
57
+ ? newDatasetObj.snapshot.files
58
+ : newDatasetObj.dataset.draft.files
59
+ const fetchMoreData = snapshotTag
60
+ ? fetchMoreResult.snapshot
61
+ : fetchMoreResult.dataset.draft
62
+ newFiles.push(...fetchMoreData.files.map(nestFiles(path)))
58
63
  return newDatasetObj
59
64
  }
60
65
 
@@ -66,31 +71,21 @@ export const fetchMoreDirectory = (
66
71
  ) =>
67
72
  fetchMore({
68
73
  query: snapshotTag ? SNAPSHOT_FILES_QUERY : DRAFT_FILES_QUERY,
69
- variables: { datasetId, snapshotTag, filePrefix: directory.filename + '/' },
74
+ variables: { datasetId, snapshotTag, tree: directory.id },
70
75
  updateQuery: mergeNewFiles(directory, snapshotTag),
71
76
  })
72
77
 
73
78
  const FileTreeUnloadedDirectory = ({ datasetId, snapshotTag, directory }) => {
74
- const [loading, setLoading] = useState(false)
75
- const [displayLoading, setDisplayLoading] = useState(false)
76
79
  const { fetchMore } = useContext(DatasetQueryContext)
77
- useEffect(() => {
78
- if (loading) {
79
- const timer = setTimeout(() => setDisplayLoading(true), 150)
80
- return () => clearTimeout(timer)
81
- }
82
- }, [loading])
83
80
  return (
84
81
  <AccordionTab
85
- label={directory.filename}
82
+ label={directory.filename.split(':').pop()}
86
83
  accordionStyle="file-tree"
87
84
  onClick={() => {
88
- // Show a loading state while we wait on the directory to stream in
89
- setLoading(true)
90
85
  fetchMoreDirectory(fetchMore, datasetId, snapshotTag, directory)
91
86
  // No need to clear since this component is unmounted immediately
92
87
  }}>
93
- <FileTreeLoading size={directory.size} />
88
+ <div>Loading...</div>
94
89
  </AccordionTab>
95
90
  )
96
91
  }
@@ -0,0 +1,158 @@
1
+ import React from 'react'
2
+ import File from './file'
3
+ import UpdateFile from '../mutations/update-file.jsx'
4
+ import DeleteDir from '../mutations/delete-dir.jsx'
5
+ import FileTreeUnloadedDirectory from './file-tree-unloaded-directory.jsx'
6
+ import { Media } from '../../styles/media'
7
+ import { AccordionTab } from '@openneuro/components/accordion'
8
+ import { DatasetFile } from '../../types/dataset-file'
9
+
10
+ export const unescapePath = (path: string): string => path.replace(/:/g, '/')
11
+
12
+ interface FileTreeProps {
13
+ datasetId: string
14
+ snapshotTag: string
15
+ path: string
16
+ name: string
17
+ files: DatasetFile[]
18
+ editMode: boolean
19
+ defaultExpanded: boolean
20
+ datasetPermissions: any
21
+ toggleFileToDelete: ({ id, path, filename }) => void
22
+ isFileToBeDeleted: (id: string) => boolean
23
+ bulkDeleteButton: JSX.Element
24
+ }
25
+
26
+ export function fileTreeLevels(
27
+ path: string,
28
+ files: DatasetFile[],
29
+ ): { currentFiles: DatasetFile[]; childFiles: Record<string, DatasetFile[]> } {
30
+ // Split files into a tree for this level and child levels
31
+ // Special cases for root (path === '')
32
+ const currentFiles = []
33
+ const childFiles = {}
34
+ for (const f of files) {
35
+ // Any paths in this filename below the current path value
36
+ const lowerPath = f.filename.substring(`${path}:`.length)
37
+ if (path === '' ? f.filename.includes(':') : lowerPath.includes(':')) {
38
+ // At the top level, use the directory component (first segment)
39
+ // Below that, use all paths before the filename (sub-01:anat) for (sub-01:anat:sub-01_T1w.nii.gz)
40
+ const childPath =
41
+ path === ''
42
+ ? f.filename.split(':')[0]
43
+ : f.filename.split(':').slice(0, -1).join(':')
44
+ if (childFiles.hasOwnProperty(childPath)) {
45
+ childFiles[childPath].push(f)
46
+ } else {
47
+ childFiles[childPath] = [f]
48
+ }
49
+ } else {
50
+ currentFiles.push(f)
51
+ }
52
+ }
53
+ return { currentFiles, childFiles }
54
+ }
55
+
56
+ const FileTree = ({
57
+ datasetId,
58
+ snapshotTag = null,
59
+ path = '',
60
+ name = '',
61
+ files = [],
62
+ editMode = false,
63
+ defaultExpanded = false,
64
+ datasetPermissions,
65
+ toggleFileToDelete,
66
+ isFileToBeDeleted,
67
+ bulkDeleteButton,
68
+ }: FileTreeProps): JSX.Element => {
69
+ const { childFiles, currentFiles } = fileTreeLevels(path, files)
70
+ return (
71
+ <AccordionTab
72
+ className=""
73
+ label={name}
74
+ accordionStyle="file-tree"
75
+ startOpen={defaultExpanded}>
76
+ {editMode && (
77
+ <Media className="filetree-dir-tools" greaterThanOrEqual="medium">
78
+ <span className="filetree-dir">
79
+ <UpdateFile
80
+ datasetId={datasetId}
81
+ path={unescapePath(path)}
82
+ tooltip={`Choose one or more files to be added to ${name}.`}
83
+ multiple>
84
+ <i className="fa fa-plus" /> Add Files
85
+ </UpdateFile>
86
+ <UpdateFile
87
+ datasetId={datasetId}
88
+ path={unescapePath(path)}
89
+ tooltip={`Choose a folder to be added to ${name}. Adding a folder with an existing name will overwrite that folder.`}
90
+ directory>
91
+ <i className="fa fa-plus" /> Add Directory
92
+ </UpdateFile>
93
+ {bulkDeleteButton || (
94
+ <DeleteDir datasetId={datasetId} path={path} name={name} />
95
+ )}
96
+ </span>
97
+ </Media>
98
+ )}
99
+ <ul className="child-files">
100
+ {currentFiles.map((file, index) => {
101
+ if (file.directory) {
102
+ if (childFiles.hasOwnProperty(file.filename)) {
103
+ return (
104
+ <li className="clearfix filetree-item filetree-dir" key={index}>
105
+ <FileTree
106
+ datasetId={datasetId}
107
+ snapshotTag={snapshotTag}
108
+ editMode={editMode}
109
+ defaultExpanded={true}
110
+ datasetPermissions={datasetPermissions}
111
+ toggleFileToDelete={toggleFileToDelete}
112
+ isFileToBeDeleted={isFileToBeDeleted}
113
+ files={childFiles[file.filename]}
114
+ path={file.filename}
115
+ name={file.filename.split(':').pop()}
116
+ bulkDeleteButton={bulkDeleteButton}
117
+ />
118
+ </li>
119
+ )
120
+ } else {
121
+ return (
122
+ <li className="clearfix filetree-item filetree-dir" key={index}>
123
+ <FileTreeUnloadedDirectory
124
+ datasetId={datasetId}
125
+ snapshotTag={snapshotTag}
126
+ directory={file}
127
+ />
128
+ </li>
129
+ )
130
+ }
131
+ } else {
132
+ return (
133
+ <li className="clearfix filetree-item filetree-file" key={index}>
134
+ <File
135
+ id={file.id}
136
+ datasetId={datasetId}
137
+ snapshotTag={snapshotTag}
138
+ path={path}
139
+ size={file.size}
140
+ editMode={editMode}
141
+ toggleFileToDelete={toggleFileToDelete}
142
+ isFileToBeDeleted={isFileToBeDeleted}
143
+ filename={file.filename.split(':').pop()}
144
+ annexKey={file.key}
145
+ datasetPermissions={datasetPermissions}
146
+ annexed={file.annexed}
147
+ isMobile={false}
148
+ />
149
+ </li>
150
+ )
151
+ }
152
+ })}
153
+ </ul>
154
+ </AccordionTab>
155
+ )
156
+ }
157
+
158
+ export default FileTree
@@ -81,7 +81,7 @@ export const apiPath = (datasetId, snapshotTag, filePath) => {
81
81
 
82
82
  interface FileProps {
83
83
  id: string
84
- size: number
84
+ size: bigint
85
85
  datasetId: string
86
86
  path: string
87
87
  filename: string
@@ -99,7 +99,7 @@ interface FileProps {
99
99
  id: string
100
100
  path: string
101
101
  filename: string
102
- }) => boolean
102
+ }) => void
103
103
  isFileToBeDeleted: (id: string) => boolean
104
104
  }
105
105
 
@@ -1,13 +1,13 @@
1
1
  import React, { useState } from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { flatToTree } from './flat-to-tree.js'
4
- import FileTree from './file-tree.jsx'
3
+ import FileTree from './file-tree'
5
4
  import { Media } from '../../styles/media'
6
5
  import { useMutation, gql } from '@apollo/client'
7
6
  import { WarnButton } from '@openneuro/components/warn-button'
8
7
  import { AccordionWrap } from '@openneuro/components/accordion'
9
8
  import styled from '@emotion/styled'
10
9
  import { Tooltip } from '@openneuro/components/tooltip'
10
+ import { DatasetFile } from '../../types/dataset-file'
11
11
  import bytes from 'bytes'
12
12
 
13
13
  const FileTreeMeta = styled.span`
@@ -33,6 +33,16 @@ const DELETE_FILES = gql`
33
33
  }
34
34
  `
35
35
 
36
+ interface FilesProps {
37
+ datasetId: string
38
+ snapshotTag: string
39
+ datasetName: string
40
+ files: DatasetFile[]
41
+ editMode: boolean
42
+ datasetPermissions: any
43
+ summary: any
44
+ }
45
+
36
46
  const Files = ({
37
47
  datasetId,
38
48
  snapshotTag,
@@ -41,14 +51,14 @@ const Files = ({
41
51
  editMode = false,
42
52
  datasetPermissions,
43
53
  summary,
44
- }) => {
54
+ }: FilesProps): JSX.Element => {
45
55
  const [filesToDelete, setFilesToDelete] = useState({})
46
56
  const [isDeleting, setIsDeleting] = useState(false)
47
57
  const [deleteFiles] = useMutation(DELETE_FILES)
48
58
 
49
- const isFileToBeDeleted = id => id in filesToDelete
59
+ const isFileToBeDeleted = (id: string): boolean => id in filesToDelete
50
60
 
51
- const toggleFileToDelete = ({ id, path, filename }) =>
61
+ const toggleFileToDelete = ({ id, path, filename }): void =>
52
62
  setFilesToDelete(prevFilesToDelete => {
53
63
  if (isFileToBeDeleted(id)) {
54
64
  delete prevFilesToDelete[id]
@@ -60,10 +70,10 @@ const Files = ({
60
70
  }
61
71
  })
62
72
 
63
- const bulkDelete = () => {
73
+ const bulkDelete = (): void => {
64
74
  if (Object.values(filesToDelete).length) {
65
75
  setIsDeleting(true)
66
- deleteFiles({
76
+ void deleteFiles({
67
77
  variables: { datasetId, files: Object.values(filesToDelete) },
68
78
  }).then(() => {
69
79
  setIsDeleting(false)
@@ -72,7 +82,6 @@ const Files = ({
72
82
  }
73
83
  }
74
84
 
75
- const fileTree = flatToTree(files)
76
85
  const disableBtn = Object.values(filesToDelete).length ? null : true
77
86
  const filesCount = Object.values(filesToDelete).length
78
87
  const bulkDeleteButton =
@@ -88,11 +97,11 @@ const Files = ({
88
97
  </Tooltip>
89
98
  ) : (
90
99
  <WarnButton
91
- message={'Bulk Delete (' + filesCount + ')'}
100
+ message={`Bulk Delete (${filesCount})`}
92
101
  icon="fas fa-dumpster"
93
102
  iconOnly={true}
94
103
  className="edit-file"
95
- tooltip={'Delete ' + filesCount}
104
+ tooltip={`Delete ${filesCount}`}
96
105
  onConfirmedClick={bulkDelete}
97
106
  />
98
107
  )}
@@ -108,7 +117,7 @@ const Files = ({
108
117
  datasetId={datasetId}
109
118
  snapshotTag={snapshotTag}
110
119
  path={''}
111
- {...fileTree}
120
+ files={files}
112
121
  name={datasetName}
113
122
  editMode={editMode}
114
123
  defaultExpanded={false}
@@ -133,7 +142,7 @@ const Files = ({
133
142
  datasetId={datasetId}
134
143
  snapshotTag={snapshotTag}
135
144
  path={''}
136
- {...fileTree}
145
+ files={files}
137
146
  name={datasetName}
138
147
  editMode={editMode}
139
148
  defaultExpanded={true}
@@ -0,0 +1,11 @@
1
+ // Temporary type representing the GraphQL DatasetFile type
2
+ // TODO - Derive this from the GraphQL schema
3
+ export interface DatasetFile {
4
+ id: string
5
+ key?: string
6
+ filename: string
7
+ size?: bigint
8
+ annexed?: boolean
9
+ urls?: string[]
10
+ directory?: boolean
11
+ }
@@ -1,18 +0,0 @@
1
- import { estimateDuration } from '../file-tree-loading.jsx'
2
-
3
- describe('FileTreeLoading component', () => {
4
- describe('estimateDuration()', () => {
5
- it('returns an estimate if navigator has a downlink set', () => {
6
- const mockNavigator = {
7
- connection: {
8
- downlink: 20,
9
- },
10
- }
11
- expect(estimateDuration(mockNavigator)(250)).toEqual(1270.703125)
12
- })
13
- it('returns an estimate if navigator does not have a downlink', () => {
14
- const mockNavigator = {}
15
- expect(estimateDuration(mockNavigator)(250)).toEqual(2491.40625)
16
- })
17
- })
18
- })
@@ -1,55 +0,0 @@
1
- import { flatToTree } from '../flat-to-tree.js'
2
-
3
- const CHANGES = Object.freeze({
4
- id: '3d9b15b3ef4e9da06e265e6078d3b4ddf8495102',
5
- filename: 'CHANGES',
6
- size: 39,
7
- })
8
-
9
- const nifti = Object.freeze({
10
- id: '50512c7261fc006eb59bfd16f2a9d3140c9efe62',
11
- filename: 'sub-01/anat/sub-01_T1w.nii.gz',
12
- size: 311112,
13
- })
14
-
15
- const sub01Unloaded = Object.freeze({
16
- id: 'directory:sub-01',
17
- filename: 'sub-01',
18
- size: 1,
19
- directory: true,
20
- })
21
-
22
- const exampleFiles = [CHANGES, nifti]
23
-
24
- describe('FileTree', () => {
25
- describe('flatToTree()', () => {
26
- it('accepts an array and returns a tree', () => {
27
- expect(flatToTree(exampleFiles)).toEqual({
28
- name: '',
29
- files: [CHANGES],
30
- directories: [
31
- {
32
- name: 'sub-01',
33
- path: 'sub-01',
34
- files: [],
35
- directories: [
36
- {
37
- name: 'anat',
38
- path: 'sub-01:anat',
39
- files: [{ ...nifti, filename: 'sub-01_T1w.nii.gz' }],
40
- directories: [],
41
- },
42
- ],
43
- },
44
- ],
45
- })
46
- })
47
- it('accepts directory stubs and returns them as directories', () => {
48
- expect(flatToTree([CHANGES, sub01Unloaded])).toEqual({
49
- name: '',
50
- files: [CHANGES],
51
- directories: [{ ...sub01Unloaded, name: sub01Unloaded.filename }],
52
- })
53
- })
54
- })
55
- })
@@ -1,65 +0,0 @@
1
- import React from 'react'
2
- import PropTypes from 'prop-types'
3
- import { useSpring, animated } from 'react-spring'
4
- import styled from '@emotion/styled'
5
-
6
- const ProgressOuter = styled.div`
7
- width: 100%;
8
- height: 2px;
9
- background-color: #f5f5f5;
10
- margin: 10px 0;
11
- `
12
- const ProgressInner = styled(animated.div)`
13
- height: 100%;
14
- color: white;
15
- line-height: 2px;
16
- text-align: center;
17
- width: 0%;
18
- `
19
-
20
- /**
21
- * Estimate time to fetch files
22
- * @param {Navigator} [navigator]
23
- * @returns {(size: number) => number}
24
- */
25
- export const estimateDuration = navigator => size => {
26
- // One file is about 100 bytes
27
- const estimatedBytes = size * 100
28
- if (navigator && 'connection' in navigator) {
29
- // Estimate duration precisely if we can
30
- const downlink = navigator.connection.downlink
31
- return (estimatedBytes / (downlink * 1024)) * 1000 + 50
32
- } else {
33
- // Fallback estimate of a conservative general connection (10 mbps)
34
- return (estimatedBytes / (10 * 1024)) * 1000 + 50
35
- }
36
- }
37
-
38
- const FileTreeLoading = ({ size }) => {
39
- const navRef = typeof navigator === 'undefined' ? undefined : navigator
40
- const config = {
41
- mass: 5,
42
- tension: 2000,
43
- friction: 200,
44
- duration: estimateDuration(navRef)(size),
45
- }
46
- const props = useSpring({
47
- config,
48
- from: {
49
- width: '0%',
50
- backgroundColor: '#f5f5f5',
51
- },
52
- to: { width: '100%', backgroundColor: 'var(--secondary)' },
53
- })
54
- return (
55
- <ProgressOuter>
56
- <ProgressInner style={props} />
57
- </ProgressOuter>
58
- )
59
- }
60
-
61
- FileTreeLoading.propTypes = {
62
- size: PropTypes.number,
63
- }
64
-
65
- export default FileTreeLoading