@standardnotes/authenticator 2.3.5
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/.babelrc +21 -0
- package/.browserlistrc +2 -0
- package/.eslintignore +3 -0
- package/.eslintrc +30 -0
- package/CHANGELOG.md +136 -0
- package/README.md +7 -0
- package/app/assets/svg/drag-indicator.svg +14 -0
- package/app/assets/svg/palette.svg +3 -0
- package/app/assets/svg/reorder-icon.svg +37 -0
- package/app/components/AuthEntry.jsx +202 -0
- package/app/components/AuthMenu.jsx +63 -0
- package/app/components/ConfirmDialog.jsx +37 -0
- package/app/components/CopyNotification.jsx +15 -0
- package/app/components/CountdownPie.jsx +131 -0
- package/app/components/DataErrorAlert.jsx +21 -0
- package/app/components/EditEntry.jsx +283 -0
- package/app/components/Home.jsx +365 -0
- package/app/components/QRCodeReader.jsx +91 -0
- package/app/components/ViewEntries.jsx +82 -0
- package/app/index.js +4 -0
- package/app/lib/otp.js +184 -0
- package/app/lib/utils.js +185 -0
- package/app/stylesheets/main.scss +443 -0
- package/editor.index.ejs +10 -0
- package/ext.json.sample +8 -0
- package/package.json +68 -0
- package/webpack.config.js +74 -0
- package/webpack.dev.js +23 -0
- package/webpack.prod.js +11 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import ConfirmDialog from '@Components/ConfirmDialog'
|
|
2
|
+
import DataErrorAlert from '@Components/DataErrorAlert'
|
|
3
|
+
import EditEntry from '@Components/EditEntry'
|
|
4
|
+
import ViewEntries from '@Components/ViewEntries'
|
|
5
|
+
import EditorKit from '@standardnotes/editor-kit'
|
|
6
|
+
import update from 'immutability-helper'
|
|
7
|
+
import React from 'react'
|
|
8
|
+
import ReorderIcon from '../assets/svg/reorder-icon.svg'
|
|
9
|
+
import CopyNotification from './CopyNotification'
|
|
10
|
+
|
|
11
|
+
const initialState = {
|
|
12
|
+
text: '',
|
|
13
|
+
entries: [],
|
|
14
|
+
parseError: false,
|
|
15
|
+
editMode: false,
|
|
16
|
+
editEntry: null,
|
|
17
|
+
confirmRemove: false,
|
|
18
|
+
confirmReorder: false,
|
|
19
|
+
displayCopy: false,
|
|
20
|
+
canEdit: true,
|
|
21
|
+
searchValue: '',
|
|
22
|
+
lastUpdated: 0,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default class Home extends React.Component {
|
|
26
|
+
constructor(props) {
|
|
27
|
+
super(props)
|
|
28
|
+
this.configureEditorKit()
|
|
29
|
+
this.state = initialState
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
configureEditorKit() {
|
|
33
|
+
const delegate = {
|
|
34
|
+
setEditorRawText: (text) => {
|
|
35
|
+
let parseError = false
|
|
36
|
+
let entries = []
|
|
37
|
+
|
|
38
|
+
if (text) {
|
|
39
|
+
try {
|
|
40
|
+
entries = this.parseNote(text)
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// Couldn't parse the content
|
|
43
|
+
parseError = true
|
|
44
|
+
this.setState({
|
|
45
|
+
parseError: true,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.setState({
|
|
51
|
+
...initialState,
|
|
52
|
+
text,
|
|
53
|
+
parseError,
|
|
54
|
+
entries,
|
|
55
|
+
})
|
|
56
|
+
},
|
|
57
|
+
generateCustomPreview: (text) => {
|
|
58
|
+
let entries = []
|
|
59
|
+
try {
|
|
60
|
+
entries = this.parseNote(text)
|
|
61
|
+
} finally {
|
|
62
|
+
// eslint-disable-next-line no-unsafe-finally
|
|
63
|
+
return {
|
|
64
|
+
html: `<div><strong>${entries.length}</strong> TokenVault Entries </div>`,
|
|
65
|
+
plain: `${entries.length} TokenVault Entries`,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
clearUndoHistory: () => {},
|
|
70
|
+
getElementsBySelector: () => [],
|
|
71
|
+
onNoteLockToggle: (isLocked) => {
|
|
72
|
+
this.setState({
|
|
73
|
+
canEdit: !isLocked,
|
|
74
|
+
})
|
|
75
|
+
},
|
|
76
|
+
onThemesChange: () => {
|
|
77
|
+
this.setState({
|
|
78
|
+
lastUpdated: Date.now(),
|
|
79
|
+
})
|
|
80
|
+
},
|
|
81
|
+
handleRequestForContentHeight: () => {
|
|
82
|
+
return document.body.scrollHeight
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.editorKit = new EditorKit(delegate, {
|
|
87
|
+
mode: 'json',
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
parseNote(text) {
|
|
92
|
+
const entries = JSON.parse(text)
|
|
93
|
+
|
|
94
|
+
if (entries instanceof Array) {
|
|
95
|
+
if (entries.length === 0) {
|
|
96
|
+
return []
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (!('service' in entry)) {
|
|
101
|
+
throw Error('Service key is missing for an entry.')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!('secret' in entry) && !('password' in entry)) {
|
|
105
|
+
throw Error('An entry does not have a secret key or a password.')
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return entries
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return []
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
saveNote(entries) {
|
|
116
|
+
this.editorKit.onEditorValueChanged(JSON.stringify(entries, null, 2))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Entry operations
|
|
120
|
+
addEntry = (entry) => {
|
|
121
|
+
this.setState((state) => {
|
|
122
|
+
const entries = state.entries.concat([entry])
|
|
123
|
+
this.saveNote(entries)
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
editMode: false,
|
|
127
|
+
editEntry: null,
|
|
128
|
+
entries,
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
editEntry = ({ id, entry }) => {
|
|
134
|
+
this.setState((state) => {
|
|
135
|
+
const entries = update(state.entries, { [id]: { $set: entry } })
|
|
136
|
+
this.saveNote(entries)
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
editMode: false,
|
|
140
|
+
editEntry: null,
|
|
141
|
+
entries,
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
removeEntry = (id) => {
|
|
147
|
+
this.setState((state) => {
|
|
148
|
+
const entries = update(state.entries, { $splice: [[id, 1]] })
|
|
149
|
+
this.saveNote(entries)
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
confirmRemove: false,
|
|
153
|
+
editEntry: null,
|
|
154
|
+
entries,
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Event Handlers
|
|
160
|
+
onAddNew = () => {
|
|
161
|
+
if (!this.state.canEdit) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
this.setState({
|
|
165
|
+
editMode: true,
|
|
166
|
+
editEntry: null,
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
onEdit = (id) => {
|
|
171
|
+
if (!this.state.canEdit) {
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
this.setState((state) => ({
|
|
175
|
+
editMode: true,
|
|
176
|
+
editEntry: {
|
|
177
|
+
id,
|
|
178
|
+
entry: state.entries[id],
|
|
179
|
+
},
|
|
180
|
+
}))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
onCancel = () => {
|
|
184
|
+
this.setState({
|
|
185
|
+
confirmRemove: false,
|
|
186
|
+
confirmReorder: false,
|
|
187
|
+
editMode: false,
|
|
188
|
+
editEntry: null,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
onRemove = (id) => {
|
|
193
|
+
if (!this.state.canEdit) {
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
this.setState((state) => ({
|
|
197
|
+
confirmRemove: true,
|
|
198
|
+
editEntry: {
|
|
199
|
+
id,
|
|
200
|
+
entry: state.entries[id],
|
|
201
|
+
},
|
|
202
|
+
}))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
onSave = ({ id, entry }) => {
|
|
206
|
+
// If there's no ID it's a new note
|
|
207
|
+
if (id != null) {
|
|
208
|
+
this.editEntry({ id, entry })
|
|
209
|
+
} else {
|
|
210
|
+
this.addEntry(entry)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
onCopyValue = () => {
|
|
215
|
+
this.setState({
|
|
216
|
+
displayCopy: true,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
if (this.clearTooltipTimer) {
|
|
220
|
+
clearTimeout(this.clearTooltipTimer)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this.clearTooltipTimer = setTimeout(() => {
|
|
224
|
+
this.setState({
|
|
225
|
+
displayCopy: false,
|
|
226
|
+
})
|
|
227
|
+
}, 2000)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
updateEntries = (entries) => {
|
|
231
|
+
this.saveNote(entries)
|
|
232
|
+
this.setState({
|
|
233
|
+
entries,
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
onReorderEntries = () => {
|
|
238
|
+
if (!this.state.canEdit) {
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
this.setState({
|
|
242
|
+
confirmReorder: true,
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
onSearchChange = (event) => {
|
|
247
|
+
const target = event.target
|
|
248
|
+
this.setState({
|
|
249
|
+
searchValue: target.value.toLowerCase(),
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
clearSearchValue = () => {
|
|
254
|
+
this.setState({
|
|
255
|
+
searchValue: '',
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
reorderEntries = () => {
|
|
260
|
+
const { entries } = this.state
|
|
261
|
+
const orderedEntries = entries.sort((a, b) => {
|
|
262
|
+
const serviceA = a.service.toLowerCase()
|
|
263
|
+
const serviceB = b.service.toLowerCase()
|
|
264
|
+
return serviceA < serviceB ? -1 : serviceA > serviceB ? 1 : 0
|
|
265
|
+
})
|
|
266
|
+
this.saveNote(orderedEntries)
|
|
267
|
+
this.setState({
|
|
268
|
+
entries: orderedEntries,
|
|
269
|
+
confirmReorder: false,
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
render() {
|
|
274
|
+
const editEntry = this.state.editEntry || {}
|
|
275
|
+
const {
|
|
276
|
+
canEdit,
|
|
277
|
+
displayCopy,
|
|
278
|
+
parseError,
|
|
279
|
+
editMode,
|
|
280
|
+
entries,
|
|
281
|
+
confirmRemove,
|
|
282
|
+
confirmReorder,
|
|
283
|
+
searchValue,
|
|
284
|
+
lastUpdated,
|
|
285
|
+
} = this.state
|
|
286
|
+
|
|
287
|
+
if (parseError) {
|
|
288
|
+
return (
|
|
289
|
+
<div className="sn-component">
|
|
290
|
+
<DataErrorAlert />
|
|
291
|
+
</div>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<div className="sn-component">
|
|
297
|
+
<CopyNotification isVisible={displayCopy} />
|
|
298
|
+
{!editMode && (
|
|
299
|
+
<div id="header">
|
|
300
|
+
<div className={`sk-horizontal-group left align-items-center ${!canEdit && 'full-width'}`}>
|
|
301
|
+
<input
|
|
302
|
+
name="search"
|
|
303
|
+
className="sk-input contrast search-bar"
|
|
304
|
+
placeholder="Search entries..."
|
|
305
|
+
value={searchValue}
|
|
306
|
+
onChange={this.onSearchChange}
|
|
307
|
+
autoComplete="off"
|
|
308
|
+
type="text"
|
|
309
|
+
/>
|
|
310
|
+
{searchValue && (
|
|
311
|
+
<div onClick={this.clearSearchValue} className="sk-button danger">
|
|
312
|
+
<div className="sk-label">✕</div>
|
|
313
|
+
</div>
|
|
314
|
+
)}
|
|
315
|
+
</div>
|
|
316
|
+
{canEdit && (
|
|
317
|
+
<div className="sk-horizontal-group right">
|
|
318
|
+
<div className="sk-button-group stretch">
|
|
319
|
+
<div onClick={this.onReorderEntries} className="sk-button info">
|
|
320
|
+
<ReorderIcon />
|
|
321
|
+
</div>
|
|
322
|
+
<div onClick={this.onAddNew} className="sk-button info">
|
|
323
|
+
<div className="sk-label">Add new</div>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
<div id="content">
|
|
331
|
+
{editMode ? (
|
|
332
|
+
<EditEntry id={editEntry.id} entry={editEntry.entry} onSave={this.onSave} onCancel={this.onCancel} />
|
|
333
|
+
) : (
|
|
334
|
+
<ViewEntries
|
|
335
|
+
entries={entries}
|
|
336
|
+
searchValue={searchValue}
|
|
337
|
+
onEdit={this.onEdit}
|
|
338
|
+
onRemove={this.onRemove}
|
|
339
|
+
onCopyValue={this.onCopyValue}
|
|
340
|
+
canEdit={canEdit}
|
|
341
|
+
lastUpdated={lastUpdated}
|
|
342
|
+
updateEntries={this.updateEntries}
|
|
343
|
+
/>
|
|
344
|
+
)}
|
|
345
|
+
{confirmRemove && (
|
|
346
|
+
<ConfirmDialog
|
|
347
|
+
title={`Remove ${editEntry.entry.service}`}
|
|
348
|
+
message="Are you sure you want to remove this entry?"
|
|
349
|
+
onConfirm={() => this.removeEntry(editEntry.id)}
|
|
350
|
+
onCancel={this.onCancel}
|
|
351
|
+
/>
|
|
352
|
+
)}
|
|
353
|
+
{confirmReorder && (
|
|
354
|
+
<ConfirmDialog
|
|
355
|
+
title={'Auto-sort entries'}
|
|
356
|
+
message="Are you sure you want to auto-sort all entries alphabetically based on service name?"
|
|
357
|
+
onConfirm={this.reorderEntries}
|
|
358
|
+
onCancel={this.onCancel}
|
|
359
|
+
/>
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { parseKeyUri } from '@Lib/otp'
|
|
2
|
+
import jsQR from 'jsqr'
|
|
3
|
+
import PropTypes from 'prop-types'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
|
|
6
|
+
const convertToGrayScale = (imageData) => {
|
|
7
|
+
if (!imageData) {
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
12
|
+
const count = imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]
|
|
13
|
+
let color = 0
|
|
14
|
+
|
|
15
|
+
if (count > 510) {
|
|
16
|
+
color = 255
|
|
17
|
+
} else if (count > 255) {
|
|
18
|
+
color = 127.5
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
imageData.data[i] = color
|
|
22
|
+
imageData.data[i + 1] = color
|
|
23
|
+
imageData.data[i + 2] = color
|
|
24
|
+
imageData.data[i + 3] = 255
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return imageData
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default class QRCodeReader extends React.Component {
|
|
31
|
+
constructor() {
|
|
32
|
+
super()
|
|
33
|
+
|
|
34
|
+
this.fileInputRef = React.createRef(null)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onImageSelected = (evt) => {
|
|
38
|
+
const file = evt.target.files[0]
|
|
39
|
+
const url = URL.createObjectURL(file)
|
|
40
|
+
const img = new Image()
|
|
41
|
+
const self = this
|
|
42
|
+
|
|
43
|
+
img.onload = function () {
|
|
44
|
+
URL.revokeObjectURL(this.src)
|
|
45
|
+
|
|
46
|
+
const canvas = document.createElement('canvas')
|
|
47
|
+
const context = canvas.getContext('2d')
|
|
48
|
+
canvas.width = this.width
|
|
49
|
+
canvas.height = this.height
|
|
50
|
+
context.drawImage(this, 0, 0)
|
|
51
|
+
|
|
52
|
+
let imageData = context.getImageData(0, 0, this.width, this.height)
|
|
53
|
+
imageData = convertToGrayScale(imageData)
|
|
54
|
+
|
|
55
|
+
const code = jsQR(imageData.data, imageData.width, imageData.height)
|
|
56
|
+
|
|
57
|
+
const { onError, onSuccess } = self.props
|
|
58
|
+
|
|
59
|
+
if (code) {
|
|
60
|
+
const otpData = parseKeyUri(code.data)
|
|
61
|
+
if (otpData.type !== 'totp') {
|
|
62
|
+
onError(`The '${otpData.type}' type is not supported.`)
|
|
63
|
+
} else {
|
|
64
|
+
onSuccess(otpData)
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
onError('Error reading QR code from image. Please try again.')
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
img.src = url
|
|
72
|
+
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
render() {
|
|
77
|
+
return (
|
|
78
|
+
<div className="qr-code-reader-container">
|
|
79
|
+
<div className="sk-button info" onClick={() => this.fileInputRef.current.click()}>
|
|
80
|
+
<div className="sk-label">Upload QR Code</div>
|
|
81
|
+
<input type="file" style={{ display: 'none' }} ref={this.fileInputRef} onChange={this.onImageSelected} />
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
QRCodeReader.propTypes = {
|
|
89
|
+
onError: PropTypes.func.isRequired,
|
|
90
|
+
onSuccess: PropTypes.func.isRequired,
|
|
91
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import AuthEntry from '@Components/AuthEntry'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'
|
|
4
|
+
|
|
5
|
+
const reorderEntries = (list, startIndex, endIndex) => {
|
|
6
|
+
const result = Array.from(list)
|
|
7
|
+
const [removed] = result.splice(startIndex, 1)
|
|
8
|
+
result.splice(endIndex, 0, removed)
|
|
9
|
+
|
|
10
|
+
return result
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ViewEntries = ({ entries, onEdit, onRemove, onCopyValue, canEdit, updateEntries, searchValue, lastUpdated }) => {
|
|
14
|
+
const onDragEnd = (result) => {
|
|
15
|
+
const droppedOutsideList = !result.destination
|
|
16
|
+
if (droppedOutsideList) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const orderedEntries = reorderEntries(entries, result.source.index, result.destination.index)
|
|
21
|
+
|
|
22
|
+
updateEntries(orderedEntries)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<DragDropContext onDragEnd={onDragEnd}>
|
|
27
|
+
<Droppable droppableId="droppable" isDropDisabled={!canEdit}>
|
|
28
|
+
{(provided) => (
|
|
29
|
+
<div {...provided.droppableProps} ref={provided.innerRef} className="auth-list">
|
|
30
|
+
{entries.map((entry, index) => {
|
|
31
|
+
/**
|
|
32
|
+
* Filtering entries by account, service and notes properties.
|
|
33
|
+
*/
|
|
34
|
+
const combinedString = `${entry.account}${entry.service}${entry.notes}`.toLowerCase()
|
|
35
|
+
if (searchValue && !combinedString.includes(searchValue)) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
return (
|
|
39
|
+
<Draggable
|
|
40
|
+
key={`${entry.service}-${index}`}
|
|
41
|
+
draggableId={`${entry.service}-${index}`}
|
|
42
|
+
index={index}
|
|
43
|
+
isDragDisabled={!canEdit}
|
|
44
|
+
>
|
|
45
|
+
{(provided) => (
|
|
46
|
+
<AuthEntry
|
|
47
|
+
{...provided.draggableProps}
|
|
48
|
+
{...provided.dragHandleProps}
|
|
49
|
+
innerRef={provided.innerRef}
|
|
50
|
+
key={index}
|
|
51
|
+
id={index}
|
|
52
|
+
entry={entry}
|
|
53
|
+
onEdit={onEdit}
|
|
54
|
+
onRemove={onRemove}
|
|
55
|
+
onCopyValue={onCopyValue}
|
|
56
|
+
canEdit={canEdit}
|
|
57
|
+
lastUpdated={lastUpdated}
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
|
+
</Draggable>
|
|
61
|
+
)
|
|
62
|
+
})}
|
|
63
|
+
{provided.placeholder}
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
</Droppable>
|
|
67
|
+
</DragDropContext>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
ViewEntries.propTypes = {
|
|
72
|
+
entries: PropTypes.arrayOf(PropTypes.object),
|
|
73
|
+
onEdit: PropTypes.func.isRequired,
|
|
74
|
+
onRemove: PropTypes.func.isRequired,
|
|
75
|
+
onCopyValue: PropTypes.func.isRequired,
|
|
76
|
+
canEdit: PropTypes.bool.isRequired,
|
|
77
|
+
lastUpdated: PropTypes.number.isRequired,
|
|
78
|
+
updateEntries: PropTypes.func.isRequired,
|
|
79
|
+
searchValue: PropTypes.string,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default ViewEntries
|
package/app/index.js
ADDED