@oslokommune/punkt-react 16.13.3 → 16.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/dist/{dialog-polyfill.esm-1hPr0gl9-EorOF9Wq.js → dialog-polyfill.esm-BmVHGRTX-CuhXqEqJ.js} +1 -1
- package/dist/index.d.ts +50 -47
- package/dist/punkt-react.es.js +4560 -3038
- package/dist/punkt-react.umd.js +670 -274
- package/dist/shared-types/fileupload.d.ts +73 -0
- package/dist/shared-types/index.d.ts +2 -0
- package/dist/shared-utils/fileupload/announcements.d.ts +11 -0
- package/dist/shared-utils/fileupload/filename.d.ts +13 -0
- package/dist/shared-utils/fileupload/focus-trap.d.ts +12 -0
- package/dist/shared-utils/fileupload/formats.d.ts +30 -0
- package/dist/shared-utils/fileupload/index.d.ts +15 -0
- package/dist/shared-utils/fileupload/navigation.d.ts +5 -0
- package/dist/shared-utils/fileupload/size.d.ts +12 -0
- package/dist/shared-utils/fileupload/transfers.d.ts +17 -0
- package/dist/shared-utils/fileupload/truncate.d.ts +13 -0
- package/dist/shared-utils/fileupload/validation.d.ts +24 -0
- package/package.json +4 -4
- package/src/components/fileupload/DropZone.tsx +35 -22
- package/src/components/fileupload/FileUpload.test.tsx +165 -10
- package/src/components/fileupload/FileUpload.tsx +116 -184
- package/src/components/fileupload/QueueDisplay.test.tsx +51 -8
- package/src/components/fileupload/QueueDisplay.tsx +71 -99
- package/src/components/fileupload/QueueItemContent.tsx +120 -135
- package/src/components/fileupload/Subcomponents.tsx +175 -144
- package/src/components/fileupload/Truncate.test.tsx +65 -207
- package/src/components/fileupload/Truncate.tsx +24 -120
- package/src/components/fileupload/extensions/Comments.tsx +94 -106
- package/src/components/fileupload/extensions/Remove.tsx +3 -5
- package/src/components/fileupload/extensions/Rename.tsx +45 -42
- package/src/components/fileupload/hooks/index.ts +1 -0
- package/src/components/fileupload/hooks/useFileAttributes.ts +37 -29
- package/src/components/fileupload/hooks/useImagePreview.ts +3 -7
- package/src/components/fileupload/hooks/useOperationState.ts +29 -8
- package/src/components/fileupload/hooks/useRequiredFormValidation.ts +43 -0
- package/src/components/fileupload/hooks.ts +1 -0
- package/src/components/fileupload/types.ts +49 -32
- package/src/components/fileupload/utils.ts +3 -54
- package/src/components/inputwrapper/InputWrapper.tsx +4 -1
- package/src/components/modal/Modal.tsx +27 -15
- package/src/components/searchinput/SearchInput.tsx +14 -13
- package/src/components/types.ts +1 -1
- package/src/components/fileupload/texts.ts +0 -20
|
@@ -2,17 +2,19 @@ import { useContext, useLayoutEffect, useRef } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { PktButton } from '../../button/Button'
|
|
4
4
|
import { PktIcon } from '../../icon/Icon'
|
|
5
|
-
import { TFileAttributes, FileItem, PktFileUploadContext, TQueueItemOperation } from '../types'
|
|
6
5
|
import { PktTextarea } from '../../textarea/Textarea'
|
|
6
|
+
import { FileItem, PktFileUploadContext, TQueueItemOperation, TQueueOperationContext } from '../types'
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const COMMENTS_ATTRIBUTE = 'comments'
|
|
9
9
|
|
|
10
10
|
export interface IComment {
|
|
11
11
|
text: string
|
|
12
|
-
timestamp:
|
|
12
|
+
timestamp: string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const formatTimestamp = (
|
|
15
|
+
const formatTimestamp = (iso: string): string => {
|
|
16
|
+
const date = new Date(iso)
|
|
17
|
+
if (Number.isNaN(date.getTime())) return iso
|
|
16
18
|
const day = String(date.getDate()).padStart(2, '0')
|
|
17
19
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
18
20
|
const year = date.getFullYear()
|
|
@@ -23,11 +25,13 @@ const formatTimestamp = (date: Date): string => {
|
|
|
23
25
|
|
|
24
26
|
const AddComment = ({
|
|
25
27
|
fileItem,
|
|
28
|
+
disabled,
|
|
26
29
|
closeOperationUi,
|
|
27
30
|
onAddComment,
|
|
28
31
|
existingComment,
|
|
29
32
|
}: {
|
|
30
33
|
fileItem: FileItem
|
|
34
|
+
disabled: boolean
|
|
31
35
|
closeOperationUi: () => void
|
|
32
36
|
onAddComment: (comment: IComment) => void
|
|
33
37
|
existingComment?: IComment
|
|
@@ -38,10 +42,7 @@ const AddComment = ({
|
|
|
38
42
|
const handleAddComment = () => {
|
|
39
43
|
const text = inputRef.current?.value?.trim()
|
|
40
44
|
if (text) {
|
|
41
|
-
onAddComment({
|
|
42
|
-
text,
|
|
43
|
-
timestamp: new Date(),
|
|
44
|
-
})
|
|
45
|
+
onAddComment({ text, timestamp: new Date().toISOString() })
|
|
45
46
|
}
|
|
46
47
|
closeOperationUi()
|
|
47
48
|
}
|
|
@@ -62,127 +63,114 @@ const AddComment = ({
|
|
|
62
63
|
rows={2}
|
|
63
64
|
id={`comment-${fileItem.fileId}`}
|
|
64
65
|
ref={inputRef}
|
|
66
|
+
disabled={disabled}
|
|
65
67
|
defaultValue={existingComment?.text}
|
|
66
68
|
/>
|
|
67
69
|
|
|
68
|
-
<PktButton skin="secondary" size="small" onClick={handleAddComment}>
|
|
70
|
+
<PktButton skin="secondary" size="small" disabled={disabled} onClick={handleAddComment}>
|
|
69
71
|
{isEditing ? 'Lagre kommentar' : 'Legg til kommentar'}
|
|
70
72
|
</PktButton>
|
|
71
|
-
<PktButton skin="tertiary" size="small" onClick={closeOperationUi}>
|
|
73
|
+
<PktButton skin="tertiary" size="small" disabled={disabled} onClick={closeOperationUi}>
|
|
72
74
|
Avbryt
|
|
73
75
|
</PktButton>
|
|
74
76
|
</>
|
|
75
77
|
)
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
+
const ShowComment = ({
|
|
81
|
+
comment,
|
|
82
|
+
disabled,
|
|
80
83
|
onDeleteComment,
|
|
81
84
|
onEditComment,
|
|
82
85
|
}: {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
</button>
|
|
119
|
-
)}
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
122
|
-
))}
|
|
86
|
+
comment: IComment
|
|
87
|
+
disabled: boolean
|
|
88
|
+
onDeleteComment: () => void
|
|
89
|
+
onEditComment: () => void
|
|
90
|
+
}) => (
|
|
91
|
+
<div className="pkt-fileupload__queue-display__item__comments">
|
|
92
|
+
<div className="pkt-fileupload__queue-display__item__comment">
|
|
93
|
+
<div className="pkt-fileupload__queue-display__item__comment__content">
|
|
94
|
+
<span className="pkt-fileupload__queue-display__item__comment__text" aria-label="Kommentar tekst">
|
|
95
|
+
{comment.text}
|
|
96
|
+
</span>
|
|
97
|
+
<time className="pkt-fileupload__queue-display__item__comment__time">
|
|
98
|
+
{formatTimestamp(comment.timestamp)}
|
|
99
|
+
</time>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="pkt-fileupload__queue-display__item__comment__actions">
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
className="pkt-fileupload__queue-display__item__comment__action"
|
|
105
|
+
onClick={onEditComment}
|
|
106
|
+
disabled={disabled}
|
|
107
|
+
aria-label="Rediger kommentar"
|
|
108
|
+
>
|
|
109
|
+
<PktIcon name="edit" />
|
|
110
|
+
</button>
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
className="pkt-fileupload__queue-display__item__comment__action"
|
|
114
|
+
onClick={onDeleteComment}
|
|
115
|
+
disabled={disabled}
|
|
116
|
+
aria-label="Slett kommentar"
|
|
117
|
+
>
|
|
118
|
+
<PktIcon name="trash-can" />
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
123
121
|
</div>
|
|
124
|
-
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
125
124
|
|
|
126
|
-
const CommentHiddenInput = (
|
|
125
|
+
const CommentHiddenInput = ({ comments }: { comments: IComment[] | undefined }) => {
|
|
127
126
|
const context = useContext(PktFileUploadContext)
|
|
128
127
|
return (
|
|
129
128
|
<input
|
|
130
129
|
type="hidden"
|
|
131
130
|
name={`${context.name}-comments`}
|
|
132
|
-
value={
|
|
131
|
+
value={comments ? JSON.stringify(comments) : ''}
|
|
133
132
|
/>
|
|
134
133
|
)
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
comments={comments}
|
|
178
|
-
onDeleteComment={(index) => deleteComment(fileItem.fileId, index)}
|
|
179
|
-
onEditComment={activateOperation ? () => activateOperation() : undefined}
|
|
180
|
-
/>
|
|
181
|
-
)
|
|
182
|
-
},
|
|
183
|
-
renderHidden: (fileItem: FileItem) => (
|
|
184
|
-
<CommentHiddenInput key={`comments${fileItem.fileId}`} comments={commentsAttribute.get(fileItem.fileId)} />
|
|
185
|
-
),
|
|
186
|
-
symbol: COMMENT_SYMBOL,
|
|
187
|
-
}
|
|
136
|
+
const getComments = (context: TQueueOperationContext): IComment[] =>
|
|
137
|
+
context.getAttribute<IComment[]>(COMMENTS_ATTRIBUTE) ?? []
|
|
138
|
+
|
|
139
|
+
export const addCommentOperation: TQueueItemOperation = {
|
|
140
|
+
id: 'comment',
|
|
141
|
+
title: (fileItem) => {
|
|
142
|
+
const comments = (fileItem.attributes?.[COMMENTS_ATTRIBUTE] as IComment[] | undefined) ?? []
|
|
143
|
+
return comments.length > 0 ? '' : 'Legg til kommentar'
|
|
144
|
+
},
|
|
145
|
+
ariaLabel: 'Legg til kommentar',
|
|
146
|
+
renderExtendedUI: (context) => {
|
|
147
|
+
const existingComment = getComments(context)[0]
|
|
148
|
+
return (
|
|
149
|
+
<AddComment
|
|
150
|
+
fileItem={context.file}
|
|
151
|
+
disabled={context.disabled}
|
|
152
|
+
closeOperationUi={context.close}
|
|
153
|
+
onAddComment={(comment) => {
|
|
154
|
+
context.setAttribute(COMMENTS_ATTRIBUTE, [comment])
|
|
155
|
+
}}
|
|
156
|
+
existingComment={existingComment}
|
|
157
|
+
/>
|
|
158
|
+
)
|
|
159
|
+
},
|
|
160
|
+
renderContent: (context) => {
|
|
161
|
+
if (context.isActive) return null
|
|
162
|
+
const comment = getComments(context)[0]
|
|
163
|
+
if (!comment) return null
|
|
164
|
+
return (
|
|
165
|
+
<ShowComment
|
|
166
|
+
comment={comment}
|
|
167
|
+
disabled={context.disabled}
|
|
168
|
+
onEditComment={context.activate}
|
|
169
|
+
onDeleteComment={() => context.setAttribute(COMMENTS_ATTRIBUTE, undefined)}
|
|
170
|
+
/>
|
|
171
|
+
)
|
|
172
|
+
},
|
|
173
|
+
renderHidden: (context) => (
|
|
174
|
+
<CommentHiddenInput key={`comments${context.file.fileId}`} comments={context.getAttribute<IComment[]>(COMMENTS_ATTRIBUTE)} />
|
|
175
|
+
),
|
|
188
176
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { TFileId,
|
|
2
|
-
|
|
3
|
-
const DELETE_SYMBOL = Symbol('deleteFile')
|
|
1
|
+
import type { TFileId, TQueueItemOperation } from '@/components/fileupload/types'
|
|
4
2
|
|
|
5
3
|
export const removeFileOperation = (onFileRemoved: (fileId: TFileId) => void): TQueueItemOperation => ({
|
|
4
|
+
id: 'remove',
|
|
6
5
|
title: 'Slett',
|
|
7
6
|
ariaLabel: 'Slett fil',
|
|
8
|
-
onClick: (
|
|
9
|
-
symbol: DELETE_SYMBOL,
|
|
7
|
+
onClick: ({ file }) => onFileRemoved(file.fileId),
|
|
10
8
|
})
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { useContext, useId, useRef } from 'react'
|
|
2
2
|
|
|
3
|
+
import { getDisplayFilename } from 'shared-utils/fileupload'
|
|
4
|
+
|
|
3
5
|
import { PktButton } from '../../button/Button'
|
|
4
|
-
import {
|
|
6
|
+
import { PktFileUploadContext, TQueueItemOperation, TQueueOperationContext } from '../types'
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
interface IRenameFileProps {
|
|
9
|
+
initialValue: string
|
|
10
|
+
disabled: boolean
|
|
11
|
+
onSave: (newName: string) => void
|
|
12
|
+
onCancel: () => void
|
|
13
|
+
}
|
|
7
14
|
|
|
8
|
-
const RenameFile = (
|
|
15
|
+
const RenameFile = ({ initialValue, disabled, onSave, onCancel }: IRenameFileProps) => {
|
|
9
16
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
10
17
|
const inputId = useId()
|
|
11
18
|
|
|
12
|
-
const save = () =>
|
|
13
|
-
props.onSave(inputRef.current?.value ?? '')
|
|
14
|
-
}
|
|
19
|
+
const save = () => onSave(inputRef.current?.value ?? '')
|
|
15
20
|
|
|
16
21
|
return (
|
|
17
22
|
<>
|
|
@@ -23,57 +28,55 @@ const RenameFile = (props: { value: string | undefined; onSave: (newName: string
|
|
|
23
28
|
ref={inputRef}
|
|
24
29
|
type="text"
|
|
25
30
|
autoFocus
|
|
26
|
-
|
|
31
|
+
disabled={disabled}
|
|
32
|
+
defaultValue={initialValue}
|
|
27
33
|
className="pkt-fileupload__queue-display__item__rename-input"
|
|
28
|
-
onKeyDown={(
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
onKeyDown={(event) => {
|
|
35
|
+
if (event.key === 'Enter') {
|
|
36
|
+
event.preventDefault()
|
|
37
|
+
event.stopPropagation()
|
|
32
38
|
save()
|
|
33
39
|
}
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
if (event.key === 'Escape') {
|
|
41
|
+
event.preventDefault()
|
|
42
|
+
event.stopPropagation()
|
|
43
|
+
onCancel()
|
|
38
44
|
}
|
|
39
45
|
}}
|
|
40
46
|
/>
|
|
41
|
-
<PktButton skin="secondary" size="small" onClick={
|
|
47
|
+
<PktButton skin="secondary" size="small" disabled={disabled} onClick={save}>
|
|
42
48
|
Lagre
|
|
43
49
|
</PktButton>
|
|
44
|
-
<PktButton skin="tertiary" size="small" onClick={
|
|
50
|
+
<PktButton skin="tertiary" size="small" disabled={disabled} onClick={onCancel}>
|
|
45
51
|
Avbryt
|
|
46
52
|
</PktButton>
|
|
47
53
|
</>
|
|
48
54
|
)
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
const RenameHiddenInput = (
|
|
57
|
+
const RenameHiddenInput = ({ targetFilename }: { targetFilename: string }) => {
|
|
52
58
|
const context = useContext(PktFileUploadContext)
|
|
53
|
-
return <input type="hidden" name={`${context.name}-targetFilename`} value={
|
|
59
|
+
return <input type="hidden" name={`${context.name}-targetFilename`} value={targetFilename} />
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
),
|
|
77
|
-
symbol: RENAME_SYMBOL,
|
|
78
|
-
}
|
|
62
|
+
const resolveCurrentName = (context: TQueueOperationContext) =>
|
|
63
|
+
context.getAttribute<string>('targetFilename') || getDisplayFilename(context.file, '')
|
|
64
|
+
|
|
65
|
+
export const renameFileOperation: TQueueItemOperation = {
|
|
66
|
+
id: 'rename',
|
|
67
|
+
title: 'Rediger',
|
|
68
|
+
ariaLabel: 'Rediger filnavn',
|
|
69
|
+
renderInlineUI: (context) => (
|
|
70
|
+
<RenameFile
|
|
71
|
+
initialValue={resolveCurrentName(context)}
|
|
72
|
+
disabled={context.disabled}
|
|
73
|
+
onSave={(newName: string) => {
|
|
74
|
+
const trimmed = newName.trim()
|
|
75
|
+
if (trimmed) context.setAttribute('targetFilename', trimmed)
|
|
76
|
+
context.close()
|
|
77
|
+
}}
|
|
78
|
+
onCancel={context.close}
|
|
79
|
+
/>
|
|
80
|
+
),
|
|
81
|
+
renderHidden: (context) => <RenameHiddenInput key={`rename${context.file.fileId}`} targetFilename={resolveCurrentName(context)} />,
|
|
79
82
|
}
|
|
@@ -1,46 +1,54 @@
|
|
|
1
|
-
import { useCallback } from 'react'
|
|
1
|
+
import { useCallback, useMemo } from 'react'
|
|
2
2
|
|
|
3
|
-
import { TFileAttributes, TFileId,
|
|
3
|
+
import { FileItem, TFileAttributes, TFileId, TFileItemList, TQueueOperationContext } from '../types'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Build accessors that let queue-item operations read and write per-file
|
|
7
|
+
* attributes without ever mutating the current `value` list. All changes are
|
|
8
|
+
* propagated via `onFileUpdated`, so controlled parents stay authoritative.
|
|
9
|
+
*/
|
|
5
10
|
export const useFileAttributes = (
|
|
6
11
|
value: TFileItemList,
|
|
7
|
-
onFileUpdated: (
|
|
12
|
+
onFileUpdated: (fileId: TFileId, updates: Partial<FileItem>) => void,
|
|
8
13
|
) => {
|
|
14
|
+
const getAttributeForFile = useCallback(
|
|
15
|
+
<T>(fileId: TFileId, attributeName: string): T | undefined => {
|
|
16
|
+
const fileItem = value.find((file) => file.fileId === fileId)
|
|
17
|
+
return fileItem?.attributes?.[attributeName] as T | undefined
|
|
18
|
+
},
|
|
19
|
+
[value],
|
|
20
|
+
)
|
|
21
|
+
|
|
9
22
|
const setAttributeForFile = useCallback(
|
|
10
|
-
(
|
|
11
|
-
const fileItem = value.find((file) => file.fileId ===
|
|
12
|
-
|
|
13
|
-
fileItem.attributes
|
|
14
|
-
|
|
15
|
-
[attributeName]
|
|
23
|
+
(fileId: TFileId, attributeName: string, attributeValue: unknown) => {
|
|
24
|
+
const fileItem = value.find((file) => file.fileId === fileId)
|
|
25
|
+
if (!fileItem) return
|
|
26
|
+
const nextAttributes = { ...fileItem.attributes, [attributeName]: attributeValue }
|
|
27
|
+
if (attributeValue === undefined) {
|
|
28
|
+
delete nextAttributes[attributeName]
|
|
16
29
|
}
|
|
17
|
-
onFileUpdated(
|
|
30
|
+
onFileUpdated(fileId, { attributes: nextAttributes as FileItem['attributes'] })
|
|
18
31
|
},
|
|
19
32
|
[onFileUpdated, value],
|
|
20
33
|
)
|
|
21
34
|
|
|
22
|
-
const
|
|
23
|
-
(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
const fileAttributes: TFileAttributes = useMemo(
|
|
36
|
+
() =>
|
|
37
|
+
<T>(attributeName: string) => ({
|
|
38
|
+
get: (fileId: TFileId) => getAttributeForFile<T>(fileId, attributeName),
|
|
39
|
+
set: (fileId: TFileId, attributeValue: T | undefined) =>
|
|
40
|
+
setAttributeForFile(fileId, attributeName, attributeValue),
|
|
41
|
+
}),
|
|
42
|
+
[getAttributeForFile, setAttributeForFile],
|
|
29
43
|
)
|
|
30
44
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
},
|
|
37
|
-
set: (fileId: TFileId, attributeValue: T | undefined) => {
|
|
38
|
-
setAttributeForFile(fileId, attributeName, attributeValue)
|
|
39
|
-
},
|
|
40
|
-
}
|
|
41
|
-
},
|
|
45
|
+
const buildAttributeAccessors = useCallback(
|
|
46
|
+
(fileId: TFileId): Pick<TQueueOperationContext, 'getAttribute' | 'setAttribute'> => ({
|
|
47
|
+
getAttribute: <T>(name: string) => getAttributeForFile<T>(fileId, name),
|
|
48
|
+
setAttribute: (name, attributeValue) => setAttributeForFile(fileId, name, attributeValue),
|
|
49
|
+
}),
|
|
42
50
|
[getAttributeForFile, setAttributeForFile],
|
|
43
51
|
)
|
|
44
52
|
|
|
45
|
-
return { fileAttributes } as const
|
|
53
|
+
return { fileAttributes, buildAttributeAccessors } as const
|
|
46
54
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useCallback, useRef, useState } from 'react'
|
|
2
2
|
|
|
3
|
+
import { navigateCyclic } from 'shared-utils/fileupload'
|
|
4
|
+
|
|
3
5
|
import { TFileAndTransfer, TFileId } from '../types'
|
|
4
6
|
|
|
5
7
|
export const useImagePreview = (previewableImages: TFileAndTransfer[]) => {
|
|
@@ -27,13 +29,7 @@ export const useImagePreview = (previewableImages: TFileAndTransfer[]) => {
|
|
|
27
29
|
const navigate = useCallback(
|
|
28
30
|
(direction: 'prev' | 'next') => {
|
|
29
31
|
if (previewableImages.length === 0) return
|
|
30
|
-
|
|
31
|
-
setCurrentIndex((prev) => {
|
|
32
|
-
if (direction === 'prev') {
|
|
33
|
-
return prev <= 0 ? previewableImages.length - 1 : prev - 1
|
|
34
|
-
}
|
|
35
|
-
return prev >= previewableImages.length - 1 ? 0 : prev + 1
|
|
36
|
-
})
|
|
32
|
+
setCurrentIndex((prev) => navigateCyclic(prev, direction, previewableImages.length))
|
|
37
33
|
},
|
|
38
34
|
[previewableImages.length],
|
|
39
35
|
)
|
|
@@ -1,25 +1,46 @@
|
|
|
1
|
-
import { useCallback, useState } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import { TFileId, TQueueItemOperation } from '../types'
|
|
4
4
|
|
|
5
5
|
export const useOperationState = (queueItemOperations: TQueueItemOperation[]) => {
|
|
6
|
-
const [
|
|
6
|
+
const [activatedIds, setActivatedIds] = useState<Record<TFileId, string>>({})
|
|
7
|
+
|
|
8
|
+
// If an operation is removed from the list while still referenced as "activated",
|
|
9
|
+
// the stored id would re-activate a freshly-added operation with the same id
|
|
10
|
+
// (e.g. an extension re-mounted). Drop any stale entries on list changes so the
|
|
11
|
+
// activation state can never refer to an operation that no longer exists.
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
setActivatedIds((prev) => {
|
|
14
|
+
const validIds = new Set(queueItemOperations.map((op) => op.id))
|
|
15
|
+
let changed = false
|
|
16
|
+
const next: Record<TFileId, string> = {}
|
|
17
|
+
for (const [fileId, opId] of Object.entries(prev)) {
|
|
18
|
+
if (validIds.has(opId)) {
|
|
19
|
+
next[fileId] = opId
|
|
20
|
+
} else {
|
|
21
|
+
changed = true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return changed ? next : prev
|
|
25
|
+
})
|
|
26
|
+
}, [queueItemOperations])
|
|
7
27
|
|
|
8
28
|
const getActivated = useCallback(
|
|
9
29
|
(fileId: TFileId): TQueueItemOperation | undefined => {
|
|
10
|
-
const
|
|
11
|
-
if (!
|
|
12
|
-
return queueItemOperations.find((op) => op.
|
|
30
|
+
const id = activatedIds[fileId]
|
|
31
|
+
if (!id) return undefined
|
|
32
|
+
return queueItemOperations.find((op) => op.id === id)
|
|
13
33
|
},
|
|
14
|
-
[
|
|
34
|
+
[activatedIds, queueItemOperations],
|
|
15
35
|
)
|
|
16
36
|
|
|
17
37
|
const activate = useCallback((fileId: TFileId, operation: TQueueItemOperation) => {
|
|
18
|
-
|
|
38
|
+
setActivatedIds((prev) => ({ ...prev, [fileId]: operation.id }))
|
|
19
39
|
}, [])
|
|
20
40
|
|
|
21
41
|
const close = useCallback((fileId: TFileId) => {
|
|
22
|
-
|
|
42
|
+
setActivatedIds((prev) => {
|
|
43
|
+
if (!(fileId in prev)) return prev
|
|
23
44
|
const updated = { ...prev }
|
|
24
45
|
delete updated[fileId]
|
|
25
46
|
return updated
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { RefObject, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* When `uploadStrategy="custom"` and the upload is marked `required`, the
|
|
5
|
+
* native `required` attribute can't do the work (the input isn't posted).
|
|
6
|
+
* This hook attaches a submit listener to the containing `<form>` that
|
|
7
|
+
* prevents submission if the queue is empty, and surfaces a validation
|
|
8
|
+
* error via `onMissingFiles`.
|
|
9
|
+
*
|
|
10
|
+
* Passive in `form` strategy — native validation handles that case.
|
|
11
|
+
*/
|
|
12
|
+
export const useRequiredFormValidation = (
|
|
13
|
+
fileInputRef: RefObject<HTMLInputElement>,
|
|
14
|
+
{
|
|
15
|
+
uploadStrategy,
|
|
16
|
+
required,
|
|
17
|
+
currentFileCount,
|
|
18
|
+
onMissingFiles,
|
|
19
|
+
}: {
|
|
20
|
+
uploadStrategy: 'form' | 'custom'
|
|
21
|
+
required: boolean
|
|
22
|
+
currentFileCount: number
|
|
23
|
+
onMissingFiles: () => void
|
|
24
|
+
},
|
|
25
|
+
) => {
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (uploadStrategy !== 'custom' || !required) return
|
|
28
|
+
const input = fileInputRef.current
|
|
29
|
+
const form = input?.form
|
|
30
|
+
if (!form) return
|
|
31
|
+
|
|
32
|
+
const handleFormSubmit = (event: SubmitEvent) => {
|
|
33
|
+
if (currentFileCount > 0) return
|
|
34
|
+
event.preventDefault()
|
|
35
|
+
onMissingFiles()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
form.addEventListener('submit', handleFormSubmit)
|
|
39
|
+
return () => {
|
|
40
|
+
form.removeEventListener('submit', handleFormSubmit)
|
|
41
|
+
}
|
|
42
|
+
}, [fileInputRef, uploadStrategy, required, currentFileCount, onMissingFiles])
|
|
43
|
+
}
|