@oslokommune/punkt-react 15.0.4 → 15.2.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 +49 -0
- package/dist/index.d.ts +173 -0
- package/dist/punkt-react.es.js +8216 -7134
- package/dist/punkt-react.umd.js +435 -435
- package/package.json +4 -4
- package/src/components/fileupload/DropZone.tsx +236 -0
- package/src/components/fileupload/FileUpload.test.tsx +584 -0
- package/src/components/fileupload/FileUpload.tsx +425 -0
- package/src/components/fileupload/QueueDisplay.test.tsx +222 -0
- package/src/components/fileupload/QueueDisplay.tsx +155 -0
- package/src/components/fileupload/QueueItemContent.tsx +200 -0
- package/src/components/fileupload/Subcomponents.tsx +363 -0
- package/src/components/fileupload/Truncate.test.tsx +256 -0
- package/src/components/fileupload/Truncate.tsx +137 -0
- package/src/components/fileupload/extensions/Comments.tsx +188 -0
- package/src/components/fileupload/extensions/Remove.tsx +10 -0
- package/src/components/fileupload/extensions/Rename.tsx +79 -0
- package/src/components/fileupload/hooks/index.ts +3 -0
- package/src/components/fileupload/hooks/useFileAttributes.ts +46 -0
- package/src/components/fileupload/hooks/useImagePreview.ts +42 -0
- package/src/components/fileupload/hooks/useOperationState.ts +30 -0
- package/src/components/fileupload/hooks.ts +4 -0
- package/src/components/fileupload/texts.ts +20 -0
- package/src/components/fileupload/types.ts +94 -0
- package/src/components/fileupload/utils.ts +56 -0
- package/src/components/index.ts +1 -0
- package/src/components/interfaces.ts +1 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import { FC, useMemo } from 'react'
|
|
3
|
+
|
|
4
|
+
import { useImagePreview, useOperationState } from './hooks'
|
|
5
|
+
import { getProgressState, QueueItemContent } from './QueueItemContent'
|
|
6
|
+
import { FilenameRenderer, ImagePreviewModal, ThumbnailRenderer } from './Subcomponents'
|
|
7
|
+
import { TruncateContext } from './Truncate'
|
|
8
|
+
import { TFileAndTransfer, FileItem, TFileItemList, TFileTransfer, TItemRenderer, TQueueItemOperation } from './types'
|
|
9
|
+
|
|
10
|
+
// ============================================
|
|
11
|
+
// Helper: Transform files with transfer status
|
|
12
|
+
// ============================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Combine `files` with their transfer state (`transfers`) and sort by priority.
|
|
16
|
+
*
|
|
17
|
+
* Sort order:
|
|
18
|
+
* - in-progress first
|
|
19
|
+
* - then errors
|
|
20
|
+
* - then everything else
|
|
21
|
+
*/
|
|
22
|
+
const useFilesAndTransfers = (files: TFileItemList, transfers: TFileTransfer[]): TFileAndTransfer[] => {
|
|
23
|
+
return useMemo(() => {
|
|
24
|
+
const mapped = files.map((fileItem: FileItem) => {
|
|
25
|
+
const transfer = transfers.find((t) => t.fileId === fileItem.fileId)
|
|
26
|
+
return {
|
|
27
|
+
...fileItem,
|
|
28
|
+
progress: transfer?.progress ?? 'queued',
|
|
29
|
+
errorMessage: transfer?.errorMessage,
|
|
30
|
+
showProgress: transfer?.showProgress,
|
|
31
|
+
lastProgress: transfer?.lastProgress,
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Sort order: in-progress first, then errors, then the rest
|
|
36
|
+
const priority = { 'in-progress': 0, error: 1, idle: 2 } as const
|
|
37
|
+
return mapped.sort((a, b) => priority[getProgressState(a.progress)] - priority[getProgressState(b.progress)])
|
|
38
|
+
}, [files, transfers])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================
|
|
42
|
+
// Helper: Filter previewable images
|
|
43
|
+
// ============================================
|
|
44
|
+
|
|
45
|
+
/** Returns only uploaded images (used for the preview modal). */
|
|
46
|
+
const usePreviewableImages = (filesAndTransfers: TFileAndTransfer[], enabled: boolean): TFileAndTransfer[] => {
|
|
47
|
+
return useMemo(() => {
|
|
48
|
+
if (!enabled) return []
|
|
49
|
+
return filesAndTransfers.filter((item) => item.progress === 'done' && item.file.type.startsWith('image/'))
|
|
50
|
+
}, [filesAndTransfers, enabled])
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============================================
|
|
54
|
+
// QueueDisplay Props
|
|
55
|
+
// ============================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Queue list renderer for `PktFileUpload`.
|
|
59
|
+
*
|
|
60
|
+
* This component is UI-only; state changes are handled via callbacks / operations passed in.
|
|
61
|
+
*/
|
|
62
|
+
interface IQueueDisplay {
|
|
63
|
+
/** Called when the user cancels/removes a file (also used to cancel transfers). */
|
|
64
|
+
cancelTransfer: (fileItemId: string) => void
|
|
65
|
+
/** Transfer states used for `uploadStrategy="custom"`. */
|
|
66
|
+
transfers?: TFileTransfer[]
|
|
67
|
+
/** Current file items. */
|
|
68
|
+
files: TFileItemList
|
|
69
|
+
/** Operations (rename/comment/remove/etc) rendered per queue item. */
|
|
70
|
+
queueItemOperations?: TQueueItemOperation[]
|
|
71
|
+
/** Custom renderer for each queue item (defaults to filename renderer). */
|
|
72
|
+
ItemRenderer?: TItemRenderer
|
|
73
|
+
/** Number of trailing characters to keep when truncating long filenames. */
|
|
74
|
+
truncateTail?: number
|
|
75
|
+
/** Enable image preview modal (only affects thumbnail view). */
|
|
76
|
+
enableImagePreview?: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ============================================
|
|
80
|
+
// QueueDisplay Component
|
|
81
|
+
// ============================================
|
|
82
|
+
|
|
83
|
+
export const QueueDisplay: FC<IQueueDisplay> = ({
|
|
84
|
+
files,
|
|
85
|
+
cancelTransfer,
|
|
86
|
+
transfers = [],
|
|
87
|
+
queueItemOperations = [],
|
|
88
|
+
ItemRenderer = FilenameRenderer,
|
|
89
|
+
truncateTail,
|
|
90
|
+
enableImagePreview = false,
|
|
91
|
+
}) => {
|
|
92
|
+
// Transform data
|
|
93
|
+
const filesAndTransfers = useFilesAndTransfers(files, transfers)
|
|
94
|
+
const previewableImages = usePreviewableImages(filesAndTransfers, enableImagePreview)
|
|
95
|
+
|
|
96
|
+
// State management
|
|
97
|
+
const operationState = useOperationState(queueItemOperations)
|
|
98
|
+
const preview = useImagePreview(previewableImages)
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<>
|
|
102
|
+
<ul className="pkt-fileupload__queue-display">
|
|
103
|
+
<TruncateContext.Provider value={{ tail: truncateTail }}>
|
|
104
|
+
{filesAndTransfers.map((transferItem) => (
|
|
105
|
+
<li
|
|
106
|
+
key={transferItem.fileId}
|
|
107
|
+
className={classNames('pkt-fileupload__queue-display__item', {
|
|
108
|
+
'pkt-fileupload__queue-display__item--in-progress': typeof transferItem.progress === 'number',
|
|
109
|
+
[`pkt-fileupload__queue-display__item--${transferItem.progress}`]:
|
|
110
|
+
typeof transferItem.progress === 'string',
|
|
111
|
+
})}
|
|
112
|
+
>
|
|
113
|
+
{/* Hidden inputs for form submission */}
|
|
114
|
+
{queueItemOperations.filter((op) => op.renderHidden).map((op) => op.renderHidden!(transferItem))}
|
|
115
|
+
|
|
116
|
+
{/* Main content based on state */}
|
|
117
|
+
<QueueItemContent
|
|
118
|
+
transferItem={transferItem}
|
|
119
|
+
activatedOperation={operationState.getActivated(transferItem.fileId)}
|
|
120
|
+
operations={queueItemOperations}
|
|
121
|
+
ItemRenderer={ItemRenderer}
|
|
122
|
+
enableImagePreview={enableImagePreview}
|
|
123
|
+
onActivate={operationState.activate}
|
|
124
|
+
onClose={operationState.close}
|
|
125
|
+
onCancelTransfer={cancelTransfer}
|
|
126
|
+
onOpenPreview={preview.open}
|
|
127
|
+
/>
|
|
128
|
+
</li>
|
|
129
|
+
))}
|
|
130
|
+
</TruncateContext.Provider>
|
|
131
|
+
</ul>
|
|
132
|
+
|
|
133
|
+
{/* Image preview modal */}
|
|
134
|
+
{enableImagePreview && previewableImages.length > 0 && (
|
|
135
|
+
<ImagePreviewModal
|
|
136
|
+
ref={preview.modalRef}
|
|
137
|
+
isOpen={preview.isOpen}
|
|
138
|
+
images={previewableImages}
|
|
139
|
+
currentIndex={preview.currentIndex}
|
|
140
|
+
onClose={preview.close}
|
|
141
|
+
onNavigate={preview.navigate}
|
|
142
|
+
/>
|
|
143
|
+
)}
|
|
144
|
+
</>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================
|
|
149
|
+
// ItemRenderers Export
|
|
150
|
+
// ============================================
|
|
151
|
+
|
|
152
|
+
export const ItemRenderers: Record<string, TItemRenderer> = {
|
|
153
|
+
filename: FilenameRenderer,
|
|
154
|
+
thumbnail: ThumbnailRenderer,
|
|
155
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { FC, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
import { PktIcon } from '..'
|
|
4
|
+
import { OperationButton, TransferError, TransferInProgress } from './Subcomponents'
|
|
5
|
+
import { TFileAndTransfer, TFileId, TItemRenderer, TQueueItemOperation, TTransferItemInProgress } from './types'
|
|
6
|
+
|
|
7
|
+
type TProgressState = 'in-progress' | 'error' | 'idle'
|
|
8
|
+
|
|
9
|
+
export const getProgressState = (progress: TFileAndTransfer['progress']): TProgressState => {
|
|
10
|
+
if (typeof progress === 'number') return 'in-progress'
|
|
11
|
+
if (progress === 'error') return 'error'
|
|
12
|
+
return 'idle'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ============================================
|
|
16
|
+
// OperationActions - Renders action buttons for a file
|
|
17
|
+
// ============================================
|
|
18
|
+
|
|
19
|
+
interface IOperationActions {
|
|
20
|
+
operations: TQueueItemOperation[]
|
|
21
|
+
activatedOperation?: TQueueItemOperation
|
|
22
|
+
onActivate: (fileId: TFileId, operation: TQueueItemOperation) => void
|
|
23
|
+
transferItem: TFileAndTransfer
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const OperationActions: FC<IOperationActions> = ({
|
|
27
|
+
operations,
|
|
28
|
+
activatedOperation,
|
|
29
|
+
onActivate,
|
|
30
|
+
transferItem,
|
|
31
|
+
}) => (
|
|
32
|
+
<div className="pkt-fileupload__queue-display__item__actions">
|
|
33
|
+
{operations
|
|
34
|
+
.filter((op) => !activatedOperation || op.symbol !== activatedOperation.symbol)
|
|
35
|
+
.map((operation) => (
|
|
36
|
+
<OperationButton
|
|
37
|
+
key={operation.symbol.toString()}
|
|
38
|
+
operation={operation}
|
|
39
|
+
onActivate={onActivate}
|
|
40
|
+
transferItem={transferItem}
|
|
41
|
+
/>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
// ============================================
|
|
47
|
+
// OperationContents - Renders operation content areas
|
|
48
|
+
// ============================================
|
|
49
|
+
|
|
50
|
+
interface IOperationContents {
|
|
51
|
+
operations: TQueueItemOperation[]
|
|
52
|
+
activatedOperation?: TQueueItemOperation
|
|
53
|
+
onActivate: (fileId: TFileId, operation: TQueueItemOperation) => void
|
|
54
|
+
transferItem: TFileAndTransfer
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const OperationContents: FC<IOperationContents> = ({
|
|
58
|
+
operations,
|
|
59
|
+
activatedOperation,
|
|
60
|
+
onActivate,
|
|
61
|
+
transferItem,
|
|
62
|
+
}) => (
|
|
63
|
+
<>
|
|
64
|
+
{operations
|
|
65
|
+
.filter((op) => op.renderContent)
|
|
66
|
+
.map((operation) => (
|
|
67
|
+
<div className="pkt-fileupload__queue-display__item__operation-content" key={operation.symbol.toString()}>
|
|
68
|
+
{operation.renderContent!(
|
|
69
|
+
transferItem,
|
|
70
|
+
() => onActivate(transferItem.fileId, operation),
|
|
71
|
+
activatedOperation?.symbol === operation.symbol,
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
))}
|
|
75
|
+
</>
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// ============================================
|
|
79
|
+
// IdleStateContent - Renders content for idle/done state
|
|
80
|
+
// ============================================
|
|
81
|
+
|
|
82
|
+
interface IIdleStateContent {
|
|
83
|
+
transferItem: TFileAndTransfer
|
|
84
|
+
activatedOperation?: TQueueItemOperation
|
|
85
|
+
operations: TQueueItemOperation[]
|
|
86
|
+
ItemRenderer: TItemRenderer
|
|
87
|
+
onActivate: (fileId: TFileId, operation: TQueueItemOperation) => void
|
|
88
|
+
onClose: (fileId: TFileId) => void
|
|
89
|
+
onPreviewClick?: () => void
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const IdleStateContent: FC<IIdleStateContent> = ({
|
|
93
|
+
transferItem,
|
|
94
|
+
activatedOperation,
|
|
95
|
+
operations,
|
|
96
|
+
ItemRenderer,
|
|
97
|
+
onActivate,
|
|
98
|
+
onClose,
|
|
99
|
+
onPreviewClick,
|
|
100
|
+
}) => {
|
|
101
|
+
const closeUI = () => onClose(transferItem.fileId)
|
|
102
|
+
|
|
103
|
+
// When any operation UI is active (rename inline or comments extended), hide all action buttons.
|
|
104
|
+
const hasOperationUIActive = activatedOperation?.renderInlineUI || activatedOperation?.renderExtendedUI
|
|
105
|
+
const visibleOperations = hasOperationUIActive ? [] : operations
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<>
|
|
109
|
+
{activatedOperation?.renderInlineUI ? (
|
|
110
|
+
<>
|
|
111
|
+
<PktIcon name="document-text" className="pkt-fileupload__queue-display__item__icon" />
|
|
112
|
+
<div className="pkt-fileupload__queue-display__item__inline-ui">
|
|
113
|
+
{activatedOperation.renderInlineUI(transferItem, closeUI)}
|
|
114
|
+
</div>
|
|
115
|
+
</>
|
|
116
|
+
) : (
|
|
117
|
+
<ItemRenderer transferItem={transferItem} queueItemOperations={operations} onPreviewClick={onPreviewClick} />
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{visibleOperations.length > 0 && (
|
|
121
|
+
<OperationActions
|
|
122
|
+
operations={visibleOperations}
|
|
123
|
+
activatedOperation={activatedOperation}
|
|
124
|
+
onActivate={onActivate}
|
|
125
|
+
transferItem={transferItem}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
|
|
129
|
+
<OperationContents
|
|
130
|
+
operations={operations}
|
|
131
|
+
activatedOperation={activatedOperation}
|
|
132
|
+
onActivate={onActivate}
|
|
133
|
+
transferItem={transferItem}
|
|
134
|
+
/>
|
|
135
|
+
|
|
136
|
+
{activatedOperation?.renderExtendedUI && (
|
|
137
|
+
<div className="pkt-fileupload__queue-display__item__expanded-operation-ui">
|
|
138
|
+
{activatedOperation.renderExtendedUI(transferItem, closeUI)}
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
</>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================
|
|
146
|
+
// QueueItemContent - Main content renderer based on state
|
|
147
|
+
// ============================================
|
|
148
|
+
|
|
149
|
+
interface IQueueItemContent {
|
|
150
|
+
transferItem: TFileAndTransfer
|
|
151
|
+
activatedOperation?: TQueueItemOperation
|
|
152
|
+
operations: TQueueItemOperation[]
|
|
153
|
+
ItemRenderer: TItemRenderer
|
|
154
|
+
enableImagePreview: boolean
|
|
155
|
+
onActivate: (fileId: TFileId, operation: TQueueItemOperation) => void
|
|
156
|
+
onClose: (fileId: TFileId) => void
|
|
157
|
+
onCancelTransfer: (fileId: string) => void
|
|
158
|
+
onOpenPreview: (fileId: TFileId) => void
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const QueueItemContent: FC<IQueueItemContent> = ({
|
|
162
|
+
transferItem,
|
|
163
|
+
activatedOperation,
|
|
164
|
+
operations,
|
|
165
|
+
ItemRenderer,
|
|
166
|
+
enableImagePreview,
|
|
167
|
+
onActivate,
|
|
168
|
+
onClose,
|
|
169
|
+
onCancelTransfer,
|
|
170
|
+
onOpenPreview,
|
|
171
|
+
}) => {
|
|
172
|
+
const state = getProgressState(transferItem.progress)
|
|
173
|
+
const isPreviewable =
|
|
174
|
+
enableImagePreview && transferItem.progress === 'done' && transferItem.file.type.startsWith('image/')
|
|
175
|
+
|
|
176
|
+
switch (state) {
|
|
177
|
+
case 'in-progress':
|
|
178
|
+
return (
|
|
179
|
+
<TransferInProgress
|
|
180
|
+
aria-live="off"
|
|
181
|
+
transferItem={transferItem as TTransferItemInProgress}
|
|
182
|
+
cancelTransfer={() => onCancelTransfer(transferItem.fileId)}
|
|
183
|
+
/>
|
|
184
|
+
)
|
|
185
|
+
case 'error':
|
|
186
|
+
return <TransferError transferItem={transferItem} onRemove={() => onCancelTransfer(transferItem.fileId)} />
|
|
187
|
+
case 'idle':
|
|
188
|
+
return (
|
|
189
|
+
<IdleStateContent
|
|
190
|
+
transferItem={transferItem}
|
|
191
|
+
activatedOperation={activatedOperation}
|
|
192
|
+
operations={operations}
|
|
193
|
+
ItemRenderer={ItemRenderer}
|
|
194
|
+
onActivate={onActivate}
|
|
195
|
+
onClose={onClose}
|
|
196
|
+
onPreviewClick={isPreviewable ? () => onOpenPreview(transferItem.fileId) : undefined}
|
|
197
|
+
/>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
}
|