diff --git a/src/language/texts/en.ts b/src/language/texts/en.ts index 92663fc4ed..3f04ff920f 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': 'Thumbnail', '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 18f85f6752..9222a6dcd7 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': 'Thumbnail', '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 5e6c9adcc8..a40a07da82 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': 'Thumbnail', '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..6322d4d66c --- /dev/null +++ b/src/layout/FileUpload/FileUploadTable/AttachmentThumbnail.module.css @@ -0,0 +1,20 @@ +.thumbnailContainer { + margin-top: 0.5rem; + margin-bottom: 0.5rem; + display: flex; + align-items: center; +} + +.thumbnail { + max-width: 100px; + max-height: 70px; + object-fit: contain; + border-radius: 2px; +} + +.thumbnailMobile { + max-width: 80px; + max-height: 60px; + object-fit: contain; + border-radius: 2px; +} \ No newline at end of file diff --git a/src/layout/FileUpload/FileUploadTable/AttachmentThumbnail.tsx b/src/layout/FileUpload/FileUploadTable/AttachmentThumbnail.tsx new file mode 100644 index 0000000000..7406f67884 --- /dev/null +++ b/src/layout/FileUpload/FileUploadTable/AttachmentThumbnail.tsx @@ -0,0 +1,65 @@ +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, UploadedAttachment } from 'src/features/attachments'; +interface IAttachmentThumbnailProps { + attachment: IAttachment; + mobileView: boolean; +} + +export const AttachmentThumbnail = ({ attachment, mobileView }: IAttachmentThumbnailProps) => { + // Get all data elements from the instance + const dataElements = useInstanceDataElements(undefined); + const instanceId = useLaxInstanceId(); + const language = useCurrentLanguage(); + + // Only uploaded attachments can have thumbnails + if (!isAttachmentUploaded(attachment)) { + return null; + } + + //Check for thumbnail metadata in the attachment + const thumbnailLink = + (attachment as UploadedAttachment)?.data?.metadata?.find( + (meta: { key: string; value: string }) => meta.key === 'thumbnailLink', + )?.value ?? null; + + if (!thumbnailLink) { + return null; + } + + // Find the thumbnail data element + const thumbnailDataElement = dataElements.find( + (el) => + el.dataType === 'thumbnail' && + el.metadata?.some((meta) => meta.key === 'attachmentLink' && meta.value === thumbnailLink), + ); + + if (!thumbnailDataElement?.id || !instanceId) { + return null; + } + + const thumbnailUrl = makeUrlRelativeIfSameDomain(getDataElementUrl(instanceId, thumbnailDataElement.id, language)); + + if (!thumbnailUrl) { + return null; + } + + return ( +
+ {`Thumbnail +
+ ); +}; diff --git a/src/layout/FileUpload/FileUploadTable/FileTable.tsx b/src/layout/FileUpload/FileUploadTable/FileTable.tsx index 7b606261f7..3851211da1 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; + }; + + //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'); + }); + + 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 && ( + + )} + + + )} + + {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 5abf1109d8..4ed3190e44 100644 --- a/src/layout/FileUpload/FileUploadTable/FileTableRow.tsx +++ b/src/layout/FileUpload/FileUploadTable/FileTableRow.tsx @@ -1,272 +1,282 @@ -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 { 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 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 && ( + + )} + {hasImages && ( + + + + )} + {!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 ( + + + + ); +}; diff --git a/src/types/shared.ts b/src/types/shared.ts index 3853c245a2..b9b98362b6 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -1,311 +1,317 @@ -import type { FileScanResult } from 'src/features/attachments/types'; -import type { LooseAutocomplete } from 'src/types'; - -export interface IAltinnOrg { - name: ITitle; - logo: string; - orgnr: string; - homepage: string; - environments: string[]; -} - -export interface IAltinnOrgs { - [org: string]: IAltinnOrg; -} - -export interface IApplicationLogic { - allowAnonymousOnStateless?: boolean | null; - autoCreate?: boolean | null; - classRef?: string | null; - schemaRef?: string | null; - disallowUserCreate?: boolean | null; -} - -export interface IDisplayAttachment { - name?: string; - iconClass: string; - grouping: string | undefined; - description: Partial, string>> | undefined; - url?: string; - dataType: string; - tags?: string[]; -} - -export interface IData { - id: string; - instanceGuid: string; - dataType: string; - filename?: string; - contentType: string; - blobStoragePath: string; - selfLinks?: ISelfLinks; - size: number; - locked: boolean; - refs: string[]; - isRead?: boolean; - tags?: string[]; - created: string; - createdBy: string; - lastChanged: string; - lastChangedBy: string; - contentHash?: unknown; - fileScanResult?: FileScanResult; - fileScanDetails?: string; -} - -export interface IDataType { - id: string; - description?: Partial, string>> | null; - allowedContentTypes: string[] | null; - allowedContributers?: string[] | null; - allowedContributors?: string[] | null; - appLogic?: IApplicationLogic | null; - taskId?: string | null; - maxSize?: number | null; - maxCount: number; - minCount: number; - grouping?: string | null; -} - -export interface IInstance { - appId: string; - created?: string; - createdBy?: string; - data: IData[]; - dueBefore?: string; - id: string; - instanceOwner: IInstanceOwner; - instanceState?: IInstanceState; - lastChanged?: string; - lastChangedBy?: string; - org: string; - selfLinks?: ISelfLinks | null; - status?: IInstanceStatus | null; - title?: ITitle | null; - visibleAfter?: string; - completeConfirmations?: unknown; - presentationTexts?: unknown; - dataValues?: unknown; -} - -export interface IInstanceStatus { - substatus: ISubstatus; -} - -export interface ISubstatus { - label: string; - description: string; -} - -export interface IInstanceOwner { - partyId: string; - personNumber?: string; - organisationNumber?: string | null; - username?: string; - party?: IParty | null; -} - -export interface IInstanceState { - isDeleted: boolean; - isMarkedForHardDelete: boolean; - isArchived: boolean; -} - -// Language for the rendered altinn app -export interface IAppLanguage { - language: string; // Language code -} - -/** - * @see https://github.com/Altinn/altinn-platform/blob/main/Altinn.Platform.Models/src/Register/Models/Organization.cs - */ -export interface IOrganisation { - orgNumber: string; - name: string; - unitType: string; - telephoneNumber: string; - mobileNumber: string; - faxNumber: string; - emailAddress: string; - internetAddress: string; - mailingAddress: string; - mailingPostalCode: string; - mailingPostalCity: string; - businessPostalCode: string; - businessPostalCity: string; - // unitStatus: string; // This exists in the model but is not clearly defined, and not used in the frontend -} - -/** - * @see https://github.com/Altinn/altinn-platform/blob/main/Altinn.Platform.Models/src/Register/Models/Party.cs - */ -export interface IParty { - partyId: number; - partyUuid?: string | null; - partyTypeName: PartyType; - orgNumber?: string | null; - ssn: string | null; - unitType?: string | null; - name: string; - isDeleted: boolean; - onlyHierarchyElementWithNoAccess: boolean; - person?: IPerson | null; - organization?: IOrganisation | null; - childParties?: IParty[] | null; -} - -/** - * @see https://github.com/Altinn/altinn-register/blob/main/src/Altinn.Platform.Models/src/Altinn.Platform.Models/Register/PartyType.cs - */ -export enum PartyType { - Person = 1, - Organisation = 2, - - /** - * Commenting these out so nobody uses them by accident. The enum linked above has - * these values, but their existence seem to be a lie: - * @see https://digdir.slack.com/archives/C079ZFUSFMW/p1738771291616989?thread_ts=1738750152.285599&cid=C079ZFUSFMW - */ - // SubUnit = 4, - SelfIdentified = 3, - // BankruptcyEstate = 5, -} - -export interface IPerson { - ssn: string; - name: string; - firstName: string; - middleName: string | null; - lastName: string; - telephoneNumber: string; - mobileNumber: string; - mailingAddress: string; - mailingPostalCode: number; - mailingPostalCity: string; - addressMunicipalNumber: number; - addressMunicipalName: string; - addressStreetName: string; - addressHouseNumber: number; - addressHouseLetter: string | null; - addressPostalCode: number; - addressCity: string; -} - -export interface IProcess { - started: string; - startEvent?: string | null; - currentTask?: ITask; - ended?: string | null; - endEvent?: string | null; - processTasks?: Pick[]; -} - -export interface IProfile { - userId: number; - userName: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - phoneNumber?: any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - email?: any; - partyId: number; - party?: IParty; - userType: number; - profileSettingPreference: IProfileSettingPreference; -} - -export interface IProfileSettingPreference { - language: string | null; - preSelectedPartyId: number; - doNotPromptForParty: boolean; -} - -export interface ISelfLinks { - apps: string; - platform: string; -} - -export interface IUserAction { - id: IActionType | string; - authorized: boolean; - type: 'ProcessAction' | 'ServerAction'; -} - -export const ELEMENT_TYPE = { - SERVICE_TASK: 'ServiceTask', - TASK: 'Task', -} as const; - -type ElementType = (typeof ELEMENT_TYPE)[keyof typeof ELEMENT_TYPE]; - -export type ITask = { - flow: number; - started: string; - elementId: string; - name: string; - altinnTaskType: string; - elementType?: ElementType; - ended?: string | null; - validated?: IValidated | null; - - read?: boolean | null; - write?: boolean | null; - actions?: IProcessActions | null; - userActions?: IUserAction[]; -}; - -export type IProcessActions = { - [k in IActionType]?: boolean; -}; - -export interface ITitle { - [key: string]: string; -} - -export interface IValidated { - timestamp: string; - canCompleteTask: boolean; -} - -export interface ITextResource { - value: string; - variables?: IVariable[]; -} - -export interface IVariable { - key: string; - dataSource: - | 'instanceContext' - | 'applicationSettings' - | 'dataModel.default' - | `dataModel.${string}` - | 'customTextParameters'; - defaultValue?: string; -} - -export interface IApplicationSettings { - [source: string]: string | undefined; -} - -export type InstanceOwnerPartyType = 'unknown' | 'org' | 'person' | 'selfIdentified'; - -/** Describes an object with key values from current instance to be used in texts. */ -export interface IInstanceDataSources { - instanceId: string; - appId: string; - instanceOwnerPartyId: string; - instanceOwnerPartyType: InstanceOwnerPartyType; - instanceOwnerName?: string; -} - -export type IActionType = 'instantiate' | 'confirm' | 'sign' | 'reject' | 'read' | 'write' | 'complete'; - -export type IAuthContext = { - read: boolean; - write: boolean; -} & { [action in IActionType]: boolean }; - -export type ProblemDetails = { - title: string; - detail: string; - status: number; -}; +import type { FileScanResult } from 'src/features/attachments/types'; +import type { LooseAutocomplete } from 'src/types'; + +export interface IAltinnOrg { + name: ITitle; + logo: string; + orgnr: string; + homepage: string; + environments: string[]; +} + +export interface IAltinnOrgs { + [org: string]: IAltinnOrg; +} + +export interface IApplicationLogic { + allowAnonymousOnStateless?: boolean | null; + autoCreate?: boolean | null; + classRef?: string | null; + schemaRef?: string | null; + disallowUserCreate?: boolean | null; +} + +export interface IMetadata { + key?: string; + value?: string; +} + +export interface IDisplayAttachment { + name?: string; + iconClass: string; + grouping: string | undefined; + description: Partial, string>> | undefined; + url?: string; + dataType: string; + tags?: string[]; +} + +export interface IData { + id: string; + instanceGuid: string; + dataType: string; + filename?: string; + contentType: string; + blobStoragePath: string; + selfLinks?: ISelfLinks; + size: number; + locked: boolean; + refs: string[]; + isRead?: boolean; + tags?: string[]; + created: string; + createdBy: string; + lastChanged: string; + lastChangedBy: string; + contentHash?: unknown; + fileScanResult?: FileScanResult; + fileScanDetails?: string; + metadata?: IMetadata[]; // Added metadata field to support thumbnails +} + +export interface IDataType { + id: string; + description?: Partial, string>> | null; + allowedContentTypes: string[] | null; + allowedContributers?: string[] | null; + allowedContributors?: string[] | null; + appLogic?: IApplicationLogic | null; + taskId?: string | null; + maxSize?: number | null; + maxCount: number; + minCount: number; + grouping?: string | null; +} + +export interface IInstance { + appId: string; + created?: string; + createdBy?: string; + data: IData[]; + dueBefore?: string; + id: string; + instanceOwner: IInstanceOwner; + instanceState?: IInstanceState; + lastChanged?: string; + lastChangedBy?: string; + org: string; + selfLinks?: ISelfLinks | null; + status?: IInstanceStatus | null; + title?: ITitle | null; + visibleAfter?: string; + completeConfirmations?: unknown; + presentationTexts?: unknown; + dataValues?: unknown; +} + +export interface IInstanceStatus { + substatus: ISubstatus; +} + +export interface ISubstatus { + label: string; + description: string; +} + +export interface IInstanceOwner { + partyId: string; + personNumber?: string; + organisationNumber?: string | null; + username?: string; + party?: IParty | null; +} + +export interface IInstanceState { + isDeleted: boolean; + isMarkedForHardDelete: boolean; + isArchived: boolean; +} + +// Language for the rendered altinn app +export interface IAppLanguage { + language: string; // Language code +} + +/** + * @see https://github.com/Altinn/altinn-platform/blob/main/Altinn.Platform.Models/src/Register/Models/Organization.cs + */ +export interface IOrganisation { + orgNumber: string; + name: string; + unitType: string; + telephoneNumber: string; + mobileNumber: string; + faxNumber: string; + emailAddress: string; + internetAddress: string; + mailingAddress: string; + mailingPostalCode: string; + mailingPostalCity: string; + businessPostalCode: string; + businessPostalCity: string; + // unitStatus: string; // This exists in the model but is not clearly defined, and not used in the frontend +} + +/** + * @see https://github.com/Altinn/altinn-platform/blob/main/Altinn.Platform.Models/src/Register/Models/Party.cs + */ +export interface IParty { + partyId: number; + partyUuid?: string | null; + partyTypeName: PartyType; + orgNumber?: string | null; + ssn: string | null; + unitType?: string | null; + name: string; + isDeleted: boolean; + onlyHierarchyElementWithNoAccess: boolean; + person?: IPerson | null; + organization?: IOrganisation | null; + childParties?: IParty[] | null; +} + +/** + * @see https://github.com/Altinn/altinn-register/blob/main/src/Altinn.Platform.Models/src/Altinn.Platform.Models/Register/PartyType.cs + */ +export enum PartyType { + Person = 1, + Organisation = 2, + + /** + * Commenting these out so nobody uses them by accident. The enum linked above has + * these values, but their existence seem to be a lie: + * @see https://digdir.slack.com/archives/C079ZFUSFMW/p1738771291616989?thread_ts=1738750152.285599&cid=C079ZFUSFMW + */ + // SubUnit = 4, + SelfIdentified = 3, + // BankruptcyEstate = 5, +} + +export interface IPerson { + ssn: string; + name: string; + firstName: string; + middleName: string | null; + lastName: string; + telephoneNumber: string; + mobileNumber: string; + mailingAddress: string; + mailingPostalCode: number; + mailingPostalCity: string; + addressMunicipalNumber: number; + addressMunicipalName: string; + addressStreetName: string; + addressHouseNumber: number; + addressHouseLetter: string | null; + addressPostalCode: number; + addressCity: string; +} + +export interface IProcess { + started: string; + startEvent?: string | null; + currentTask?: ITask; + ended?: string | null; + endEvent?: string | null; + processTasks?: Pick[]; +} + +export interface IProfile { + userId: number; + userName: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + phoneNumber?: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + email?: any; + partyId: number; + party?: IParty; + userType: number; + profileSettingPreference: IProfileSettingPreference; +} + +export interface IProfileSettingPreference { + language: string | null; + preSelectedPartyId: number; + doNotPromptForParty: boolean; +} + +export interface ISelfLinks { + apps: string; + platform: string; +} + +export interface IUserAction { + id: IActionType | string; + authorized: boolean; + type: 'ProcessAction' | 'ServerAction'; +} + +export const ELEMENT_TYPE = { + SERVICE_TASK: 'ServiceTask', + TASK: 'Task', +} as const; + +type ElementType = (typeof ELEMENT_TYPE)[keyof typeof ELEMENT_TYPE]; + +export type ITask = { + flow: number; + started: string; + elementId: string; + name: string; + altinnTaskType: string; + elementType?: ElementType; + ended?: string | null; + validated?: IValidated | null; + + read?: boolean | null; + write?: boolean | null; + actions?: IProcessActions | null; + userActions?: IUserAction[]; +}; + +export type IProcessActions = { + [k in IActionType]?: boolean; +}; + +export interface ITitle { + [key: string]: string; +} + +export interface IValidated { + timestamp: string; + canCompleteTask: boolean; +} + +export interface ITextResource { + value: string; + variables?: IVariable[]; +} + +export interface IVariable { + key: string; + dataSource: + | 'instanceContext' + | 'applicationSettings' + | 'dataModel.default' + | `dataModel.${string}` + | 'customTextParameters'; + defaultValue?: string; +} + +export interface IApplicationSettings { + [source: string]: string | undefined; +} + +export type InstanceOwnerPartyType = 'unknown' | 'org' | 'person' | 'selfIdentified'; + +/** Describes an object with key values from current instance to be used in texts. */ +export interface IInstanceDataSources { + instanceId: string; + appId: string; + instanceOwnerPartyId: string; + instanceOwnerPartyType: InstanceOwnerPartyType; + instanceOwnerName?: string; +} + +export type IActionType = 'instantiate' | 'confirm' | 'sign' | 'reject' | 'read' | 'write' | 'complete'; + +export type IAuthContext = { + read: boolean; + write: boolean; +} & { [action in IActionType]: boolean }; + +export type ProblemDetails = { + title: string; + detail: string; + status: number; +};