@openneuro/app 4.16.1 → 4.17.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openneuro/app",
3
- "version": "4.16.1",
3
+ "version": "4.17.0",
4
4
  "description": "React JS web frontend for the OpenNeuro platform.",
5
5
  "license": "MIT",
6
6
  "main": "public/client.js",
@@ -20,8 +20,8 @@
20
20
  "@emotion/react": "11.6.0",
21
21
  "@emotion/styled": "11.6.0",
22
22
  "@niivue/niivue": "0.23.1",
23
- "@openneuro/client": "^4.16.1",
24
- "@openneuro/components": "^4.16.1",
23
+ "@openneuro/client": "^4.17.0",
24
+ "@openneuro/components": "^4.17.0",
25
25
  "bids-validator": "1.10.0",
26
26
  "bytes": "^3.0.0",
27
27
  "comlink": "^4.0.5",
@@ -81,5 +81,5 @@
81
81
  "publishConfig": {
82
82
  "access": "public"
83
83
  },
84
- "gitHead": "0739211264d035ff4531bf7961232c1abd0165d5"
84
+ "gitHead": "2a6d05304f3927dc0d1896d5988ffddcaa4b5c3c"
85
85
  }
@@ -8,7 +8,8 @@ export const DATASET_COMMENTS = gql`
8
8
  text
9
9
  createDate
10
10
  user {
11
- email
11
+ name
12
+ orcid
12
13
  }
13
14
  parent {
14
15
  id
@@ -1454,7 +1454,7 @@ OCI-1131441 (R. Poldrack, PI) in any publications.
1454
1454
  >
1455
1455
  Uploaded by
1456
1456
  </h2>
1457
- Test User on 2021-12-17 - about 1 year ago
1457
+ Test User on 2021-12-17 - over 1 year ago
1458
1458
  </div>
1459
1459
  <div
1460
1460
  class="dataset-meta-block undefined"
@@ -1464,7 +1464,7 @@ OCI-1131441 (R. Poldrack, PI) in any publications.
1464
1464
  >
1465
1465
  Last Updated
1466
1466
  </h2>
1467
- 2021-12-17 - about 1 year ago
1467
+ 2021-12-17 - over 1 year ago
1468
1468
  </div>
1469
1469
  <div
1470
1470
  class="dataset-meta-block undefined"
@@ -1,5 +1,210 @@
1
1
  // Vitest Snapshot v1
2
2
 
3
+ exports[`Comment component > renders an ORCID user comment 1`] = `
4
+ {
5
+ "asFragment": [Function],
6
+ "baseElement": <body>
7
+ <div>
8
+ <div
9
+ class="comment"
10
+ >
11
+ <div
12
+ class="row comment-header"
13
+ >
14
+ By
15
+ Example Exampler
16
+
17
+ <a
18
+ href="https://orcid.org/1234-5678-9101"
19
+ >
20
+ <img
21
+ alt="ORCID logo"
22
+ height="16"
23
+ src="/packages/openneuro-app/src/assets/ORCIDiD_iconvector.svg"
24
+ width="16"
25
+ />
26
+ </a>
27
+ - almost 2 years ago
28
+ </div>
29
+ <div
30
+ class="row comment-body"
31
+ >
32
+ <div
33
+ class="DraftEditor-root"
34
+ >
35
+ <div
36
+ class="DraftEditor-editorContainer"
37
+ >
38
+ <div
39
+ class="public-DraftEditor-content"
40
+ contenteditable="false"
41
+ spellcheck="false"
42
+ style="outline: none; user-select: text; white-space: pre-wrap; word-wrap: break-word;"
43
+ >
44
+ <div
45
+ data-contents="true"
46
+ >
47
+ <div
48
+ class=""
49
+ data-block="true"
50
+ data-editor="9001"
51
+ data-offset-key="3sm42-0-0"
52
+ >
53
+ <div
54
+ class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"
55
+ data-offset-key="3sm42-0-0"
56
+ >
57
+ <span
58
+ data-offset-key="3sm42-0-0"
59
+ >
60
+ <br
61
+ data-text="true"
62
+ />
63
+ </span>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ <div
73
+ class="row replies"
74
+ >
75
+ <div
76
+ class="comment-reply"
77
+ />
78
+ </div>
79
+ </div>
80
+ </body>,
81
+ "container": <div>
82
+ <div
83
+ class="comment"
84
+ >
85
+ <div
86
+ class="row comment-header"
87
+ >
88
+ By
89
+ Example Exampler
90
+
91
+ <a
92
+ href="https://orcid.org/1234-5678-9101"
93
+ >
94
+ <img
95
+ alt="ORCID logo"
96
+ height="16"
97
+ src="/packages/openneuro-app/src/assets/ORCIDiD_iconvector.svg"
98
+ width="16"
99
+ />
100
+ </a>
101
+ - almost 2 years ago
102
+ </div>
103
+ <div
104
+ class="row comment-body"
105
+ >
106
+ <div
107
+ class="DraftEditor-root"
108
+ >
109
+ <div
110
+ class="DraftEditor-editorContainer"
111
+ >
112
+ <div
113
+ class="public-DraftEditor-content"
114
+ contenteditable="false"
115
+ spellcheck="false"
116
+ style="outline: none; user-select: text; white-space: pre-wrap; word-wrap: break-word;"
117
+ >
118
+ <div
119
+ data-contents="true"
120
+ >
121
+ <div
122
+ class=""
123
+ data-block="true"
124
+ data-editor="9001"
125
+ data-offset-key="3sm42-0-0"
126
+ >
127
+ <div
128
+ class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"
129
+ data-offset-key="3sm42-0-0"
130
+ >
131
+ <span
132
+ data-offset-key="3sm42-0-0"
133
+ >
134
+ <br
135
+ data-text="true"
136
+ />
137
+ </span>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ <div
147
+ class="row replies"
148
+ >
149
+ <div
150
+ class="comment-reply"
151
+ />
152
+ </div>
153
+ </div>,
154
+ "debug": [Function],
155
+ "findAllByAltText": [Function],
156
+ "findAllByDisplayValue": [Function],
157
+ "findAllByLabelText": [Function],
158
+ "findAllByPlaceholderText": [Function],
159
+ "findAllByRole": [Function],
160
+ "findAllByTestId": [Function],
161
+ "findAllByText": [Function],
162
+ "findAllByTitle": [Function],
163
+ "findByAltText": [Function],
164
+ "findByDisplayValue": [Function],
165
+ "findByLabelText": [Function],
166
+ "findByPlaceholderText": [Function],
167
+ "findByRole": [Function],
168
+ "findByTestId": [Function],
169
+ "findByText": [Function],
170
+ "findByTitle": [Function],
171
+ "getAllByAltText": [Function],
172
+ "getAllByDisplayValue": [Function],
173
+ "getAllByLabelText": [Function],
174
+ "getAllByPlaceholderText": [Function],
175
+ "getAllByRole": [Function],
176
+ "getAllByTestId": [Function],
177
+ "getAllByText": [Function],
178
+ "getAllByTitle": [Function],
179
+ "getByAltText": [Function],
180
+ "getByDisplayValue": [Function],
181
+ "getByLabelText": [Function],
182
+ "getByPlaceholderText": [Function],
183
+ "getByRole": [Function],
184
+ "getByTestId": [Function],
185
+ "getByText": [Function],
186
+ "getByTitle": [Function],
187
+ "queryAllByAltText": [Function],
188
+ "queryAllByDisplayValue": [Function],
189
+ "queryAllByLabelText": [Function],
190
+ "queryAllByPlaceholderText": [Function],
191
+ "queryAllByRole": [Function],
192
+ "queryAllByTestId": [Function],
193
+ "queryAllByText": [Function],
194
+ "queryAllByTitle": [Function],
195
+ "queryByAltText": [Function],
196
+ "queryByDisplayValue": [Function],
197
+ "queryByLabelText": [Function],
198
+ "queryByPlaceholderText": [Function],
199
+ "queryByRole": [Function],
200
+ "queryByTestId": [Function],
201
+ "queryByText": [Function],
202
+ "queryByTitle": [Function],
203
+ "rerender": [Function],
204
+ "unmount": [Function],
205
+ }
206
+ `;
207
+
3
208
  exports[`Comment component > renders with an empty comment 1`] = `
