import { html } from 'lit' import { classMap } from 'lit/directives/class-map.js' import { ifDefined } from 'lit/directives/if-defined.js' import { getDisplayFilename, splitFilenameForTruncation } from 'shared-utils/fileupload' import type { FileItem, TQueueItemOperation, TQueueOperationContext, TTransferProgress, } from './fileupload-types' type TRenderQueueItemProps = { file: FileItem inputName: string disabled: boolean fileSize: string thumbnailUrl?: string previewEnabled?: boolean canOpenPreview?: boolean transferProgress: TTransferProgress transferErrorMessage?: string transferShowProgress?: boolean transferLastProgress?: number loadingText?: string operations: TQueueItemOperation[] activeOperationId?: string truncateTail?: number onActivateOperation: (fileId: string, operationId: string) => void onCloseOperation: (fileId: string) => void getFileAttribute: (fileId: string, name: string) => T | undefined setFileAttribute: (fileId: string, name: string, value: unknown) => void onCancel: (fileId: string) => void onOpenPreview?: (fileId: string) => void onThumbnailImageError?: (fileId: string) => void } /** * Render a filename with optional middle-truncation. The split-or-not decision * lives in `splitFilenameForTruncation` (shared with React) so the threshold * matches across both runtimes. */ const renderTruncatedFilename = (filename: string, truncateTail?: number) => { const split = splitFilenameForTruncation(filename, truncateTail) if (!split) return html`${filename}` return html`${split.head}${split.tail}` } const renderTitleWithSize = (filename: string, fileSize: string, truncateTail?: number) => html`

${renderTruncatedFilename(filename, truncateTail)}${fileSize ? html` (${fileSize})` : null}

` const getProgressState = (progress: TTransferProgress): 'in-progress' | 'error' | 'idle' => { if (typeof progress === 'number') return 'in-progress' if (progress === 'error') return 'error' return 'idle' } const renderLoadingText = (loadingText?: string) => html`${loadingText ?? 'Laster opp...'}` const createOperationContext = ({ operationId, file, inputName, disabled, isActive, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }: { operationId: string file: FileItem inputName: string disabled: boolean isActive: boolean onActivateOperation: (fileId: string, operationId: string) => void onCloseOperation: (fileId: string) => void getFileAttribute: (fileId: string, name: string) => T | undefined setFileAttribute: (fileId: string, name: string, value: unknown) => void }): TQueueOperationContext => ({ file, inputName, disabled, isActive, activate: () => onActivateOperation(file.fileId, operationId), close: () => onCloseOperation(file.fileId), getAttribute: (name: string) => getFileAttribute(file.fileId, name), setAttribute: (name: string, value: unknown) => setFileAttribute(file.fileId, name, value), }) const renderTransferInProgress = ({ file, fileSize, disabled, transferProgress, transferShowProgress, loadingText, truncateTail, onCancel, iconName = 'document-text', }: { file: FileItem fileSize: string disabled: boolean transferProgress: number transferShowProgress?: boolean loadingText?: string truncateTail?: number onCancel: (fileId: string) => void iconName?: string }) => { const filename = getDisplayFilename(file) const showProgressBar = transferShowProgress ?? transferProgress !== 0 return html` ${renderTitleWithSize(filename, fileSize, truncateTail)} ${showProgressBar ? html` ` : renderLoadingText(loadingText)} ` } const renderTransferError = ({ file, fileSize, disabled, transferShowProgress, transferLastProgress, transferErrorMessage, truncateTail, onCancel, }: { file: FileItem fileSize: string disabled: boolean transferShowProgress?: boolean transferLastProgress?: number transferErrorMessage?: string truncateTail?: number onCancel: (fileId: string) => void }) => { const filename = getDisplayFilename(file) const showProgressError = !!transferShowProgress const progressValue = Math.round((transferLastProgress ?? 1) * 100) return html` ${renderTitleWithSize(filename, fileSize, truncateTail)} ${showProgressError ? html` ` : null} ${transferErrorMessage ? html`${transferErrorMessage}` : null} ` } const renderOperationButtons = ({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }: { file: FileItem disabled: boolean inputName: string operations: TQueueItemOperation[] activeOperationId?: string onActivateOperation: (fileId: string, operationId: string) => void onCloseOperation: (fileId: string) => void getFileAttribute: (fileId: string, name: string) => T | undefined setFileAttribute: (fileId: string, name: string, value: unknown) => void }) => { const activeOperation = operations.find((operation) => operation.id === activeOperationId) const hasOperationUIActive = !!( activeOperation?.renderInlineUI || activeOperation?.renderExtendedUI ) if (hasOperationUIActive) return null const operationButtons = operations .map((operation) => { const context = createOperationContext({ operationId: operation.id, file, inputName, disabled, isActive: operation.id === activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) const title = typeof operation.title === 'function' ? operation.title(file) : operation.title if (!title) return null const ariaLabel = typeof operation.ariaLabel === 'function' ? operation.ariaLabel(file) : operation.ariaLabel return html`` }) .filter(Boolean) if (operationButtons.length === 0) return null return html`
${operationButtons}
` } const renderOperationContent = ({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }: { file: FileItem disabled: boolean inputName: string operations: TQueueItemOperation[] activeOperationId?: string onActivateOperation: (fileId: string, operationId: string) => void onCloseOperation: (fileId: string) => void getFileAttribute: (fileId: string, name: string) => T | undefined setFileAttribute: (fileId: string, name: string, value: unknown) => void }) => operations.map((operation) => { if (!operation.renderContent) return null const context = createOperationContext({ operationId: operation.id, file, inputName, disabled, isActive: operation.id === activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) const content = operation.renderContent(context) if (content === null || content === undefined) { return null } return html`
${content}
` }) const renderExpandedOperationContent = ({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }: { file: FileItem disabled: boolean inputName: string operations: TQueueItemOperation[] activeOperationId?: string onActivateOperation: (fileId: string, operationId: string) => void onCloseOperation: (fileId: string) => void getFileAttribute: (fileId: string, name: string) => T | undefined setFileAttribute: (fileId: string, name: string, value: unknown) => void }) => { if (!activeOperationId) return null const activeOperation = operations.find((operation) => operation.id === activeOperationId) if (!activeOperation?.renderExtendedUI) return null const context = createOperationContext({ operationId: activeOperation.id, file, inputName, disabled, isActive: true, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) return html`
${activeOperation.renderExtendedUI(context)}
` } const renderHiddenOperationInputs = ({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }: { file: FileItem disabled: boolean inputName: string operations: TQueueItemOperation[] activeOperationId?: string onActivateOperation: (fileId: string, operationId: string) => void onCloseOperation: (fileId: string) => void getFileAttribute: (fileId: string, name: string) => T | undefined setFileAttribute: (fileId: string, name: string, value: unknown) => void }) => operations.map((operation) => { if (!operation.renderHidden) return null const context = createOperationContext({ operationId: operation.id, file, inputName, disabled, isActive: operation.id === activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) return operation.renderHidden(context) }) const renderFilenameIdleMain = (file: FileItem, fileSize: string, truncateTail?: number) => { const filename = getDisplayFilename(file) return html` ${renderTitleWithSize(filename, fileSize, truncateTail)} ` } const renderThumbnailIdleMain = ({ file, thumbnailUrl, previewEnabled, canOpenPreview, truncateTail, onOpenPreview, onThumbnailImageError, }: { file: FileItem thumbnailUrl?: string previewEnabled?: boolean canOpenPreview?: boolean truncateTail?: number onOpenPreview?: (fileId: string) => void onThumbnailImageError?: (fileId: string) => void }) => { const filename = getDisplayFilename(file) const hasThumbnail = !!thumbnailUrl const showExpandButton = !!previewEnabled && !!canOpenPreview const isInteractive = showExpandButton && !!onOpenPreview return html`
${renderTruncatedFilename(filename, truncateTail)}
` } export const renderFilenameQueueItem = ({ file, inputName, disabled, fileSize, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, transferProgress, transferErrorMessage, transferShowProgress, transferLastProgress, loadingText, truncateTail, onCancel, }: TRenderQueueItemProps) => { const state = getProgressState(transferProgress) const activeOperation = operations.find((operation) => operation.id === activeOperationId) const inlineOperationTemplate = activeOperation?.renderInlineUI ? activeOperation.renderInlineUI( createOperationContext({ operationId: activeOperation.id, file, inputName, disabled, isActive: true, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }), ) : null return html`
  • ${renderHiddenOperationInputs({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, })} ${state === 'in-progress' ? renderTransferInProgress({ file, fileSize, disabled, transferProgress: transferProgress as number, transferShowProgress, loadingText, truncateTail, onCancel, }) : state === 'error' ? renderTransferError({ file, fileSize, disabled, transferShowProgress, transferLastProgress, transferErrorMessage, truncateTail, onCancel, }) : inlineOperationTemplate ? html`
    ${inlineOperationTemplate}
    ` : renderFilenameIdleMain(file, fileSize, truncateTail)} ${state === 'idle' ? renderOperationButtons({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) : null} ${state === 'idle' ? renderOperationContent({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) : null} ${state === 'idle' ? renderExpandedOperationContent({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) : null}
  • ` } export const renderThumbnailQueueItem = ({ file, inputName, disabled, fileSize, thumbnailUrl, previewEnabled, canOpenPreview, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, transferProgress, transferErrorMessage, transferShowProgress, transferLastProgress, loadingText, truncateTail, onCancel, onOpenPreview, onThumbnailImageError, }: TRenderQueueItemProps) => { const state = getProgressState(transferProgress) const activeOperation = operations.find((operation) => operation.id === activeOperationId) const inlineOperationTemplate = activeOperation?.renderInlineUI ? activeOperation.renderInlineUI( createOperationContext({ operationId: activeOperation.id, file, inputName, disabled, isActive: true, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }), ) : null return html`
  • ${renderHiddenOperationInputs({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, })} ${state === 'in-progress' ? renderTransferInProgress({ file, fileSize, disabled, transferProgress: transferProgress as number, transferShowProgress, loadingText, truncateTail, onCancel, iconName: 'picture', }) : state === 'error' ? renderTransferError({ file, fileSize, disabled, transferShowProgress, transferLastProgress, transferErrorMessage, truncateTail, onCancel, }) : inlineOperationTemplate ? html`
    ${inlineOperationTemplate}
    ` : renderThumbnailIdleMain({ file, thumbnailUrl, previewEnabled, canOpenPreview, truncateTail, onOpenPreview, onThumbnailImageError, })} ${state === 'idle' ? renderOperationButtons({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) : null} ${state === 'idle' ? renderOperationContent({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) : null} ${state === 'idle' ? renderExpandedOperationContent({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) : null}
  • ` }