diff --git a/src/language/texts/en.ts b/src/language/texts/en.ts index db7c020b72..a67aafa984 100644 --- a/src/language/texts/en.ts +++ b/src/language/texts/en.ts @@ -74,6 +74,7 @@ export function en() { 'form_filler.file_uploader_delete_warning': 'Are you sure you want to delete this attachment?', 'form_filler.file_uploader_delete_button_confirm': 'Yes, delete attachment', 'form_filler.file_uploader_list_header_file_size': 'File size', + 'form_filler.file_uploader_list_header_thumbnail': 'Preview', 'form_filler.file_uploader_list_header_name': 'Name', 'form_filler.file_uploader_list_header_status': 'Status', 'form_filler.file_uploader_list_header_delete_sr': 'Delete', diff --git a/src/language/texts/nb.ts b/src/language/texts/nb.ts index 94fd17c1c9..1bd1b2d55b 100644 --- a/src/language/texts/nb.ts +++ b/src/language/texts/nb.ts @@ -75,6 +75,7 @@ export function nb() { 'form_filler.file_uploader_delete_warning': 'Er du sikker på at du vil slette dette vedlegget?', 'form_filler.file_uploader_delete_button_confirm': 'Ja, slett vedlegg', 'form_filler.file_uploader_list_header_file_size': 'Filstørrelse', + 'form_filler.file_uploader_list_header_thumbnail': 'Forhåndsvisning', 'form_filler.file_uploader_list_header_name': 'Navn', 'form_filler.file_uploader_list_header_status': 'Status', 'form_filler.file_uploader_list_status_done': 'Ferdig lastet', diff --git a/src/language/texts/nn.ts b/src/language/texts/nn.ts index a1e3e6f73a..094e804b06 100644 --- a/src/language/texts/nn.ts +++ b/src/language/texts/nn.ts @@ -75,6 +75,7 @@ export function nn() { 'form_filler.file_uploader_delete_warning': 'Er du sikker på at du vil sletta dette vedlegget?', 'form_filler.file_uploader_delete_button_confirm': 'Ja, slett vedlegg', 'form_filler.file_uploader_list_header_file_size': 'Filstorleik', + 'form_filler.file_uploader_list_header_thumbnail': 'Førehandsvisning', 'form_filler.file_uploader_list_header_name': 'Namn', 'form_filler.file_uploader_list_header_status': 'Status', 'form_filler.file_uploader_list_status_done': 'Ferdig lasta', diff --git a/src/layout/FileUpload/FileUploadTable/AttachmentThumbnail.module.css b/src/layout/FileUpload/FileUploadTable/AttachmentThumbnail.module.css new file mode 100644 index 0000000000..112b383e4e --- /dev/null +++ b/src/layout/FileUpload/FileUploadTable/AttachmentThumbnail.module.css @@ -0,0 +1,132 @@ +.thumbnailContainer { + margin-top: 0.5rem; + margin-bottom: 0.5rem; + display: flex; + align-items: center; +} + +.thumbnail { + max-width: 80px; + max-height: 56px; + object-fit: contain; +} + +.thumbnailMobile { + max-width: 64px; + max-height: 48px; + object-fit: contain; +} + +.previewBackdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn 0.3s ease-in-out; +} + +.previewModal { + position: relative; + background-color: #fff; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + max-width: 90vw; + max-height: 90vh; + overflow: auto; + padding: 0; + display: flex; + flex-direction: column; +} + +.previewHeader { + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + background-color: #fff; + border-radius: 8px 8px 0 0; + gap: 16px; +} + +.previewHeaderMobile { + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 20px; + background-color: #fff; + border-radius: 8px 8px 0 0; + gap: 8px; +} + +.fileName { + font-weight: 500; + color: #333; + flex: 1; + word-break: break-word; + font-size: 0.95rem; +} + +.previewImage { + max-width: 100%; + max-height: calc(90vh - 100px); + object-fit: contain; + display: block; + padding: 0 20px 20px 20px; + background-color: #fff; +} + +.previewImageMobile { + max-width: 100%; + max-height: calc(90vh - 100px); + object-fit: contain; + display: block; + padding: 0 10px 10px 10px; + background-color: #fff; +} + +.closeButton { + position: relative; + background-color: transparent; + border: none; + font-size: 32px; + cursor: pointer; + color: var(--ds-color-text-subtle); + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: background-color 0.2s ease; + flex-shrink: 0; + padding: 0; +} + +.closeButton:hover { + color: var(--ds-color-text-default); + background-color: var(--ds-color-surface-tinted); +} + +.imageLoading { + display: flex; + justify-content: center; + align-items: center; + min-height: 200px; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/src/layout/FileUpload/FileUploadTable/AttachmentThumbnail.tsx b/src/layout/FileUpload/FileUploadTable/AttachmentThumbnail.tsx new file mode 100644 index 0000000000..72ddf37954 --- /dev/null +++ b/src/layout/FileUpload/FileUploadTable/AttachmentThumbnail.tsx @@ -0,0 +1,59 @@ +import React from 'react'; + +import { isAttachmentUploaded } from 'src/features/attachments'; +import { useInstanceDataElements, useLaxInstanceId } from 'src/features/instance/InstanceContext'; +import { useCurrentLanguage } from 'src/features/language/LanguageProvider'; +import classes from 'src/layout/FileUpload/FileUploadTable/AttachmentThumbnail.module.css'; +import { getDataElementUrl } from 'src/utils/urls/appUrlHelper'; +import { makeUrlRelativeIfSameDomain } from 'src/utils/urls/urlHelper'; +import type { IAttachment } from 'src/features/attachments'; + +interface IAttachmentThumbnailProps { + attachment: IAttachment; + mobileView: boolean; + onThumbnailClick?: () => void; +} + +export const AttachmentThumbnail = ({ attachment, mobileView, onThumbnailClick }: IAttachmentThumbnailProps) => { + const dataElements = useInstanceDataElements(undefined); + const instanceId = useLaxInstanceId(); + const language = useCurrentLanguage(); + + // Only uploaded attachments can have thumbnails + if (!instanceId || !isAttachmentUploaded(attachment)) { + return ''; + } + + const thumbnailLink = attachment.data.metadata?.find((meta) => meta.key === 'thumbnailLink')?.value; + if (!thumbnailLink) { + return ''; + } + + const thumbnailDataElement = dataElements.find( + (el) => + el.dataType === 'thumbnail' && + el.metadata?.some((meta) => meta.key === 'attachmentLink' && meta.value === thumbnailLink), + ); + + const url = thumbnailDataElement + ? makeUrlRelativeIfSameDomain(getDataElementUrl(instanceId, thumbnailDataElement.id, language)) + : undefined; + + return ( +
e.key === 'Enter' && onThumbnailClick() : undefined} + > + {`Thumbnail +
+ ); +}; diff --git a/src/layout/FileUpload/FileUploadTable/FileTable.tsx b/src/layout/FileUpload/FileUploadTable/FileTable.tsx index be497d2b5e..1691d916cd 100644 --- a/src/layout/FileUpload/FileUploadTable/FileTable.tsx +++ b/src/layout/FileUpload/FileUploadTable/FileTable.tsx @@ -1,146 +1,166 @@ -import React from 'react'; - -import { isAttachmentUploaded } from 'src/features/attachments'; -import { Lang } from 'src/features/language/Lang'; -import { usePdfModeActive } from 'src/features/pdf/PdfWrapper'; -import classes from 'src/layout/FileUpload/FileUploadTable/FileTableComponent.module.css'; -import { FileTableRow } from 'src/layout/FileUpload/FileUploadTable/FileTableRow'; -import { FileTableRowProvider } from 'src/layout/FileUpload/FileUploadTable/FileTableRowContext'; -import { EditWindowComponent } from 'src/layout/FileUploadWithTag/EditWindowComponent'; -import { atLeastOneTagExists } from 'src/utils/formComponentUtils'; -import { useItemWhenType } from 'src/utils/layout/useNodeItem'; -import type { IAttachment } from 'src/features/attachments'; -import type { IOptionInternal } from 'src/features/options/castOptionsToStrings'; -import type { FileTableRowContext } from 'src/layout/FileUpload/FileUploadTable/FileTableRowContext'; - -export interface FileTableProps { - baseComponentId: string; - attachments: IAttachment[]; - mobileView: boolean; - options?: IOptionInternal[]; - isFetching: boolean; - isSummary?: boolean; -} - -export function FileTable({ - attachments, - mobileView, - baseComponentId, - options, - isSummary, - isFetching, -}: FileTableProps): React.JSX.Element | null { - const { textResourceBindings, type, readOnly } = useItemWhenType<'FileUpload' | 'FileUploadWithTag'>( - baseComponentId, - (t) => t === 'FileUpload' || t === 'FileUploadWithTag', - ); - const hasTag = type === 'FileUploadWithTag'; - const pdfModeActive = usePdfModeActive(); - const [editIndex, setEditIndex] = React.useState(-1); - if (!attachments || attachments.length === 0) { - return null; - } - const tagTitle = - (textResourceBindings && 'tagTitle' in textResourceBindings && textResourceBindings?.tagTitle) || undefined; - const label = (attachment: IAttachment) => { - if (!isAttachmentUploaded(attachment)) { - return undefined; - } - - const firstTag = attachment.data.tags && attachment.data.tags[0]; - return options?.find((option) => option.value === firstTag)?.label; - }; - - return ( - - {(atLeastOneTagExists(attachments) || !hasTag) && ( - - - - {!mobileView && ( - - )} - {hasTag && !mobileView && ( - - )} - {!(hasTag && mobileView) && !pdfModeActive && !mobileView && ( - - )} - - {!pdfModeActive && ( - - )} - - - )} - - {attachments.map((attachment, index: number) => { - const isMissingTag = hasTag && isAttachmentUploaded(attachment) && !attachment.data.tags?.length; - const showSimpleRow = isAttachmentUploaded(attachment) - ? !hasTag || readOnly || (hasTag && !isMissingTag && editIndex !== index) - : true; - - const ctx: FileTableRowContext = { - setEditIndex, - editIndex, - index, - }; - - return showSimpleRow ? ( - - - - ) : ( - - - - - - ); - })} - -
- - - - - - - - -

- -

-
- -
- ); -} +import React from 'react'; + +import { isAttachmentUploaded } from 'src/features/attachments'; +import { Lang } from 'src/features/language/Lang'; +import { usePdfModeActive } from 'src/features/pdf/PdfWrapper'; +import classes from 'src/layout/FileUpload/FileUploadTable/FileTableComponent.module.css'; +import { FileTableRow } from 'src/layout/FileUpload/FileUploadTable/FileTableRow'; +import { FileTableRowProvider } from 'src/layout/FileUpload/FileUploadTable/FileTableRowContext'; +import { EditWindowComponent } from 'src/layout/FileUploadWithTag/EditWindowComponent'; +import { atLeastOneTagExists } from 'src/utils/formComponentUtils'; +import { useItemWhenType } from 'src/utils/layout/useNodeItem'; +import type { IAttachment } from 'src/features/attachments'; +import type { IOptionInternal } from 'src/features/options/castOptionsToStrings'; +import type { FileTableRowContext } from 'src/layout/FileUpload/FileUploadTable/FileTableRowContext'; + +export interface FileTableProps { + baseComponentId: string; + attachments: IAttachment[]; + mobileView: boolean; + options?: IOptionInternal[]; + isFetching: boolean; + isSummary?: boolean; +} + +export function FileTable({ + attachments, + mobileView, + baseComponentId, + options, + isSummary, + isFetching, +}: FileTableProps): React.JSX.Element | null { + const { textResourceBindings, type, readOnly } = useItemWhenType<'FileUpload' | 'FileUploadWithTag'>( + baseComponentId, + (t) => t === 'FileUpload' || t === 'FileUploadWithTag', + ); + const hasTag = type === 'FileUploadWithTag'; + const pdfModeActive = usePdfModeActive(); + const [editIndex, setEditIndex] = React.useState(-1); + if (!attachments || attachments.length === 0) { + return null; + } + const tagTitle = + (textResourceBindings && 'tagTitle' in textResourceBindings && textResourceBindings?.tagTitle) || undefined; + const label = (attachment: IAttachment) => { + if (!isAttachmentUploaded(attachment)) { + return undefined; + } + const firstTag = attachment.data.tags && attachment.data.tags[0]; + return options?.find((option) => option.value === firstTag)?.label; + }; + const thumbnailTitle = !mobileView ? 'form_filler.file_uploader_list_header_thumbnail' : undefined; + + //Check if any uploaded attachment has thumbnails + const hasImages = attachments.some((attachment) => { + if (!isAttachmentUploaded(attachment)) { + return false; + } + return attachment.data.metadata?.some((meta) => meta.key === 'thumbnailLink' && !!meta.value?.trim()); + }); + + const calculateColSpan = () => { + if (mobileView) { + return hasImages ? 4 : 3; + } + return hasImages ? 7 : 6; + }; + + return ( + + {(atLeastOneTagExists(attachments) || !hasTag) && ( + + + + {!mobileView && ( + + )} + {hasTag && !mobileView && ( + + )} + {!(hasTag && mobileView) && !pdfModeActive && !mobileView && ( + + )} + {hasImages && !pdfModeActive && ( + + )} + {!pdfModeActive && ( + + )} + + + )} + + {attachments.map((attachment, index: number) => { + const isMissingTag = hasTag && isAttachmentUploaded(attachment) && !attachment.data.tags?.length; + const showSimpleRow = isAttachmentUploaded(attachment) + ? !hasTag || readOnly || (hasTag && !isMissingTag && editIndex !== index) + : true; + + const ctx: FileTableRowContext = { + setEditIndex, + editIndex, + index, + }; + + return showSimpleRow ? ( + + + + ) : ( + + + + + + ); + })} + +
+ + + + + + + + + + +

+ +

+
+ +
+ ); +} diff --git a/src/layout/FileUpload/FileUploadTable/FileTableRow.tsx b/src/layout/FileUpload/FileUploadTable/FileTableRow.tsx index 5bfd05e916..a1afc684ca 100644 --- a/src/layout/FileUpload/FileUploadTable/FileTableRow.tsx +++ b/src/layout/FileUpload/FileUploadTable/FileTableRow.tsx @@ -1,272 +1,301 @@ -import React from 'react'; - -import classNames from 'classnames'; - -import { AltinnLoader } from 'src/components/AltinnLoader'; -import { useTaskOverrides } from 'src/core/contexts/TaskOverrides'; -import { isAttachmentUploaded } from 'src/features/attachments'; -import { FileScanResults } from 'src/features/attachments/types'; -import { Lang } from 'src/features/language/Lang'; -import { useLanguage } from 'src/features/language/useLanguage'; -import { usePdfModeActive } from 'src/features/pdf/PdfWrapper'; -import { AttachmentFileName } from 'src/layout/FileUpload/FileUploadTable/AttachmentFileName'; -import { FileTableButtons } from 'src/layout/FileUpload/FileUploadTable/FileTableButtons'; -import classes from 'src/layout/FileUpload/FileUploadTable/FileTableRow.module.css'; -import { useFileTableRow } from 'src/layout/FileUpload/FileUploadTable/FileTableRowContext'; -import { EditButton } from 'src/layout/Summary2/CommonSummaryComponents/EditButton'; -import { AltinnPalette } from 'src/theme/altinnAppTheme'; -import { getSizeWithUnit } from 'src/utils/attachmentsUtils'; -import { useExternalItem } from 'src/utils/layout/hooks'; -import type { IAttachment } from 'src/features/attachments'; - -interface IFileUploadTableRowProps { - attachment: IAttachment; - mobileView: boolean; - baseComponentId: string; - tagLabel: string | undefined; - isSummary?: boolean; -} - -export function FileTableRow({ - baseComponentId, - attachment, - mobileView, - tagLabel, - isSummary, -}: IFileUploadTableRowProps) { - const { langAsString } = useLanguage(); - const component = useExternalItem(baseComponentId); - const hasTag = component?.type === 'FileUploadWithTag'; - const pdfModeActive = usePdfModeActive(); - const readableSize = getSizeWithUnit(attachment.data.size, 2); - - const hasOverriddenTaskId = Boolean(useTaskOverrides()?.taskId); - - const uniqueId = isAttachmentUploaded(attachment) ? attachment.data.id : attachment.data.temporaryId; - - const getStatusFromScanResult = () => { - if (!attachment.uploaded) { - return langAsString('general.loading'); - } - - const scanResult = attachment.data.fileScanResult; - - switch (scanResult) { - case FileScanResults.Pending: - return langAsString('form_filler.file_uploader_status_scanning'); - case FileScanResults.Infected: - return langAsString('form_filler.file_uploader_status_infected'); - case FileScanResults.Clean: - case FileScanResults.NotApplicable: - default: - return langAsString('form_filler.file_uploader_list_status_done'); - } - }; - - const status = getStatusFromScanResult(); - - const rowStyle = - isSummary || pdfModeActive - ? classNames(classes.noRowSpacing, classes.grayUnderlineDotted) - : classes.blueUnderlineDotted; - - return ( - - - {hasTag && !mobileView && } - {!(hasTag && mobileView) && !pdfModeActive && !mobileView && ( - - )} - - {!isSummary && ( - - )} - {isSummary && !pdfModeActive && ( - - - - )} - - ); -} - -const NameCell = ({ - mobileView, - attachment, - readableSize, - hasTag, - uploadStatus, - tagLabel, -}: { - mobileView: boolean; - attachment: IAttachment; - readableSize: string; - hasTag: boolean; - uploadStatus: string; - tagLabel?: string; -}) => { - const { langAsString } = useLanguage(); - const uniqueId = isAttachmentUploaded(attachment) ? attachment.data.id : attachment.data.temporaryId; - return ( - <> - -
- - {mobileView && ( -
- {attachment.uploaded ? ( -
- {tagLabel && mobileView && ( -
- -
- )} - {`${readableSize} ${mobileView ? uploadStatus : ''}`} - {hasTag && !mobileView && ( -
- -
- )} -
- ) : ( - - )} -
- )} -
- - {!mobileView ? {readableSize} : null} - - ); -}; - -const FileTypeCell = ({ tagLabel }: { tagLabel: string | undefined }) => { - const { langAsString } = useLanguage(); - const { index } = useFileTableRow(); - return {tagLabel && langAsString(tagLabel)}; -}; - -const StatusCellContent = ({ - uploaded, - status, - scanResult, -}: { - uploaded: boolean; - status: string; - scanResult?: string; -}) => { - const getStatusElement = () => { - if (!uploaded) { - return ( - - ); - } - - const getTestId = () => { - switch (scanResult) { - case FileScanResults.Infected: - return 'status-infected'; - case FileScanResults.Pending: - return 'status-scanning'; - default: - return 'status-success'; - } - }; - - const getClassName = () => { - switch (scanResult) { - case FileScanResults.Infected: - return classes.statusInfected; - case FileScanResults.Pending: - return classes.statusScanning; - default: - return ''; - } - }; - - return ( -
- {status} -
- ); - }; - - return {getStatusElement()}; -}; - -interface IButtonCellContentProps { - deleting: boolean; - baseComponentId: string; - mobileView: boolean; - attachment: IAttachment; -} - -const ButtonCellContent = ({ deleting, baseComponentId, mobileView, attachment }: IButtonCellContentProps) => { - const { langAsString } = useLanguage(); - - if (deleting) { - return ( - - - - ); - } - - return ( - - - - ); -}; +import React from 'react'; + +import classNames from 'classnames'; + +import { AltinnLoader } from 'src/components/AltinnLoader'; +import { useTaskOverrides } from 'src/core/contexts/TaskOverrides'; +import { isAttachmentUploaded } from 'src/features/attachments'; +import { FileScanResults } from 'src/features/attachments/types'; +import { Lang } from 'src/features/language/Lang'; +import { useLanguage } from 'src/features/language/useLanguage'; +import { usePdfModeActive } from 'src/features/pdf/PdfWrapper'; +import { AttachmentFileName } from 'src/layout/FileUpload/FileUploadTable/AttachmentFileName'; +import { AttachmentThumbnail } from 'src/layout/FileUpload/FileUploadTable/AttachmentThumbnail'; +import { FileTableButtons } from 'src/layout/FileUpload/FileUploadTable/FileTableButtons'; +import classes from 'src/layout/FileUpload/FileUploadTable/FileTableRow.module.css'; +import { useFileTableRow } from 'src/layout/FileUpload/FileUploadTable/FileTableRowContext'; +import { ThumbnailPreviewModal } from 'src/layout/FileUpload/FileUploadTable/ThumbnailPreviewModal'; +import { EditButton } from 'src/layout/Summary2/CommonSummaryComponents/EditButton'; +import { AltinnPalette } from 'src/theme/altinnAppTheme'; +import { getSizeWithUnit } from 'src/utils/attachmentsUtils'; +import { useExternalItem } from 'src/utils/layout/hooks'; +import type { IAttachment } from 'src/features/attachments'; + +interface IFileUploadTableRowProps { + attachment: IAttachment; + mobileView: boolean; + baseComponentId: string; + tagLabel: string | undefined; + isSummary?: boolean; + hasImages?: boolean; +} + +export function FileTableRow({ + baseComponentId, + attachment, + mobileView, + tagLabel, + isSummary, + hasImages, +}: IFileUploadTableRowProps) { + const { langAsString } = useLanguage(); + const component = useExternalItem(baseComponentId); + const hasTag = component?.type === 'FileUploadWithTag'; + const pdfModeActive = usePdfModeActive(); + const readableSize = getSizeWithUnit(attachment.data.size, 2); + const [isPreviewOpen, setIsPreviewOpen] = React.useState(false); + + const hasOverriddenTaskId = Boolean(useTaskOverrides()?.taskId); + + const uniqueId = isAttachmentUploaded(attachment) ? attachment.data.id : attachment.data.temporaryId; + + const getStatusFromScanResult = () => { + if (!attachment.uploaded) { + return langAsString('general.loading'); + } + + const scanResult = attachment.data.fileScanResult; + + switch (scanResult) { + case FileScanResults.Pending: + return langAsString('form_filler.file_uploader_status_scanning'); + case FileScanResults.Infected: + return langAsString('form_filler.file_uploader_status_infected'); + case FileScanResults.Clean: + case FileScanResults.NotApplicable: + default: + return langAsString('form_filler.file_uploader_list_status_done'); + } + }; + + const handleThumbnailClick = () => { + if (isAttachmentUploaded(attachment)) { + const link = attachment.data.metadata?.find((meta) => meta.key === 'thumbnailLink')?.value; + if (link) { + setIsPreviewOpen(true); + } + } + }; + + const status = getStatusFromScanResult(); + + const rowStyle = + isSummary || pdfModeActive + ? classNames(classes.noRowSpacing, classes.grayUnderlineDotted) + : classes.blueUnderlineDotted; + + return ( + <> + + + {!mobileView && {readableSize}} + {hasTag && !mobileView && } + {!(hasTag && mobileView) && !pdfModeActive && !mobileView && ( + + )} + {hasImages && !pdfModeActive && ( + + + + )} + {!isSummary && ( + + )} + {isSummary && !pdfModeActive && ( + + + + )} + + setIsPreviewOpen(false)} + attachment={attachment} + fileName={isAttachmentUploaded(attachment) ? (attachment.data.filename ?? '') : ''} + mobileView={mobileView} + /> + + ); +} + +const NameCell = ({ + mobileView, + attachment, + readableSize, + hasTag, + uploadStatus, + tagLabel, +}: { + mobileView: boolean; + attachment: IAttachment; + readableSize: string; + hasTag: boolean; + uploadStatus: string; + tagLabel?: string; +}) => { + const { langAsString } = useLanguage(); + const uniqueId = isAttachmentUploaded(attachment) ? attachment.data.id : attachment.data.temporaryId; + return ( + +
+ + {mobileView && ( +
+ {attachment.uploaded ? ( +
+ {tagLabel && mobileView && ( +
+ +
+ )} + {`${readableSize} ${mobileView ? uploadStatus : ''}`} + {hasTag && !mobileView && ( +
+ +
+ )} +
+ ) : ( + + )} +
+ )} +
+ + ); +}; + +const FileTypeCell = ({ tagLabel }: { tagLabel: string | undefined }) => { + const { langAsString } = useLanguage(); + const { index } = useFileTableRow(); + return {tagLabel && langAsString(tagLabel)}; +}; + +const StatusCellContent = ({ + uploaded, + status, + scanResult, +}: { + uploaded: boolean; + status: string; + scanResult?: string; +}) => { + const getStatusElement = () => { + if (!uploaded) { + return ( + + ); + } + + const getTestId = () => { + switch (scanResult) { + case FileScanResults.Infected: + return 'status-infected'; + case FileScanResults.Pending: + return 'status-scanning'; + default: + return 'status-success'; + } + }; + + const getClassName = () => { + switch (scanResult) { + case FileScanResults.Infected: + return classes.statusInfected; + case FileScanResults.Pending: + return classes.statusScanning; + default: + return ''; + } + }; + + return ( +
+ {status} +
+ ); + }; + + return {getStatusElement()}; +}; + +interface IButtonCellContentProps { + deleting: boolean; + baseComponentId: string; + mobileView: boolean; + attachment: IAttachment; +} + +const ButtonCellContent = ({ deleting, baseComponentId, mobileView, attachment }: IButtonCellContentProps) => { + const { langAsString } = useLanguage(); + + if (deleting) { + return ( + + + + ); + } + + return ( + + + + ); +}; diff --git a/src/layout/FileUpload/FileUploadTable/ThumbnailPreviewModal.tsx b/src/layout/FileUpload/FileUploadTable/ThumbnailPreviewModal.tsx new file mode 100644 index 0000000000..8a31ca89d2 --- /dev/null +++ b/src/layout/FileUpload/FileUploadTable/ThumbnailPreviewModal.tsx @@ -0,0 +1,89 @@ +import React from 'react'; + +import { Spinner } from 'src/app-components/loading/Spinner/Spinner'; +import { type IAttachment, isAttachmentUploaded } from 'src/features/attachments'; +import { useLaxInstanceId } from 'src/features/instance/InstanceContext'; +import { useCurrentLanguage } from 'src/features/language/LanguageProvider'; +import classes from 'src/layout/FileUpload/FileUploadTable/AttachmentThumbnail.module.css'; +import { getDataElementUrl } from 'src/utils/urls/appUrlHelper'; +import { makeUrlRelativeIfSameDomain } from 'src/utils/urls/urlHelper'; + +interface ThumbnailPreviewModalProps { + isOpen: boolean; + onClose: () => void; + attachment: IAttachment; + fileName: string; + mobileView?: boolean; +} + +export function ThumbnailPreviewModal({ + isOpen, + onClose, + attachment, + fileName, + mobileView, +}: ThumbnailPreviewModalProps): React.JSX.Element | null { + const language = useCurrentLanguage(); + const instanceId = useLaxInstanceId(); + const [isImageLoading, setIsImageLoading] = React.useState(true); + const url = + isAttachmentUploaded(attachment) && instanceId + ? makeUrlRelativeIfSameDomain(getDataElementUrl(instanceId, attachment.data.id, language)) + : undefined; + + const handleImageLoad = () => { + setIsImageLoading(false); + }; + + if (!isOpen) { + return null; + } + + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + return ( +
{ + if (e.key === 'Escape') { + onClose(); + } + }} + role='button' + tabIndex={0} + > +
+
+ {fileName} + +
+ {isImageLoading && ( +
+ +
+ )} + {fileName} +
+
+ ); +} diff --git a/src/types/shared.ts b/src/types/shared.ts index 6effda0735..81a47fce6f 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -21,6 +21,11 @@ export interface IApplicationLogic { disallowUserCreate?: boolean | null; } +export interface IMetadata { + key?: string; + value?: string; +} + export interface IDisplayAttachment { name?: string; iconClass: string; @@ -51,6 +56,7 @@ export interface IData { contentHash?: unknown; fileScanResult?: FileScanResult; fileScanDetails?: string; + metadata?: IMetadata[]; // Added metadata field to support thumbnails } export interface IDataType {