4
209
  {
5
210
  "asFragment": [Function],
@@ -11,7 +216,9 @@ exports[`Comment component > renders with an empty comment 1`] = `
11
216
  <div
12
217
  class="row comment-header"
13
218
  >
14
- By example@example.com - almost 2 years ago
219
+ By
220
+ Example Exampler
221
+ - almost 2 years ago
15
222
  </div>
16
223
  <div
17
224
  class="row comment-body"
@@ -72,7 +279,9 @@ exports[`Comment component > renders with an empty comment 1`] = `
72
279
  <div
73
280
  class="row comment-header"
74
281
  >
75
- By example@example.com - almost 2 years ago
282
+ By
283
+ Example Exampler
284
+ - almost 2 years ago
76
285
  </div>
77
286
  <div
78
287
  class="row comment-body"
@@ -17,7 +17,31 @@ describe('Comment component', () => {
17
17
  data={{
18
18
  id: '9001',
19
19
  text: emptyState,
20
- user: { id: '1234', email: 'example@example.com' },
20
+ user: {
21
+ id: '1234',
22
+ email: 'example@example.com',
23
+ name: 'Example Exampler',
24
+ },
25
+ createDate: new Date('2019-04-02T19:56:41.222Z').toISOString(),
26
+ }}
27
+ />,
28
+ )
29
+ expect(wrapper).toMatchSnapshot()
30
+ })
31
+ it('renders an ORCID user comment', () => {
32
+ formatDistanceToNow.mockReturnValueOnce('almost 2 years')
33
+
34
+ const wrapper = render(
35
+ <Comment
36
+ data={{
37
+ id: '9001',
38
+ text: emptyState,
39
+ user: {
40
+ id: '1234',
41
+ email: 'example@example.com',
42
+ name: 'Example Exampler',
43
+ orcid: '1234-5678-9101',
44
+ },
21
45
  createDate: new Date('2019-04-02T19:56:41.222Z').toISOString(),
22
46
  }}
23
47
  />,
@@ -11,6 +11,7 @@ import LoggedIn from '../../authentication/logged-in.jsx'
11
11
  import { toast } from 'react-toastify'
12
12
  import ToastContent from '../../common/partials/toast-content'
13
13
  import { Icon } from '@openneuro/components/icon'
14
+ import { Username } from '../../users/username'
14
15
 
15
16
  const Comment = ({ datasetId, data, children }) => {
16
17
  const [replyMode, setReplyMode] = useState(false)
@@ -21,9 +22,8 @@ const Comment = ({ datasetId, data, children }) => {
21
22
  <>
22
23
  <div className="comment">
23
24
  <div className="row comment-header">
24
- {`By ${data.user.email} - ${formatDistanceToNow(
25
- parseISO(data.createDate),
26
- )} ago`}
25
+ By <Username user={data.user} />
26
+ {` - ${formatDistanceToNow(parseISO(data.createDate))} ago`}
27
27
  </div>
28
28
  <div className="row comment-body">
29
29
  {editMode ? (
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import File from './file'
3
3
  import UpdateFile from '../mutations/update-file.jsx'
4
- import DeleteDir from '../mutations/delete-dir.jsx'
4
+ import DeleteFile from '../mutations/delete-file.jsx'
5
5
  import FileTreeUnloadedDirectory from './file-tree-unloaded-directory.jsx'
6
6
  import { Media } from '../../styles/media'
7
7
  import { AccordionTab } from '@openneuro/components/accordion'
@@ -91,8 +91,10 @@ const FileTree = ({
91
91
  directory>
92
92
  <i className="fa fa-plus" /> Add Directory
93
93
  </UpdateFile>
94
- {bulkDeleteButton || (
95
- <DeleteDir datasetId={datasetId} path={path} name={name} />
94
+ {path === '' ? (
95
+ bulkDeleteButton
96
+ ) : (
97
+ <DeleteFile datasetId={datasetId} path={path} filename={name} />
96
98
  )}
97
99
  </span>
98
100
  </Media>
@@ -117,7 +117,7 @@ const File = ({
117
117
  datasetPermissions,
118
118
  toggleFileToDelete,
119
119
  isFileToBeDeleted,
120
- }: FileProps) => {
120
+ }: FileProps): JSX.Element => {
121
121
  const { icon, color } = getFileIcon(filename)
122
122
  const snapshotVersionPath = snapshotTag ? `/versions/${snapshotTag}` : ''
123
123
  // React route to display the file
@@ -14,37 +14,3 @@ exports[`DeleteDataset mutation > renders with common props 1`] = `
14
14
  </span>
15
15
  </DocumentFragment>
16
16
  `;
17
-
18
- exports[`DeleteDir mutation > renders with common props 1`] = `
19
- <DocumentFragment>
20
- <span
21
- class="delete-file"
22
- >
23
- <div
24
- class="warn-btn edit-file"
25
- >
26
- <span
27
- class=" "
28
- data-flow="up"
29
- data-tooltip="Delete undefined"
30
- >
31
- <span
32
- class="warn-btn-click"
33
- >
34
- <button
35
- aria-label=""
36
- class="on-button on-button--medium on-button--default btn-warn-component"
37
- role="button"
38
- type="button"
39
- >
40
- <i
41
- aria-hidden="true"
42
- class="fa fa-trash css-1qxtz39"
43
- />
44
- </button>
45
- </span>
46
- </span>
47
- </div>
48
- </span>
49
- </DocumentFragment>
50
- `;
@@ -0,0 +1,195 @@
1
+ import { fileCacheDeleteFilter } from '../delete-file.jsx'
2
+
3
+ describe('DeleteFile mutation', () => {
4
+ describe('fileCacheDeleteFilter', () => {
5
+ it('removes a deleted file', () => {
6
+ expect(
7
+ fileCacheDeleteFilter(
8
+ {
9
+ id: 'DatasetFile:abcdef',
10
+ key: 'cdefgh',
11
+ filename: 'sub-01:anat:sub-01_T1w.nii.gz',
12
+ directory: false,
13
+ },
14
+ 'sub-01:anat',
15
+ 'sub-01_T1w.nii.gz',
16
+ [],
17
+ ),
18
+ ).toBe(false)
19
+ })
20
+ it('does not remove a present file', () => {
21
+ expect(
22
+ fileCacheDeleteFilter(
23
+ {
24
+ id: 'DatasetFile:abcdef',
25
+ key: 'cdefgh',
26
+ filename: 'sub-02:anat:sub-02_T1w.nii.gz',
27
+ directory: false,
28
+ },
29
+ 'sub-01:anat',
30
+ 'sub-01_T1w.nii.gz',
31
+ [],
32
+ ),
33
+ ).toBe(true)
34
+ })
35
+ it('removes a matching directory', () => {
36
+ expect(
37
+ fileCacheDeleteFilter(
38
+ {
39
+ id: 'DatasetFile:abcdef',
40
+ key: 'cdefgh',
41
+ filename: 'sub-01:anat',
42
+ directory: true,
43
+ },
44
+ 'sub-01:anat',
45
+ '',
46
+ [],
47
+ ),
48
+ ).toBe(false)
49
+ })
50
+ it('removes empty directories with a child being deleted', () => {
51
+ const cachedFileObjects = [
52
+ {
53
+ __typename: 'DatasetFile',
54
+ id: '3d9b15b3ef4e9da06e265e6078d3b4ddf8495102',
55
+ key: 'c2ee90bf6c477b9e808e2b649a9492e947493297',
56
+ filename: 'CHANGES',
57
+ directory: false,
58
+ },
59
+ {
60
+ __typename: 'DatasetFile',
61
+ id: '63888a199a5ce37377b1cd708cda59577dad218f',
62
+ key: 'fa84e5f958ec72d42b3e196e592f6db9f7104b19',
63
+ filename: 'README',
64
+ directory: false,
65
+ },
66
+ {
67
+ __typename: 'DatasetFile',
68
+ id: 'aef1717a00106adc115f64990944d86e154d3e03',
69
+ key: 'e652e8add021fe2684ce2404b431dee4315c9c95',
70
+ filename: 'dataset_description.json',
71
+ directory: false,
72
+ },
73
+ {
74
+ __typename: 'DatasetFile',
75
+ id: '0a2a5d8d72a31f03608db59c4cfd650aba77c363',
76
+ key: 'b08aa0ec5b5e716479824859524a22140fb2af82',
77
+ filename: 'T1w.json',
78
+ directory: false,
79
+ },
80
+ {
81
+ __typename: 'DatasetFile',
82
+ id: 'f682a32c9538082fa6c8ad11e9a536dc07d1d0cf',
83
+ key: '37ecbdc7ab8ffaf2cddecc338092f6679089287d',
84
+ filename: 'participants.tsv',
85
+ directory: false,
86
+ },
87
+ {
88
+ __typename: 'DatasetFile',
89
+ id: 'c2ffc386e99bb26fbfc0d6bb33713b91b95a51f2',
90
+ key: null,
91
+ filename: 'sub-01',
92
+ directory: true,
93
+ },
94
+ {
95
+ __typename: 'DatasetFile',
96
+ id: '141c63f3373f17477c83f42c9fab01e6825052a0',
97
+ key: null,
98
+ filename: 'sub-02',
99
+ directory: true,
100
+ },
101
+ {
102
+ __typename: 'DatasetFile',
103
+ id: '635818b25263badb6d105aab8e33822f54ebbecf',
104
+ key: null,
105
+ filename: 'sub-02:anat',
106
+ directory: true,
107
+ },
108
+ {
109
+ __typename: 'DatasetFile',
110
+ id: 'b6f937773aa2130aa9d06fc3024cd1b150baa70b',
111
+ key: 'SHA256E-s311112--c3527d7944a9619afb57863a34e6af7ec3fe4f108e56c860d9e700699ff806fb.nii.gz',
112
+ filename: 'sub-02:anat:sub-02_T1w.nii.gz',
113
+ directory: false,
114
+ },
115
+ ]
116
+ expect(
117
+ fileCacheDeleteFilter(
118
+ {
119
+ id: '141c63f3373f17477c83f42c9fab01e6825052a0',
120
+ key: null,
121
+ filename: 'sub-02',
122
+ directory: true,
123
+ },
124
+ 'sub-02:anat',
125
+ 'sub-02_T1w.nii.gz',
126
+ cachedFileObjects,
127
+ ),
128
+ ).toBe(false)
129
+ expect(
130
+ fileCacheDeleteFilter(
131
+ {
132
+ id: '635818b25263badb6d105aab8e33822f54ebbecf',
133
+ key: null,
134
+ filename: 'sub-02:anat',
135
+ directory: true,
136
+ },
137
+ 'sub-02:anat',
138
+ 'sub-02_T1w.nii.gz',
139
+ cachedFileObjects,
140
+ ),
141
+ ).toBe(false)
142
+ expect(
143
+ fileCacheDeleteFilter(
144
+ {
145
+ id: 'b6f937773aa2130aa9d06fc3024cd1b150baa70b',
146
+ key: 'SHA256E-s311112--c3527d7944a9619afb57863a34e6af7ec3fe4f108e56c860d9e700699ff806fb.nii.gz',
147
+ filename: 'sub-02:anat:sub-02_T1w.nii.gz',
148
+ directory: false,
149
+ },
150
+ 'sub-02:anat',
151
+ 'sub-02_T1w.nii.gz',
152
+ cachedFileObjects,
153
+ ),
154
+ ).toBe(false)
155
+ })
156
+ it('does not remove non-empty directories with a child being deleted', () => {
157
+ expect(
158
+ fileCacheDeleteFilter(
159
+ {
160
+ id: 'DatasetFile:abcdef',
161
+ key: 'cdefgh',
162
+ filename: 'sub-02:anat',
163
+ directory: true,
164
+ },
165
+ '',
166
+ 'sub-02:anat:sub-02_T1w.json',
167
+ [
168
+ {
169
+ id: 'DatasetFile:123456',
170
+ key: 'cdefgh',
171
+ filename: 'sub-02:anat:sub-02_T1w.nii.gz',
172
+ directory: false,
173
+ },
174
+ ],
175
+ ),
176
+ ).toBe(true)
177
+ })
178
+ it('does not remove directories that are empty but not having a child deleted', () => {
179
+ // This is also false because the directory has no files in cachedFileObjects
180
+ expect(
181
+ fileCacheDeleteFilter(
182
+ {
183
+ id: 'DatasetFile:abcdef',
184
+ key: 'cdefgh',
185
+ filename: 'sub-02:anat',
186
+ directory: true,
187
+ },
188
+ 'sub-01:anat',
189
+ '',
190
+ [],
191
+ ),
192
+ ).toBe(true)
193
+ })
194
+ })
195
+ })
@@ -2,10 +2,8 @@ import React from 'react'
2
2
  import { render, fireEvent, screen } from '@testing-library/react'
3
3
  import { MockedProvider } from '@apollo/client/testing'
4
4
  import DeleteDataset, { DELETE_DATASET } from '../delete.jsx'
5
- import DeleteDir, { DELETE_FILES } from '../delete-dir.jsx'
6
5
 
7
6
  const datasetId = 'ds999999'
8
- const path = 'sub-99'
9
7
 
10
8
  const deleteDatasetMock = {
11
9
  request: {
@@ -20,19 +18,6 @@ const deleteDatasetMock = {
20
18
  })),
21
19
  }
22
20
 
23
- const deleteDirMock = {
24
- request: {
25
- query: DELETE_FILES,
26
- variables: {
27
- datasetId,
28
- files: [{ path: 'sub-99' }],
29
- },
30
- },
31
- newData: vi.fn(() => ({
32
- data: {},
33
- })),
34
- }
35
-
36
21
  describe('DeleteDataset mutation', () => {
37
22
  it('renders with common props', () => {
38
23
  const { asFragment } = render(
@@ -43,35 +28,3 @@ describe('DeleteDataset mutation', () => {
43
28
  expect(asFragment()).toMatchSnapshot()
44
29
  })
45
30
  })
46
-
47
- describe('DeleteDir mutation', () => {
48
- it('renders with common props', () => {
49
- const { asFragment } = render(
50
- <MockedProvider mocks={[deleteDirMock]} addTypename={false}>
51
- <DeleteDir
52
- datasetId="ds002"
53
- fileTree={{
54
- files: [],
55
- directories: [],
56
- path: '',
57
- }}
58
- />
59
- </MockedProvider>,
60
- )
61
- expect(asFragment()).toMatchSnapshot()
62
- })
63
- it('fires the correct mutation', async () => {
64
- render(
65
- <MockedProvider mocks={[deleteDirMock]} addTypename={false}>
66
- <DeleteDir {...{ datasetId, path }} />
67
- </MockedProvider>,
68
- )
69
-
70
- // click "Delete" button
71
- await fireEvent.click(screen.getByRole('button'))
72
- // confirm delete
73
- await fireEvent.click(screen.getByLabelText('confirm'))
74
-
75
- expect(deleteDirMock.newData).toHaveBeenCalled()
76
- })
77
- })
@@ -1,7 +1,6 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { gql } from '@apollo/client'
4
- import { Mutation } from '@apollo/client/react/components'
3
+ import { gql, useMutation } from '@apollo/client'
5
4
  import { WarnButton } from '@openneuro/components/warn-button'
6
5
 
7
6
  const DELETE_FILE = gql`
@@ -10,25 +9,99 @@ const DELETE_FILE = gql`
10
9
  }
11
10
  `
12
11
 
13
- const DeleteFile = ({ datasetId, path, filename }) => (
14
- <Mutation mutation={DELETE_FILE} awaitRefetchQueries={true}>
15
- {deleteFiles => (
16
- <span className="delete-file">
17
- <WarnButton
18
- message=""
19
- iconOnly={true}
20
- icon="fa-trash"
21
- className="edit-file"
22
- onConfirmedClick={() => {
23
- deleteFiles({
24
- variables: { datasetId, files: [{ path, filename }] },
25
- })
26
- }}
27
- />
28
- </span>
29
- )}
30
- </Mutation>
31
- )
12
+ /**
13
+ * Given a file object, path/filename for deletion, and a list of currently loaded files, filter any that will be deleted and orphan directories
14
+ */
15
+ export function fileCacheDeleteFilter(file, path, filename, cachedFileObjects) {
16
+ const fullPath = [path, filename].filter(Boolean).join(':')
17
+ if (file.filename === fullPath) {
18
+ return false
19
+ } else {
20
+ if (file.directory && fullPath.startsWith(file.filename)) {
21
+ // If a file other than the deletion target is removed
22
+ // And no other files match this directory prefix
23
+ for (const f of cachedFileObjects) {
24
+ if (f.directory || f.filename === fullPath) {
25
+ continue
26
+ } else {
27
+ if (f.filename.startsWith(path)) {
28
+ return true
29
+ }
30
+ }
31
+ }
32
+ return false
33
+ }
34
+ return true
35
+ }
36
+ }
37
+
38
+ const DeleteFile = ({ datasetId, path, filename }) => {
39
+ const [deleteFiles] = useMutation(DELETE_FILE, {
40
+ awaitRefetchQueries: true,
41
+ update(cache, { data: { deleteFiles } }) {
42
+ if (deleteFiles) {
43
+ cache.modify({
44
+ id: `Draft:${datasetId}`,
45
+ fields: {
46
+ files(cachedFiles) {
47
+ // Filter any removed files from the Draft.files cache
48
+ const cachedFileObjects = cachedFiles.map(f =>
49
+ cache.readFragment({
50
+ id: cache.identify(f),
51
+ fragment: gql`
52
+ fragment DeletedFile on DatasetFile {
53
+ id
54
+ key
55
+ filename
56
+ directory
57
+ }
58
+ `,
59
+ }),
60
+ )
61
+ const remainingFiles = cachedFiles.filter(f => {
62
+ // Get the cache key for each file we have loaded
63
+ const file = cache.readFragment({
64
+ id: cache.identify(f),
65
+ fragment: gql`
66
+ fragment DeletedFile on DatasetFile {
67
+ id
68
+ key
69
+ filename
70
+ directory
71
+ }
72
+ `,
73
+ })
74
+ return fileCacheDeleteFilter(
75
+ file,
76
+ path,
77
+ filename,
78
+ cachedFileObjects,
79
+ )
80
+ })
81
+ return remainingFiles
82
+ },
83
+ },
84
+ })
85
+ }
86
+ },
87
+ })
88
+
89
+ return (
90
+ <span className="delete-file">
91
+ <WarnButton
92
+ message=""
93
+ iconOnly={true}
94
+ icon="fa-trash"
95
+ className="edit-file"
96
+ onConfirmedClick={() => {
97
+ deleteFiles({
98
+ variables: { datasetId, files: [{ path, filename }] },
99
+ })
100
+ }}
101
+ />
102
+ </span>
103
+ )
104
+ }
32
105
 
33
106
  DeleteFile.propTypes = {
34
107
  datasetId: PropTypes.string,
@@ -4,7 +4,7 @@ import ORCIDiDLogo from '../../assets/ORCIDiD_iconvector.svg'
4
4
  /**
5
5
  * Display component for usernames showing ORCID linking if connected
6
6
  */
7
- export const Username = ({ user }) => {
7
+ export const Username = ({ user }): JSX.Element => {
8
8
  if (user.orcid) {
9
9
  return (
10
10
  <>
@@ -15,6 +15,6 @@ export const Username = ({ user }) => {
15
15
  </>
16
16
  )
17
17
  } else {
18
- return user.name
18
+ return user.name as JSX.Element
19
19
  }
20
20
  }
@@ -1,43 +0,0 @@
1
- import React from 'react'
2
- import PropTypes from 'prop-types'
3
- import { gql } from '@apollo/client'
4
- import { Mutation } from '@apollo/client/react/components'
5
- import WarnButton from '../../common/forms/warn-button.jsx'
6
-
7
- export const DELETE_FILES = gql`
8
- mutation deleteFiles($datasetId: ID!, $files: [DeleteFile]!) {
9
- deleteFiles(datasetId: $datasetId, files: $files)
10
- }
11
- `
12
-
13
- const DeleteDir = ({ datasetId, path }) => (
14
- <Mutation mutation={DELETE_FILES} awaitRefetchQueries={true}>
15
- {deleteFiles => (
16
- <span className="delete-file">
17
- <WarnButton
18
- message="Delete"
19
- icon="fa-trash"
20
- warn={true}
21
- className="edit-file"
22
- action={cb => {
23
- deleteFiles({
24
- variables: {
25
- datasetId,
26
- files: [{ path }],
27
- },
28
- }).then(() => {
29
- cb()
30
- })
31
- }}
32
- />
33
- </span>
34
- )}
35
- </Mutation>
36
- )
37
-
38
- DeleteDir.propTypes = {
39
- datasetId: PropTypes.string,
40
- path: PropTypes.string,
41
- }
42
-
43
- export default DeleteDir
@@ -1,41 +0,0 @@
1
- import React from 'react'
2
- import PropTypes from 'prop-types'
3
- import { gql } from '@apollo/client'
4
- import { Mutation } from '@apollo/client/react/components'
5
- import WarnButton from '../../common/forms/warn-button.jsx'
6
-
7
- const DELETE_FILE = gql`
8
- mutation deleteFiles($datasetId: ID!, $files: [DeleteFile]) {
9
- deleteFiles(datasetId: $datasetId, files: $files)
10
- }
11
- `
12
-
13
- const DeleteFile = ({ datasetId, path, filename }) => (
14
- <Mutation mutation={DELETE_FILE} awaitRefetchQueries={true}>
15
- {deleteFiles => (
16
- <span className="delete-file">
17
- <WarnButton
18
- message="Delete"
19
- icon="fa-trash"
20
- warn={true}
21
- className="edit-file"
22
- action={cb => {
23
- deleteFiles({
24
- variables: { datasetId, files: [{ path, filename }] },
25
- }).then(() => {
26
- cb()
27
- })
28
- }}
29
- />
30
- </span>
31
- )}
32
- </Mutation>
33
- )
34
-
35
- DeleteFile.propTypes = {
36
- datasetId: PropTypes.string,
37
- path: PropTypes.string,
38
- filename: PropTypes.string,
39
- }
40
-
41
- export default DeleteFile
@@ -1,43 +0,0 @@
1
- import React from 'react'
2
- import PropTypes from 'prop-types'
3
- import { gql } from '@apollo/client'
4
- import { Mutation } from '@apollo/client/react/components'
5
- import { WarnButton } from '@openneuro/components/warn-button'
6
-
7
- export const DELETE_FILES = gql`
8
- mutation deleteFiles($datasetId: ID!, $files: [DeleteFile]!) {
9
- deleteFiles(datasetId: $datasetId, files: $files)
10
- }
11
- `
12
-
13
- const DeleteDir = ({ datasetId, path, name }) => (
14
- <Mutation mutation={DELETE_FILES} awaitRefetchQueries={true}>
15
- {deleteFiles => (
16
- <span className="delete-file">
17
- <WarnButton
18
- message=""
19
- iconOnly={true}
20
- icon="fa-trash"
21
- className="edit-file"
22
- tooltip={'Delete ' + name}
23
- onConfirmedClick={() => {
24
- deleteFiles({
25
- variables: {
26
- datasetId,
27
- files: [{ path }],
28
- },
29
- })
30
- }}
31
- />
32
- </span>
33
- )}
34
- </Mutation>
35
- )
36
-
37
- DeleteDir.propTypes = {
38
- datasetId: PropTypes.string,
39
- path: PropTypes.string,
40
- name: PropTypes.string,
41
- }
42
-
43
- export default DeleteDir