@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.
@@ -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
+ }