@dignite/vault-extract 0.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.
Files changed (39) hide show
  1. package/README.md +17 -0
  2. package/fesm2022/dignite-vault-extract-config.mjs +82 -0
  3. package/fesm2022/dignite-vault-extract-config.mjs.map +1 -0
  4. package/fesm2022/dignite-vault-extract-documents-cabinet-list.component-Ch0gpSCc.mjs +184 -0
  5. package/fesm2022/dignite-vault-extract-documents-cabinet-list.component-Ch0gpSCc.mjs.map +1 -0
  6. package/fesm2022/dignite-vault-extract-documents-content-type-DjCs-s4E.mjs +115 -0
  7. package/fesm2022/dignite-vault-extract-documents-content-type-DjCs-s4E.mjs.map +1 -0
  8. package/fesm2022/dignite-vault-extract-documents-document-detail.component-DHs42DWJ.mjs +1146 -0
  9. package/fesm2022/dignite-vault-extract-documents-document-detail.component-DHs42DWJ.mjs.map +1 -0
  10. package/fesm2022/dignite-vault-extract-documents-document-file-preview.component-CStXf8v9.mjs +72 -0
  11. package/fesm2022/dignite-vault-extract-documents-document-file-preview.component-CStXf8v9.mjs.map +1 -0
  12. package/fesm2022/dignite-vault-extract-documents-document-list.component-jThR5cct.mjs +642 -0
  13. package/fesm2022/dignite-vault-extract-documents-document-list.component-jThR5cct.mjs.map +1 -0
  14. package/fesm2022/dignite-vault-extract-documents-document-overview.component-BHUUUIVr.mjs +318 -0
  15. package/fesm2022/dignite-vault-extract-documents-document-overview.component-BHUUUIVr.mjs.map +1 -0
  16. package/fesm2022/dignite-vault-extract-documents-document-recycle-bin.component-dqeBrw22.mjs +178 -0
  17. package/fesm2022/dignite-vault-extract-documents-document-recycle-bin.component-dqeBrw22.mjs.map +1 -0
  18. package/fesm2022/dignite-vault-extract-documents-document-type-list.component-C8kXFJGb.mjs +464 -0
  19. package/fesm2022/dignite-vault-extract-documents-document-type-list.component-C8kXFJGb.mjs.map +1 -0
  20. package/fesm2022/dignite-vault-extract-documents-export-template-list.component-DlmZFFF1.mjs +361 -0
  21. package/fesm2022/dignite-vault-extract-documents-export-template-list.component-DlmZFFF1.mjs.map +1 -0
  22. package/fesm2022/dignite-vault-extract-documents-extensible-table-DkLXuoWo.mjs +53 -0
  23. package/fesm2022/dignite-vault-extract-documents-extensible-table-DkLXuoWo.mjs.map +1 -0
  24. package/fesm2022/dignite-vault-extract-documents-field-definition-list.component-ClmWkRun.mjs +530 -0
  25. package/fesm2022/dignite-vault-extract-documents-field-definition-list.component-ClmWkRun.mjs.map +1 -0
  26. package/fesm2022/dignite-vault-extract-documents-field-reextraction-modal.component-D7OOycv9.mjs +163 -0
  27. package/fesm2022/dignite-vault-extract-documents-field-reextraction-modal.component-D7OOycv9.mjs.map +1 -0
  28. package/fesm2022/dignite-vault-extract-documents-format-bytes-Cd3QwfQZ.mjs +19 -0
  29. package/fesm2022/dignite-vault-extract-documents-format-bytes-Cd3QwfQZ.mjs.map +1 -0
  30. package/fesm2022/dignite-vault-extract-documents-format-field-value-Xjb8lwzA.mjs +22 -0
  31. package/fesm2022/dignite-vault-extract-documents-format-field-value-Xjb8lwzA.mjs.map +1 -0
  32. package/fesm2022/dignite-vault-extract-documents.mjs +71 -0
  33. package/fesm2022/dignite-vault-extract-documents.mjs.map +1 -0
  34. package/fesm2022/dignite-vault-extract.mjs +522 -0
  35. package/fesm2022/dignite-vault-extract.mjs.map +1 -0
  36. package/package.json +38 -0
  37. package/types/dignite-vault-extract-config.d.ts +5 -0
  38. package/types/dignite-vault-extract-documents.d.ts +5 -0
  39. package/types/dignite-vault-extract.d.ts +521 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dignite-vault-extract-documents-document-detail.component-DHs42DWJ.mjs","sources":["../../../packages/vault-extract/documents/src/lib/shared/strip-code-fences.ts","../../../packages/vault-extract/documents/src/lib/documents/document-detail/document-detail.component.ts","../../../packages/vault-extract/documents/src/lib/documents/document-detail/document-detail.component.html"],"sourcesContent":["/**\n * Removes Markdown code-fence delimiter lines from LLM-produced Markdown, keeping the fenced content in place.\n *\n * A vision-LLM OCR transcription (and, defensively, any LLM Markdown rendered in the operator UI) sometimes\n * arrives wrapped — wholly, or only partly — in a ```` ```markdown ```` fence despite the OCR prompt forbidding\n * it (#448). `marked` then renders the fenced block (typically a table) as a literal `<pre><code>` block instead\n * of Markdown, so the table shows as raw pipes. In this channel the payload is digitized DOCUMENT text\n * (headings / tables / lists), never source code, so a triple-backtick / triple-tilde fence is always such an\n * artifact: drop the fence delimiter lines (keeping their content) before parsing — this handles a whole-output\n * fence, a partial fence, and an unmatched (never-closed) fence uniformly.\n *\n * The backend `VisionLlmOutputGuard.StripCodeFences` strips this at the source for newly extracted documents;\n * this frontend twin also rescues documents already stored with the fence baked in (`Document.Markdown` is\n * write-once, so they are never re-extracted).\n *\n * A fence delimiter is a line whose trimmed text is a run of >= 3 back-ticks or >= 3 tildes, optionally\n * followed by an info string that contains no further back-tick — so an inline `` `code` `` / ```` ```code``` ````\n * span sitting on its own line is not mistaken for a fence.\n */\nexport function stripMarkdownCodeFences(markdown: string): string {\n // Fast path: no fence character at all (the overwhelming majority of documents) — return untouched.\n if (!markdown || (markdown.indexOf('`') < 0 && markdown.indexOf('~') < 0)) {\n return markdown;\n }\n return markdown\n .split('\\n')\n .filter(line => !isCodeFenceLine(line))\n .join('\\n');\n}\n\nfunction isCodeFenceLine(line: string): boolean {\n const s = line.trim();\n const marker = s[0];\n if (marker !== '`' && marker !== '~') {\n return false;\n }\n let run = 0;\n while (run < s.length && s[run] === marker) {\n run++;\n }\n // >= 3 markers, and no back-tick after the run (a back-tick would make it an inline span, not a fence).\n return run >= 3 && s.indexOf('`', run) < 0;\n}\n","import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n OnInit,\n effect,\n inject,\n signal,\n computed,\n} from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { forkJoin, of, switchMap, tap, timer, Subscription } from 'rxjs';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { CommonModule, DOCUMENT, Location } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { marked } from 'marked';\nimport { LocalizationPipe, LocalizationService, PermissionService } from '@abp/ng.core';\nimport { DynamicFormComponent, type FormFieldConfig } from '@abp/ng.components/dynamic-form';\nimport { Confirmation, ConfirmationService, ToasterService } from '@abp/ng.theme.shared';\nimport {\n CabinetDto,\n CabinetService,\n DocumentDto,\n DocumentLifecycleStatus,\n DocumentPipelineRunDto,\n DocumentPipelineRunService,\n DocumentReviewDisposition,\n DocumentReviewReasons,\n DocumentService,\n DocumentTypeDto,\n DocumentTypeService,\n FieldDataType,\n FieldDefinitionDto,\n FieldDefinitionService,\n EXTRACT_PERMISSIONS,\n PipelineRunStatus,\n} from '@dignite/vault-extract';\nimport { formatExtractedFieldValue } from '../../shared/format-field-value';\nimport { stripMarkdownCodeFences } from '../../shared/strip-code-fences';\nimport { DocumentFileBlobService } from '../../shared/document-file-blob.service';\nimport { isImageContentType, isPdfContentType } from '../../shared/content-type';\n\ninterface PipelineRow {\n pipelineCode: string;\n labelKey: string;\n isKnown: boolean;\n run: DocumentPipelineRunDto | null;\n // Pre-computed view fields. Without these, the template re-invoked\n // getRunStatusBadgeClass / getElapsedMs / formatElapsed / isRetryable on\n // every change detection cycle for every row. Now they are derived once\n // when the pipelineRows signal recomputes (i.e. when the document is\n // (re)loaded).\n statusBadgeClass: string;\n statusLabel: string;\n inProgress: boolean;\n elapsedDisplay: string | null;\n retryable: boolean;\n}\n\n// Mirrors core/src/Dignite.Vault.Extract.Domain.Shared/Documents/ExtractPipelines.cs.\nconst KNOWN_PIPELINE_CODES = [\n 'text-extraction',\n 'classification',\n 'field-extraction',\n] as const;\n\n// #440 interim live status: while a document is still in-flight, silently re-fetch it + its pipeline runs so\n// the detail page reflects server-side progress without a manual Refresh. Bounded and self-terminating — to\n// be replaced by server push (SSE / SignalR), see issue #440. Start fast, then back the interval off toward\n// the cap so a long-running pipeline does not keep polling at full rate.\nconst POLL_BASE_INTERVAL_MS = 2000;\nconst POLL_MAX_INTERVAL_MS = 10000;\n\n@Component({\n selector: 'lib-document-detail',\n templateUrl: './document-detail.component.html',\n styleUrls: ['./document-detail.component.scss'],\n imports: [CommonModule, FormsModule, DynamicFormComponent, LocalizationPipe],\n providers: [DocumentFileBlobService],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class DocumentDetailComponent implements OnInit {\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n private readonly location = inject(Location);\n private readonly documentService = inject(DocumentService);\n private readonly documentPipelineRunService = inject(DocumentPipelineRunService);\n private readonly documentTypeService = inject(DocumentTypeService);\n private readonly fieldDefinitionService = inject(FieldDefinitionService);\n private readonly cabinetService = inject(CabinetService);\n private readonly toaster = inject(ToasterService);\n private readonly confirmation = inject(ConfirmationService);\n private readonly permissionService = inject(PermissionService);\n private readonly localization = inject(LocalizationService);\n private readonly destroyRef = inject(DestroyRef);\n // #440: DOM document handle for the Page Visibility check that pauses background polling on a hidden tab.\n private readonly domDocument = inject(DOCUMENT);\n // Original-file blob load / sanitize / revoke lifecycle (#277), shared with the file-preview page.\n protected readonly fileBlob = inject(DocumentFileBlobService);\n\n readonly canDelete = this.permissionService.getGrantedPolicy(\n EXTRACT_PERMISSIONS.Documents.Delete,\n );\n readonly canEditFields = this.permissionService.getGrantedPolicy(\n EXTRACT_PERMISSIONS.Documents.ConfirmClassification,\n );\n readonly canViewCabinets = this.permissionService.getGrantedPolicy(\n EXTRACT_PERMISSIONS.Cabinets.Default,\n );\n\n document = signal<DocumentDto | null>(null);\n // #306/#354: when this document is a sub-document (originDocumentId set), the source/container document\n // loaded for the provenance banner so it can show the parent's title instead of a raw id. null when this\n // is a normal document, or when the parent is inaccessible (soft-deleted / cross-layer) — the banner then\n // falls back to the id.\n parentDocument = signal<DocumentDto | null>(null);\n // #306/#354: true when the parent (origin) lookup failed — the source was removed (soft / permanently deleted) or is\n // cross-layer / inaccessible. Drives the banner's \"source unavailable\" label and hides the dead \"view parent\" link,\n // distinguishing a failed lookup from the brief in-flight window (both otherwise leave parentDocument null).\n parentLookupFailed = signal(false);\n // #216: PipelineRun was split into an independent aggregate root and removed from\n // DocumentDto.pipelineRuns. It is now an independent signal loaded separately through\n // DocumentPipelineRunService in loadDocument.\n pipelineRuns = signal<DocumentPipelineRunDto[]>([]);\n isLoading = signal(true);\n // Left column three-tab area (#274): Markdown preview by default; switching to 'file' triggers lazy\n // loading of the original-file blob.\n activeTab = signal<'preview' | 'source' | 'file'>('preview');\n retryingPipeline = signal<string | null>(null);\n isRerecognizing = signal(false);\n isEditingFields = signal(false);\n isSavingFields = signal(false);\n fieldDefinitions = signal<FieldDefinitionDto[]>([]);\n extractedFieldFormFields = signal<FormFieldConfig[]>([]);\n // Candidate cabinets for the document: cabinetId-to-name display mapping plus reassignment dropdown\n // options (#257). Loaded only with Cabinets.Default permission.\n cabinets = signal<CabinetDto[]>([]);\n // Cabinet reassignment (#257) edit state: entering edit shows the dropdown; empty selectedCabinetId\n // means unclassified.\n isEditingCabinet = signal(false);\n isSavingCabinet = signal(false);\n selectedCabinetId = signal<string>('');\n // Document types visible in the current layer, used for typeCode-to-displayName mapping and the\n // confirm-classification picker. Populated together with field definition loading.\n documentTypes = signal<DocumentTypeDto[]>([]);\n\n // #395: manual confirm/assign classification — the authoritative override for UnresolvedClassification,\n // relocated here from the removed review-queue page.\n showClassifyDialog = signal(false);\n selectedTypeId = signal('');\n isConfirmingClassification = signal(false);\n\n // #395: reject (#237/#284 recoverable disposition). The review queue was the only place this action\n // lived; it moves to the remediation hub so the disposition is not lost.\n showRejectDialog = signal(false);\n rejectReason = signal('');\n isRejecting = signal(false);\n // #411: in-flight guard for the \"Allow duplicate\" operator action.\n isAllowingDuplicate = signal(false);\n\n readonly DocumentLifecycleStatus = DocumentLifecycleStatus;\n readonly DocumentReviewDisposition = DocumentReviewDisposition;\n readonly DocumentReviewReasons = DocumentReviewReasons;\n readonly PipelineRunStatus = PipelineRunStatus;\n\n pipelineRows = computed<PipelineRow[]>(() => {\n if (!this.document()) return [];\n\n // #216: run source changed from doc.pipelineRuns to the independent pipelineRuns signal\n // (DocumentPipelineRunService).\n const allRuns = this.pipelineRuns();\n const known: PipelineRow[] = KNOWN_PIPELINE_CODES.map(code => this.toPipelineRow(\n code,\n `::Document:Pipeline:${code}`,\n true,\n this.pickLatestRun(allRuns, code),\n ));\n\n const unknownCodes = Array.from(\n new Set(\n allRuns\n .map(r => r.pipelineCode)\n .filter((code): code is string => !!code && !KNOWN_PIPELINE_CODES.includes(code as typeof KNOWN_PIPELINE_CODES[number]))\n )\n );\n\n const unknown: PipelineRow[] = unknownCodes.map(code => this.toPipelineRow(\n code,\n code,\n false,\n this.pickLatestRun(allRuns, code),\n ));\n\n return [...known, ...unknown];\n });\n\n protected toPipelineRow(\n pipelineCode: string,\n labelKey: string,\n isKnown: boolean,\n run: DocumentPipelineRunDto | null,\n ): PipelineRow {\n return {\n pipelineCode,\n labelKey,\n isKnown,\n run,\n statusBadgeClass: this.getRunStatusBadgeClass(run?.status),\n statusLabel: this.getRunStatusLabel(run?.status),\n inProgress: this.isRunInProgress(run?.status),\n elapsedDisplay: run ? this.formatElapsedOrNull(run) : null,\n retryable: isKnown && this.isRetryable(run),\n };\n }\n\n protected formatElapsedOrNull(run: DocumentPipelineRunDto): string | null {\n return this.getElapsedMs(run) === null ? null : this.formatElapsed(run);\n }\n\n // #284: operator attention required, for any unresolved review reason, equals server-provided\n // requiresReview. The client does not infer it locally.\n needsReview = computed(() => this.document()?.requiresReview ?? false);\n\n // #395: classification still unresolved AND the operator may act — drives the \"Confirm classification\"\n // CTA. Mirrors the list's needsConfirmation; the blocking UnresolvedClassification reason is the only one\n // a manual type assignment resolves.\n needsClassification = computed(() =>\n this.canEditFields &&\n (((this.document()?.reviewReasons ?? DocumentReviewReasons.None) & DocumentReviewReasons.UnresolvedClassification)\n !== DocumentReviewReasons.None),\n );\n\n // #411: a suspected duplicate AND the operator may act — drives the \"Allow\" CTA (release as not a duplicate).\n // The opposite resolution (confirm the duplicate) is the existing Delete action.\n needsDuplicateReview = computed(() =>\n this.canEditFields &&\n (((this.document()?.reviewReasons ?? DocumentReviewReasons.None) & DocumentReviewReasons.DuplicateSuspected)\n !== DocumentReviewReasons.None),\n );\n\n // #412: required fields are missing AND the operator may act — drives the \"Complete fields\" CTA. The reason\n // is non-blocking and clears itself once the values are filled in the fields card below, so the banner\n // offers a jump-and-edit shortcut rather than a confirm/approve button.\n needsFieldCompletion = computed(() =>\n this.canEditFields &&\n (((this.document()?.reviewReasons ?? DocumentReviewReasons.None) & DocumentReviewReasons.MissingRequiredFields)\n !== DocumentReviewReasons.None),\n );\n\n // #284: pure availability axis. After the two axes became orthogonal, the review banner and processing\n // banner are judged independently; the template's @if needsReview takes precedence over isProcessing.\n isProcessing = computed(() => {\n const status = this.document()?.lifecycleStatus;\n return status === DocumentLifecycleStatus.Uploaded ||\n status === DocumentLifecycleStatus.Processing;\n });\n\n isReady = computed(() =>\n this.document()?.lifecycleStatus === DocumentLifecycleStatus.Ready\n );\n\n // #306/#354: this document was derived from a constituent of another (container) document. Drives the\n // provenance banner and its \"view parent / view siblings\" navigation. originDocumentId is a system signal\n // carried on the Document output contract (DocumentDto), null for normally-uploaded documents.\n isSubDocument = computed(() => !!this.document()?.originDocumentId);\n\n // True when any critical pipeline, text extraction or classification, has an in-progress run\n // (Pending/Running).\n pipelineInProgress = computed(() =>\n this.pipelineRows().some(r => r.isKnown && r.inProgress)\n );\n\n // #263 \"rerecognize\" availability: extracted text exists, ConfirmClassification permission is present\n // (same as canEditFields), no critical pipeline is currently running, and the page is not loading. This\n // avoids stacking reclassification onto a document already being processed or reprocessed.\n // Use pipelineInProgress instead of !isProcessing(): the latter is always false when needsReview() is\n // true, which would still expose the button on a pending-review document while reclassification is in\n // progress (review #5). The in-flight POST is covered by button [disabled]=\"isRerecognizing()\".\n canRerecognize = computed(() =>\n this.canEditFields &&\n !!this.document()?.markdown &&\n !this.pipelineInProgress() &&\n !this.isLoading()\n );\n\n isReextracting = signal(false);\n\n // #289 \"field re-extraction only\" availability: same prerequisites as \"rerecognize\", plus already\n // classified with documentTypeCode. Field extraction is attached to a type, so unclassified documents\n // have nothing to extract from; this mirrors the backend NotClassified guard.\n canReextractFields = computed(() =>\n this.canEditFields &&\n !!this.document()?.documentTypeCode &&\n !!this.document()?.markdown &&\n !this.pipelineInProgress() &&\n !this.isLoading()\n );\n\n isImage = computed(() =>\n isImageContentType(this.document()?.fileOrigin?.contentType)\n );\n\n isPdf = computed(() =>\n isPdfContentType(this.document()?.fileOrigin?.contentType)\n );\n\n // Sub-documents (#306/#346) are spawned with no FileOrigin — their Markdown is seeded from the source\n // segment slice, so there is no original file to preview or download. Gate every source-file affordance\n // (Original File tab, footer, Download) on this so the UI never calls GetBlobAsync for a blob-less\n // document, which fails with Extract:DocumentNoSourceBlob (and re-fires on every reload after rerecognize\n // / Refresh while the file tab is active).\n hasSourceFile = computed(() => !!this.document()?.fileOrigin);\n\n // Intermediate computed for Markdown source (#274 review): when document() changes but markdown does\n // not, such as field or cabinet changes, return the same string so downstream renderedMarkdown can\n // short-circuit by value equality and avoid repeated marked.parse calls.\n private markdownSource = computed(() => this.document()?.markdown ?? '');\n\n // Markdown preview (#274): marked renders to an HTML string. When the template binds [innerHTML],\n // Angular's built-in DomSanitizer sanitizes it automatically by stripping <script>, on*, and\n // javascript:. Never bypassSecurityTrustHtml: Markdown is attacker-influenced content because VLM OCR\n // can be prompt-injected by text inside an image, so the sanitizer must stay on end to end.\n renderedMarkdown = computed<string>(() => this.renderMarkdown(this.markdownSource()));\n\n // #418: shared Markdown -> HTML step, used by both the left-column preview and the LongText\n // extracted-field values. Same GFM options and the same sanitize-on-bind contract: every caller MUST\n // bind the result via [innerHTML] so Angular's DomSanitizer runs. Never bypassSecurityTrustHtml — a\n // LongText field value is attacker-influenced too (LLM extraction / VLM OCR can be prompt-injected), so\n // the sanitizer has to stay on end to end.\n // #448: strip stray ```markdown code fences first. A vision-LLM OCR transcription is sometimes wrapped —\n // wholly or partly — in a code fence despite the prompt forbidding it, which would make marked render the\n // fenced table as literal <pre><code> (raw pipes) instead of a GFM table. The backend guard removes this at\n // the source for new documents; stripping here also rescues documents already stored with the fence\n // (Document.Markdown is write-once).\n private renderMarkdown(md: string): string {\n return md ? (marked.parse(stripMarkdownCodeFences(md), { gfm: true, async: false }) as string) : '';\n }\n\n // Owning cabinet name for the document, cabinetId to name. Returns null when unclassified or\n // unresolved because of missing permission or deleted cabinet.\n cabinetName = computed<string | null>(() => {\n const id = this.document()?.cabinetId;\n if (!id) return null;\n return this.cabinets().find(c => c.id === id)?.name ?? null;\n });\n\n // Document type displayName, mapped from typeCode. Returns null when unclassified, and falls back to\n // code for cross-layer or deleted types.\n documentTypeDisplayName = computed<string | null>(() => {\n const code = this.document()?.documentTypeCode;\n if (!code) return null;\n return this.documentTypes().find(t => t.typeCode === code)?.displayName ?? code;\n });\n\n // Type-bound extracted fields (field architecture v2). Show only values corresponding to currently\n // active field definitions:\n // The backend ExtractedFields output pierces soft-delete and still includes historical values for\n // deleted field definitions, preserving data for downstream consumers (#206/#207). The operator UI no\n // longer shows them, matching the list's dynamic columns which also use only active definitions. Labels\n // use displayName and sorting uses displayOrder.\n extractedFieldEntries = computed<{ key: string; label: string; value: string; isMarkdown: boolean; renderedHtml: string }[]>(() => {\n const fields = this.document()?.extractedFields;\n if (!fields) return [];\n const defByName = new Map(this.fieldDefinitions().map(d => [d.name ?? '', d]));\n return Object.keys(fields)\n .filter(key => defByName.has(key))\n .sort((a, b) =>\n (defByName.get(a)!.displayOrder ?? 0) - (defByName.get(b)!.displayOrder ?? 0) ||\n a.localeCompare(b))\n .map(key => {\n const def = defByName.get(key)!;\n const raw = fields[key];\n const value = this.formatFieldValue(raw);\n // #418: a LongText value is rendered as Markdown here in the detail view (never in the compact\n // list, so multi-paragraph content cannot blow apart table rows). LongText is an explicit\n // config-time choice on FieldDefinition.DataType — a declared type, not a runtime guess. Only\n // render when there is real string content; a null / empty value (formatted as \"—\") stays plain\n // text. renderedHtml keeps the sanitize-on-bind contract via renderMarkdown + [innerHTML].\n const isMarkdown =\n def.dataType === FieldDataType.LongText &&\n typeof raw === 'string' &&\n raw.trim().length > 0;\n return {\n key,\n label: def.displayName || key,\n value,\n isMarkdown,\n renderedHtml: isMarkdown ? this.renderMarkdown(value) : '',\n };\n });\n });\n\n // Field card display condition: existing extracted values for read-only display, or editable with field\n // definitions on this type so empty fields can be completed.\n showFieldsCard = computed(() =>\n this.extractedFieldEntries().length > 0 ||\n (this.canEditFields && this.fieldDefinitions().length > 0)\n );\n\n private documentId!: string;\n // If the blob has not loaded when Download File is clicked, set this flag and trigger one download\n // after the blob is ready. See downloadFile plus the constructor effect.\n private pendingDownload = false;\n // #440: subscription to the pending poll tick (null when not polling) + the current backoff interval.\n private pollTimer: Subscription | null = null;\n private pollIntervalMs = POLL_BASE_INTERVAL_MS;\n\n constructor() {\n // Pending download completion: when the blob arrives, trigger one download; when blob loading fails,\n // show a hint. pendingDownload is a plain boolean, and this effect reruns only when fileBlob signals\n // change. The download click first changes signals by triggering loading, so the logic naturally hits\n // only once.\n effect(() => {\n const url = this.fileBlob.blobUrl();\n const failed = this.fileBlob.hasError();\n if (!this.pendingDownload) return;\n if (url) {\n this.pendingDownload = false;\n this.fileBlob.download(this.downloadFileName());\n } else if (failed) {\n this.pendingDownload = false;\n this.toaster.error('::Document:DownloadFailed', '::Error');\n }\n });\n\n // #440: pause the background poll while the tab is hidden (no point re-fetching an unseen page) and\n // resume with an immediate refresh when it returns to the foreground. Always tear the timer down on\n // destroy so navigating away stops polling.\n const onVisibilityChange = () => {\n if (this.domDocument.hidden) {\n this.clearPollTimer();\n } else if (this.shouldPoll() && !this.pollTimer) {\n this.pollReload();\n }\n };\n this.domDocument.addEventListener('visibilitychange', onVisibilityChange);\n this.destroyRef.onDestroy(() => {\n this.domDocument.removeEventListener('visibilitychange', onVisibilityChange);\n this.clearPollTimer();\n });\n }\n\n ngOnInit(): void {\n // React to the :id route param rather than reading a one-time snapshot. Navigating between documents\n // that share this route — a sub-document's \"view parent\"/\"view siblings\" actions, or any /documents/:id\n // link — reuses this component instance, so ngOnInit does not fire again. Without reacting to the param\n // the URL would change but the loaded document would not. Reload whenever the id actually changes,\n // dropping the previous document's cached file blob and resetting the tab so a stale preview never\n // carries over to the new document.\n this.route.paramMap\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(params => {\n const id = params.get('id');\n if (!id || id === this.documentId) return;\n this.documentId = id;\n this.fileBlob.reset();\n this.activeTab.set('preview');\n this.loadDocument();\n });\n }\n\n refresh(): void {\n this.loadDocument();\n }\n\n private loadDocument(): void {\n this.isLoading.set(true);\n // Any explicit (re)load — navigation, manual Refresh, post-action reload — restarts the poll backoff\n // from the base interval.\n this.pollIntervalMs = POLL_BASE_INTERVAL_MS;\n this.fetchDocument(false);\n }\n\n // #440: a background poll tick. Silently re-fetches — never toggles isLoading, so the full-page spinner\n // and the Refresh-button spin stay quiet — and skips the one-time metadata (parent / cabinets) that does\n // not change mid-pipeline, refreshing field definitions only when classification has just resolved a type.\n private pollReload(): void {\n this.fetchDocument(true);\n }\n\n private fetchDocument(quiet: boolean): void {\n // doc and runs are independent after #216, so load them once in parallel; fieldDefinitions still\n // depend on doc.documentTypeCode and remain sequential.\n forkJoin({\n doc: this.documentService.get(this.documentId),\n runs: this.documentPipelineRunService.getList(this.documentId),\n })\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: ({ doc, runs }) => {\n const previousTypeCode = this.document()?.documentTypeCode ?? null;\n this.document.set(doc);\n this.pipelineRuns.set(runs);\n if (!quiet) {\n this.isLoading.set(false);\n }\n // Original-file blob lazy loading (#274): do not fetch while loading the document by default;\n // download only when the Original File tab is selected. See selectTab.\n // If the user is already on that tab, call ensureFilePreview once after reload\n // (Refresh / rerecognize). It returns early when the blob is cached and resets a previous error\n // for retry, preventing Refresh from being ineffective on a stuck preview (#274 review).\n if (this.activeTab() === 'file') {\n this.ensureFilePreview();\n }\n // Field definitions are used for: 1. displayName in extracted-field display for all viewers;\n // 2. completing empty fields when editable. Backend GetListAsync has been open to\n // Documents.Default since #223, so load whenever a type exists and no longer gate on edit\n // permission. #395: visible types are always loaded (even when unclassified) so the\n // confirm-classification picker has its options.\n // #440: on a quiet poll, reload them only when the type actually changed (classification just\n // resolved) — otherwise the fields card would flash empty on every tick.\n if (!quiet || (doc.documentTypeCode ?? null) !== previousTypeCode) {\n this.fieldDefinitions.set([]);\n this.loadDocumentTypesAndFields(doc.documentTypeCode);\n }\n // #306/#354: a sub-document carries its source (container) id. Fetch the parent's lightweight\n // metadata so the provenance banner can show its title; reset first so a previous document's\n // parent never lingers when navigating between documents in the same component instance.\n // #440: skipped on a quiet poll — provenance is immutable for a loaded document, so re-fetching\n // it would only make the banner flicker.\n if (!quiet) {\n this.parentDocument.set(null);\n this.parentLookupFailed.set(false);\n if (doc.originDocumentId) {\n this.loadParentDocument(doc.originDocumentId);\n }\n }\n // Cabinet name mapping: fetch only when Cabinets.Default is granted and not already loaded. If\n // there is no permission, the cabinet row is hidden.\n if (this.canViewCabinets && this.cabinets().length === 0) {\n this.loadCabinets();\n }\n // #440: (re)schedule or stop the poll based on the freshly loaded state.\n this.syncPolling();\n },\n error: () => {\n if (!quiet) {\n this.isLoading.set(false);\n }\n // #440: stop polling on a failed tick rather than hammering a failing endpoint. Manual Refresh\n // stays available, and any successful (re)load restarts it.\n this.stopPolling();\n },\n });\n }\n\n // #440: a document warrants polling only while it is genuinely still advancing on the server — a known\n // pipeline run is Pending/Running, or it is pre-terminal (Uploaded/Processing) and NOT parked waiting on\n // the operator. The needs-review guard matters because a blocking review reason (e.g. low-confidence\n // UnresolvedClassification) leaves lifecycle at Processing indefinitely — DeriveLifecycleAsync only\n // reaches Ready once no blocking reason remains — so without the guard such a document would poll forever.\n // When a run is actually in progress (e.g. the operator re-classified a needs-review document) we still\n // poll, because pipelineInProgress() takes precedence.\n private shouldPoll(): boolean {\n return this.pipelineInProgress() || (this.isProcessing() && !this.needsReview());\n }\n\n // #440: called after every load. Cancels any pending tick, then — if still in-flight and the tab is\n // visible — schedules the next silent re-fetch, backing the interval off toward the cap.\n private syncPolling(): void {\n this.clearPollTimer();\n if (!this.shouldPoll()) {\n this.pollIntervalMs = POLL_BASE_INTERVAL_MS;\n return;\n }\n if (this.domDocument.hidden) {\n return; // resumed by the visibilitychange handler in the constructor\n }\n this.pollTimer = timer(this.pollIntervalMs).subscribe(() => {\n this.pollIntervalMs = Math.min(this.pollIntervalMs * 2, POLL_MAX_INTERVAL_MS);\n this.pollReload();\n });\n }\n\n private clearPollTimer(): void {\n this.pollTimer?.unsubscribe();\n this.pollTimer = null;\n }\n\n private stopPolling(): void {\n this.clearPollTimer();\n this.pollIntervalMs = POLL_BASE_INTERVAL_MS;\n }\n\n // doc.documentTypeCode is the current code projection in the Document output contract (#207). The\n // field definition API associates by immutable DocumentTypeId, so first resolve code to id from types\n // visible in the current layer, then query. #395: visible types are always stored (the\n // confirm-classification picker needs them even when the document is unclassified); field definitions\n // are only fetched when the document already has a type.\n private loadDocumentTypesAndFields(typeCode: string | null | undefined): void {\n this.documentTypeService.getVisible()\n .pipe(\n // One getVisible call serves two purposes: store documentTypes for document type displayName\n // mapping + the confirm picker, then resolve typeId for field definition lookup.\n tap(types => this.documentTypes.set(types)),\n switchMap(types => {\n if (!typeCode) return of<FieldDefinitionDto[]>([]);\n const documentTypeId = types.find(t => t.typeCode === typeCode)?.id;\n if (!documentTypeId) return of<FieldDefinitionDto[]>([]);\n return this.fieldDefinitionService.getList({ documentTypeId });\n }),\n takeUntilDestroyed(this.destroyRef),\n )\n .subscribe({\n next: defs => this.fieldDefinitions.set(\n [...defs].sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0) || (a.name ?? '').localeCompare(b.name ?? '')),\n ),\n error: () => this.fieldDefinitions.set([]),\n });\n }\n\n // Cabinet candidates for name mapping display plus reassignment dropdown (#257). Called only with\n // Cabinets.Default permission.\n private loadCabinets(): void {\n this.cabinetService.getList()\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: list => this.cabinets.set(list),\n error: () => this.cabinets.set([]),\n });\n }\n\n // #306/#354: load the source (container) document of a sub-document for the provenance banner. A removed\n // (soft / permanently deleted) or cross-layer parent (404 / filtered) is an EXPECTED outcome — a sub-document\n // outlives its source — so pass skipHandleError to suppress ABP's global error popup; the component handles it by\n // flagging parentLookupFailed, and the banner shows a \"source unavailable\" label. It never blocks the\n // sub-document's own page.\n private loadParentDocument(originDocumentId: string): void {\n this.documentService.get(originDocumentId, { skipHandleError: true })\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: parent => {\n this.parentDocument.set(parent);\n this.parentLookupFailed.set(false);\n },\n error: () => {\n this.parentDocument.set(null);\n this.parentLookupFailed.set(true);\n },\n });\n }\n\n // Cabinet reassignment (#257): enter edit mode and preselect the current cabinet; empty string means\n // unclassified.\n startEditCabinet(): void {\n this.selectedCabinetId.set(this.document()?.cabinetId ?? '');\n this.isEditingCabinet.set(true);\n }\n\n cancelEditCabinet(): void {\n this.isEditingCabinet.set(false);\n }\n\n saveCabinet(): void {\n const doc = this.document();\n if (!doc) return;\n this.isSavingCabinet.set(true);\n // Empty string becomes null, meaning removed from cabinet / unclassified. Backend validates cabinet\n // existence and current-layer ownership.\n const cabinetId = this.selectedCabinetId() || null;\n this.documentService.updateCabinet(doc.id!, { cabinetId })\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: updated => {\n this.document.set(updated);\n this.isSavingCabinet.set(false);\n this.isEditingCabinet.set(false);\n this.toaster.success('::Document:CabinetUpdated', '::Success');\n },\n error: () => {\n this.isSavingCabinet.set(false);\n this.toaster.error('::Document:UpdateFailed', '::Error');\n },\n });\n }\n\n // Unified entry point for the Original File tab (#274 review): reset the previous preview error first\n // so <img> rebuilds and retries, and failed downloads can refetch. Then ensure the blob is ready; the\n // service prevents duplicate requests and revokes on component destroy.\n // Fixes imageError getting stuck and Refresh being ineffective.\n private ensureFilePreview(): void {\n // A blob-less sub-document has no original file: never call getBlob for it (would throw\n // Extract:DocumentNoSourceBlob). The Original File tab is hidden for it, but loadDocument still reaches\n // here when the tab was active, so guard at the source — this is the path that re-fired on every reload.\n if (!this.hasSourceFile()) return;\n this.fileBlob.resetError();\n this.fileBlob.ensureLoaded(this.documentId);\n }\n\n // Tab switch (#274): when switching to Original File, ensure the original-file preview is ready.\n selectTab(tab: 'preview' | 'source' | 'file'): void {\n this.activeTab.set(tab);\n if (tab === 'file') {\n this.ensureFilePreview();\n }\n }\n\n // Download File footer action: trigger browser download of the original file directly, avoiding an\n // extra trip through the preview page.\n // If the blob is cached, from viewing Original File or a previous download, trigger immediately. If not\n // cached, set pendingDownload and reuse the service fetch, which prevents duplicate requests. Blob\n // arrival or failure is completed by the constructor effect.\n downloadFile(): void {\n // Defensive: the Download action is hidden for blob-less sub-documents; never trigger a getBlob for one.\n if (!this.hasSourceFile()) return;\n if (this.fileBlob.blobUrl()) {\n this.fileBlob.download(this.downloadFileName());\n return;\n }\n this.pendingDownload = true;\n this.fileBlob.ensureLoaded(this.documentId);\n }\n\n private downloadFileName(): string {\n return this.document()?.fileOrigin?.originalFileName || this.document()?.title || 'document';\n }\n\n // <img> render failure, where the blob downloaded but decode failed, is folded into the unified preview\n // error signal.\n onPreviewError(): void {\n this.fileBlob.markError();\n }\n\n goBack(): void {\n // Return to wherever the operator came from — the review queue, the list, a cabinet-filtered view, etc.\n // — instead of always the list. The Angular Router stamps an incrementing navigationId on history.state;\n // it is > 1 once any in-app navigation has happened, and 1 on a direct deep-link / refresh (no in-app\n // history to pop). Fall back to the list in that case so Back never leaves the app.\n const navId = (this.location.getState() as { navigationId?: number } | null)?.navigationId;\n if (navId && navId > 1) {\n this.location.back();\n } else {\n this.router.navigate(['/documents/list']);\n }\n }\n\n // #354: open this sub-document's source (container) document.\n openParentDocument(): void {\n const originDocumentId = this.document()?.originDocumentId;\n if (!originDocumentId) return;\n this.router.navigate(['/documents', originDocumentId]);\n }\n\n // #411: open a suspected-duplicate candidate so the operator can compare it before allowing / discarding.\n openDocument(documentId: string): void {\n this.router.navigate(['/documents', documentId]);\n }\n\n // #354: list the sibling sub-documents (all those derived from the same source, including this one),\n // reusing the list's originDocumentId provenance filter via a deep-link query param.\n viewSiblingDocuments(): void {\n const originDocumentId = this.document()?.originDocumentId;\n if (!originDocumentId) return;\n this.router.navigate(['/documents/list'], { queryParams: { originDocumentId } });\n }\n\n delete(): void {\n const doc = this.document();\n if (!doc) return;\n this.confirmation\n .warn('::Document:AreYouSureToDelete', '::AreYouSure')\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(status => {\n if (status !== Confirmation.Status.confirm) return;\n this.documentService.delete(doc.id!)\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: () => {\n this.toaster.success('::Document:DeletedSuccessfully', '::Success');\n this.router.navigate(['/documents/list']);\n },\n });\n });\n }\n\n // #263 \"rerecognize\": rerun AI automatic classification on the existing Markdown, cascading to field\n // re-extraction without rerunning OCR.\n // This is an overwriting operation, replacing current type and manually edited field values, so confirm\n // first. Reload after success to reflect Processing state.\n rerecognize(): void {\n const doc = this.document();\n if (!doc || this.isRerecognizing()) return;\n this.confirmation\n .warn('::Document:Rerecognize:Confirm', '::AreYouSure')\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(status => {\n if (status !== Confirmation.Status.confirm) return;\n this.isRerecognizing.set(true);\n this.documentService.rerecognize(doc.id!)\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: () => {\n this.isRerecognizing.set(false);\n this.toaster.success('::Document:RerecognizeQueued', '::Success');\n this.loadDocument();\n },\n error: () => {\n this.isRerecognizing.set(false);\n this.toaster.error('::Document:RerecognizeFailed', '::Error');\n },\n });\n });\n }\n\n // #289 \"field re-extraction only\": rerun only the field-extraction pipeline on the existing\n // classification, without reclassification or OCR.\n // Safe leaf operation that overwrites only field values, including manual corrections. Confirm first.\n // Lifecycle-neutral: does not move already Ready documents backward.\n reextractFields(): void {\n const doc = this.document();\n if (!doc || this.isReextracting()) return;\n this.confirmation\n .warn('::Document:ReextractFields:Confirm', '::AreYouSure')\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(status => {\n if (status !== Confirmation.Status.confirm) return;\n this.isReextracting.set(true);\n this.documentService.reextractFields(doc.id!)\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: () => {\n this.isReextracting.set(false);\n this.toaster.success('::Document:ReextractFieldsQueued', '::Success');\n this.loadDocument();\n },\n error: () => {\n this.isReextracting.set(false);\n this.toaster.error('::Document:ReextractFieldsFailed', '::Error');\n },\n });\n });\n }\n\n // #395: manual confirm / assign document type. Authoritative override that clears\n // UnresolvedClassification and cascades field extraction (backend confirmClassification). Defaults the\n // picker to the document's current low-confidence type when present so the operator usually just confirms.\n openClassifyDialog(): void {\n const doc = this.document();\n if (!doc) return;\n this.selectedTypeId.set(\n this.documentTypes().find(t => t.typeCode === doc.documentTypeCode)?.id ?? '',\n );\n this.showClassifyDialog.set(true);\n }\n\n closeClassifyDialog(): void {\n this.showClassifyDialog.set(false);\n this.selectedTypeId.set('');\n }\n\n submitClassify(): void {\n const doc = this.document();\n if (!doc || !this.selectedTypeId() || this.isConfirmingClassification()) return;\n this.isConfirmingClassification.set(true);\n this.documentService.confirmClassification(doc.id!, { documentTypeId: this.selectedTypeId() })\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: () => {\n this.isConfirmingClassification.set(false);\n this.closeClassifyDialog();\n this.toaster.success('::Document:ClassificationConfirmed', '::Success');\n this.loadDocument();\n },\n error: () => {\n this.isConfirmingClassification.set(false);\n this.toaster.error('::Document:ConfirmFailed', '::Error');\n },\n });\n }\n\n // #395: reject the document (#237/#284 recoverable disposition). The rejection reason is required by the\n // backend RejectReviewInput.Reason; reload after success so disposition/badge reflect the new state.\n openRejectDialog(): void {\n if (!this.document()) return;\n this.rejectReason.set('');\n this.showRejectDialog.set(true);\n }\n\n closeRejectDialog(): void {\n this.showRejectDialog.set(false);\n this.rejectReason.set('');\n }\n\n submitReject(): void {\n const doc = this.document();\n if (!doc || this.isRejecting()) return;\n const reason = this.rejectReason().trim();\n if (!reason) {\n this.toaster.warn('::Document:Review:RejectReasonRequired');\n return;\n }\n this.isRejecting.set(true);\n this.documentService.rejectReview(doc.id!, { reason })\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: () => {\n this.isRejecting.set(false);\n this.closeRejectDialog();\n this.toaster.success('::Document:Review:RejectedSuccessfully', '::Success');\n this.loadDocument();\n },\n error: () => {\n this.isRejecting.set(false);\n this.toaster.error('::Document:Review:ActionFailed', '::Error');\n },\n });\n }\n\n // #411: operator decides a suspected duplicate is not a duplicate (or is an acceptable re-upload). The backend\n // sets the durable DuplicateAllowed override, clears the blocking reason, and re-derives lifecycle (releasing the\n // document to Ready + DocumentReadyEto when nothing else blocks). Reload so the badge / banner reflect the change.\n allowDuplicate(): void {\n const doc = this.document();\n if (!doc || this.isAllowingDuplicate()) return;\n this.isAllowingDuplicate.set(true);\n this.documentService.allowDuplicate(doc.id!)\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: () => {\n this.isAllowingDuplicate.set(false);\n this.toaster.success('::Document:Review:DuplicateAllowed', '::Success');\n this.loadDocument();\n },\n error: () => {\n this.isAllowingDuplicate.set(false);\n this.toaster.error('::Document:Review:ActionFailed', '::Error');\n },\n });\n }\n\n getStatusBadgeClass(status: DocumentLifecycleStatus | undefined): string {\n switch (status) {\n case DocumentLifecycleStatus.Uploaded: return 'badge bg-secondary';\n case DocumentLifecycleStatus.Processing: return 'badge bg-warning text-dark';\n case DocumentLifecycleStatus.Ready: return 'badge bg-success';\n case DocumentLifecycleStatus.Failed: return 'badge bg-danger';\n default: return 'badge bg-secondary';\n }\n }\n\n // #284: header badge shows only lifecycle on the availability axis. Review state is expressed by the\n // banner plus detail-area reviewReasonDetails and is not duplicated.\n getDocumentStatusBadgeClass(doc: DocumentDto): string {\n return this.getStatusBadgeClass(doc.lifecycleStatus);\n }\n\n getStatusLabel(status: DocumentLifecycleStatus | undefined): string {\n switch (status) {\n case DocumentLifecycleStatus.Uploaded: return '::Document:Status:Uploaded';\n case DocumentLifecycleStatus.Processing: return '::Document:Status:Processing';\n case DocumentLifecycleStatus.Ready: return '::Document:Status:Ready';\n case DocumentLifecycleStatus.Failed: return '::Document:Status:Failed';\n default: return '::Document:Status:Unknown';\n }\n }\n\n getDocumentStatusLabel(doc: DocumentDto): string {\n return this.getStatusLabel(doc.lifecycleStatus);\n }\n\n // #284: review reason to localization key, used for detail-area reviewReasonDetails rendering.\n reviewReasonLabel(reason: DocumentReviewReasons | undefined): string {\n switch (reason) {\n case DocumentReviewReasons.UnresolvedClassification:\n return '::Document:ReviewReason:UnresolvedClassification';\n case DocumentReviewReasons.MissingRequiredFields:\n return '::Document:ReviewReason:MissingRequiredFields';\n case DocumentReviewReasons.SegmentationIncomplete:\n return '::Document:ReviewReason:SegmentationIncomplete';\n case DocumentReviewReasons.DuplicateSuspected:\n return '::Document:ReviewReason:DuplicateSuspected';\n default:\n return '::Document:NeedsReview';\n }\n }\n\n // #412: per-reason remediation hint shown in the banner, so the operator knows what each reason expects\n // without inferring it from the (reason-neutral) headline. Each reason has exactly one remedy: assign a\n // type, fill the fields below, re-classify, or release/delete the duplicate.\n reviewReasonHint(reason: DocumentReviewReasons | undefined): string {\n switch (reason) {\n case DocumentReviewReasons.UnresolvedClassification:\n return '::Document:Review:Hint:UnresolvedClassification';\n case DocumentReviewReasons.MissingRequiredFields:\n return '::Document:Review:Hint:MissingRequiredFields';\n case DocumentReviewReasons.SegmentationIncomplete:\n return '::Document:Review:Hint:SegmentationIncomplete';\n case DocumentReviewReasons.DuplicateSuspected:\n return '::Document:Review:Hint:DuplicateSuspected';\n default:\n return '::Document:NeedsReview';\n }\n }\n\n // #412: \"Complete fields\" CTA — jump to the extracted-fields card and open the edit form so the missing\n // required values can be filled. Filling them clears MissingRequiredFields server-side on the next save.\n completeFields(): void {\n if (this.canEditFields && this.fieldDefinitions().length > 0 && !this.isEditingFields()) {\n this.startEditFields();\n }\n document.getElementById('extracted-fields-card')\n ?.scrollIntoView({ behavior: 'smooth', block: 'start' });\n }\n\n getRunStatusBadgeClass(status: PipelineRunStatus | undefined): string {\n switch (status) {\n case PipelineRunStatus.Pending: return 'badge bg-secondary';\n case PipelineRunStatus.Running: return 'badge bg-warning text-dark';\n case PipelineRunStatus.Succeeded: return 'badge bg-success';\n case PipelineRunStatus.Failed: return 'badge bg-danger';\n case PipelineRunStatus.Skipped: return 'badge bg-light text-dark border';\n default: return 'badge bg-light text-muted border';\n }\n }\n\n getRunStatusLabel(status: PipelineRunStatus | undefined): string {\n switch (status) {\n case PipelineRunStatus.Pending: return '::Document:Pipeline:Status:Pending';\n case PipelineRunStatus.Running: return '::Document:Pipeline:Status:Running';\n case PipelineRunStatus.Succeeded: return '::Document:Pipeline:Status:Succeeded';\n case PipelineRunStatus.Failed: return '::Document:Pipeline:Status:Failed';\n case PipelineRunStatus.Skipped: return '::Document:Pipeline:Status:Skipped';\n default: return '::Document:Pipeline:Status:NotStarted';\n }\n }\n\n isRunInProgress(status: PipelineRunStatus | undefined): boolean {\n return status === PipelineRunStatus.Pending || status === PipelineRunStatus.Running;\n }\n\n isRetryable(run: DocumentPipelineRunDto | null | undefined): boolean {\n return !!run && run.status === PipelineRunStatus.Failed;\n }\n\n retryPipeline(pipelineCode: string): void {\n if (this.retryingPipeline() !== null) return;\n\n this.retryingPipeline.set(pipelineCode);\n this.documentService.retryPipeline(this.documentId, { pipelineCode })\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: () => {\n this.retryingPipeline.set(null);\n this.toaster.success('::Document:Pipeline:RetryQueued', '::Success');\n this.loadDocument();\n },\n error: () => {\n this.retryingPipeline.set(null);\n this.toaster.error('::Document:Pipeline:RetryFailed', '::Error');\n },\n });\n }\n\n getElapsedMs(run: DocumentPipelineRunDto): number | null {\n if (!run.startedAt) return null;\n const start = new Date(run.startedAt).getTime();\n if (Number.isNaN(start)) return null;\n const end = run.completedAt ? new Date(run.completedAt).getTime() : Date.now();\n if (Number.isNaN(end) || end < start) return null;\n return end - start;\n }\n\n formatFieldValue(value: unknown): string {\n return formatExtractedFieldValue(value);\n }\n\n startEditFields(): void {\n this.extractedFieldFormFields.set(this.createExtractedFieldFormFields());\n this.isEditingFields.set(true);\n }\n\n cancelEditFields(): void {\n this.isEditingFields.set(false);\n this.extractedFieldFormFields.set([]);\n }\n\n saveFields(formValue: Record<string, unknown>): void {\n const doc = this.document();\n if (!doc) return;\n this.isSavingFields.set(true);\n\n const fields: Record<string, unknown> = {};\n for (const def of this.fieldDefinitions()) {\n const key = def.name ?? '';\n const value = formValue[key];\n\n if (this.shouldOmitFieldValue(value)) continue;\n fields[key] = this.coerceValue(def, value);\n }\n\n this.documentService.updateExtractedFields(doc.id!, { fields })\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: updated => {\n this.document.set(updated);\n this.isSavingFields.set(false);\n this.isEditingFields.set(false);\n this.extractedFieldFormFields.set([]);\n this.toaster.success('::Document:FieldsUpdated', '::Success');\n },\n error: () => {\n this.isSavingFields.set(false);\n this.toaster.error('::Document:UpdateFailed', '::Error');\n },\n });\n }\n\n private createExtractedFieldFormFields(): FormFieldConfig[] {\n const values = this.document()?.extractedFields ?? {};\n\n return this.fieldDefinitions().map(def => {\n const config: FormFieldConfig = {\n key: def.name ?? '',\n label: `${def.displayName} (${def.name})`,\n // Multi-value fields (#212, text only) use a textarea with one value per line; single-value fields\n // choose input type by DataType.\n type: def.allowMultiple ? 'textarea' : this.toFormFieldType(def.dataType),\n value: this.toFormInitialValue(def, values[def.name ?? '']),\n required: def.isRequired,\n order: def.displayOrder,\n gridSize: 12,\n validators: def.isRequired\n ? [{ type: 'required', message: '::FieldDefinition:Required' }]\n : [],\n };\n\n if (def.allowMultiple) {\n config.placeholder = this.localization.instant('::FieldDefinition:AllowMultipleEditHint');\n } else if (def.dataType === FieldDataType.Number) {\n config.step = 'any';\n } else if (def.dataType === FieldDataType.Boolean) {\n config.options = {\n defaultValues: [\n { key: 'true', value: 'true' },\n { key: 'false', value: 'false' },\n ],\n };\n }\n\n return config;\n });\n }\n\n private toFormFieldType(dataType: FieldDataType | undefined): FormFieldConfig['type'] {\n switch (dataType) {\n // Long text, such as summaries or descriptions, uses a multiline editor. The default branch of\n // toFormInitialValue already fills it back as a string unchanged.\n case FieldDataType.LongText:\n return 'textarea';\n case FieldDataType.Number:\n return 'number';\n case FieldDataType.Boolean:\n return 'select';\n case FieldDataType.Date:\n return 'date';\n case FieldDataType.DateTime:\n return 'datetime-local';\n default:\n return 'text';\n }\n }\n\n private toFormInitialValue(def: FieldDefinitionDto, value: unknown): unknown {\n // Multi-value fields (#212): output array becomes one textarea line per value. Non-arrays, including\n // null or unextracted values, become empty.\n if (def.allowMultiple) {\n return Array.isArray(value) ? value.map(v => String(v)).join('\\n') : '';\n }\n\n if (value === null || value === undefined) return '';\n\n switch (def.dataType) {\n case FieldDataType.Number:\n return this.toNumberInputValue(value);\n case FieldDataType.Boolean:\n return this.parseBoolean(value) ? 'true' : 'false';\n case FieldDataType.Date:\n return this.toDateInputValue(value);\n case FieldDataType.DateTime:\n return this.toDateTimeLocalInputValue(value);\n default:\n return typeof value === 'object' ? JSON.stringify(value) : String(value);\n }\n }\n\n private shouldOmitFieldValue(value: unknown): boolean {\n return value === null ||\n value === undefined ||\n (typeof value === 'string' && value.trim() === '');\n }\n\n // Convert to the corresponding JSON type by field DataType. Date/DateTime/Text are always stored as\n // strings.\n private coerceValue(def: FieldDefinitionDto, value: unknown): unknown {\n // Multi-value fields (#212): textarea one value per line, trimmed and with empty lines removed,\n // becomes string[], symmetric with backend UpdateExtractedFieldsAsync receiving arrays.\n if (def.allowMultiple) {\n return String(value ?? '')\n .split(/\\r?\\n/)\n .map(s => s.trim())\n .filter(s => s.length > 0);\n }\n\n switch (def.dataType) {\n case FieldDataType.Number: {\n const n = typeof value === 'number' ? value : Number(value);\n return !Number.isNaN(n) ? n : value;\n }\n case FieldDataType.Boolean:\n return this.parseBoolean(value);\n default:\n return value;\n }\n }\n\n private toNumberInputValue(value: unknown): string {\n const raw = String(value).trim();\n if (raw === '') return '';\n const n = Number(raw);\n return Number.isNaN(n) ? '' : raw;\n }\n\n private parseBoolean(value: unknown): boolean {\n if (typeof value === 'boolean') return value;\n if (typeof value === 'number') return value !== 0;\n const normalized = String(value).trim().toLowerCase();\n return normalized === 'true' || normalized === '1' || normalized === 'yes';\n }\n\n private toDateInputValue(value: unknown): string {\n const raw = String(value);\n return /^\\d{4}-\\d{2}-\\d{2}/.test(raw) ? raw.slice(0, 10) : raw;\n }\n\n private toDateTimeLocalInputValue(value: unknown): string {\n const raw = String(value);\n if (!raw) return '';\n if (/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}/.test(raw)) {\n return raw.slice(0, 16);\n }\n\n const parsed = new Date(raw);\n if (Number.isNaN(parsed.getTime())) return raw;\n\n const pad = (n: number) => String(n).padStart(2, '0');\n return `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}-${pad(parsed.getDate())}` +\n `T${pad(parsed.getHours())}:${pad(parsed.getMinutes())}`;\n }\n\n formatElapsed(run: DocumentPipelineRunDto): string {\n const ms = this.getElapsedMs(run);\n if (ms == null) return '';\n if (ms < 1000) return `${ms} ms`;\n const seconds = ms / 1000;\n if (seconds < 60) return `${seconds.toFixed(1)} s`;\n const minutes = Math.floor(seconds / 60);\n const remSeconds = Math.round(seconds - minutes * 60);\n return `${minutes}m ${remSeconds}s`;\n }\n\n protected pickLatestRun(runs: DocumentPipelineRunDto[], pipelineCode: string): DocumentPipelineRunDto | null {\n const matches = runs.filter(r => r.pipelineCode === pipelineCode);\n if (matches.length === 0) return null;\n return matches.reduce((prev, curr) => ((curr.attemptNumber ?? 0) > (prev.attemptNumber ?? 0) ? curr : prev));\n }\n}\n","<div class=\"container-fluid py-4\">\n\n <!-- Back + header -->\n <div class=\"d-flex align-items-center mb-3 gap-2\">\n <button class=\"btn btn-outline-secondary btn-sm\" (click)=\"goBack()\">\n <i class=\"fas fa-arrow-left me-1\"></i>\n {{ '::Back' | abpLocalization }}\n </button>\n @if (document()) {\n <span class=\"ms-2 fw-semibold text-truncate\" style=\"max-width: 480px;\" [title]=\"document()!.title || document()!.fileOrigin?.originalFileName\">\n {{ document()!.title || document()!.fileOrigin?.originalFileName }}\n </span>\n <span [class]=\"getDocumentStatusBadgeClass(document()!)\" class=\"ms-2\">\n @if (isProcessing()) {\n <span class=\"spinner-border spinner-border-sm me-1\" role=\"status\"></span>\n }\n {{ getDocumentStatusLabel(document()!) | abpLocalization }}\n </span>\n }\n <div class=\"ms-auto d-flex gap-2\">\n <!-- Display label is \"重新分类 / Re-classify\": it re-runs classification on the already-extracted text,\n NOT OCR (the #263 internal name stays \"rerecognize\"). The tooltip states the original file is not\n re-scanned, to kill the \"re-OCR from the original file\" misconception. -->\n @if (canRerecognize()) {\n <button\n class=\"btn btn-outline-primary btn-sm\"\n (click)=\"rerecognize()\"\n [disabled]=\"isRerecognizing()\"\n title=\"{{ '::Document:Rerecognize:Hint' | abpLocalization }}\"\n >\n @if (isRerecognizing()) {\n <span class=\"spinner-border spinner-border-sm me-1\" role=\"status\"></span>\n {{ '::Document:Rerecognizing' | abpLocalization }}\n } @else {\n <i class=\"fas fa-wand-magic-sparkles me-1\"></i>\n {{ '::Document:Rerecognize' | abpLocalization }}\n }\n </button>\n }\n @if (canReextractFields()) {\n <button\n class=\"btn btn-outline-secondary btn-sm\"\n (click)=\"reextractFields()\"\n [disabled]=\"isReextracting()\"\n title=\"{{ '::Document:ReextractFields' | abpLocalization }}\"\n >\n @if (isReextracting()) {\n <span class=\"spinner-border spinner-border-sm me-1\" role=\"status\"></span>\n {{ '::Document:ReextractFieldsInProgress' | abpLocalization }}\n } @else {\n <i class=\"fas fa-list-check me-1\"></i>\n {{ '::Document:ReextractFields' | abpLocalization }}\n }\n </button>\n }\n @if (canDelete && document()) {\n <button\n class=\"btn btn-outline-danger btn-sm\"\n (click)=\"delete()\"\n title=\"{{ '::Delete' | abpLocalization }}\"\n >\n <i class=\"fas fa-trash me-1\"></i>\n {{ '::Delete' | abpLocalization }}\n </button>\n }\n <button\n class=\"btn btn-outline-secondary btn-sm\"\n (click)=\"refresh()\"\n [disabled]=\"isLoading()\"\n title=\"{{ '::Refresh' | abpLocalization }}\"\n >\n <i class=\"fas fa-sync-alt\" [class.fa-spin]=\"isLoading()\"></i>\n </button>\n </div>\n </div>\n\n <!-- Loading state -->\n @if (isLoading()) {\n <div class=\"text-center py-5\">\n <div class=\"spinner-border text-primary\" role=\"status\"></div>\n </div>\n }\n\n @if (document()) {\n <!-- Sub-document provenance banner (#306/#354): this document was derived from a container document.\n Offer navigation back to the source and to its sibling sub-documents. -->\n @if (isSubDocument()) {\n <div class=\"alert alert-info d-flex flex-wrap align-items-center gap-2 mb-3\" role=\"alert\">\n <i class=\"fas fa-sitemap me-1\"></i>\n <span>\n {{ '::Document:SubDocumentOf' | abpLocalization }}\n @if (parentDocument(); as parent) {\n <strong>{{ parent.title || parent.fileOrigin?.originalFileName || document()!.originDocumentId }}</strong>\n } @else if (parentLookupFailed()) {\n <!-- Source removed (soft / permanently deleted) or cross-layer: show an explicit \"unavailable\" label\n instead of a raw id, and drop the dead \"view parent\" link below. The sub-document stays fully usable. -->\n <strong>{{ '::Document:SourceDocumentUnavailable' | abpLocalization }}</strong>\n } @else {\n <strong>{{ document()!.originDocumentId }}</strong>\n }\n </span>\n <div class=\"ms-auto d-flex gap-2\">\n @if (parentDocument()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-primary\" (click)=\"openParentDocument()\">\n <i class=\"fas fa-arrow-up me-1\"></i>{{ '::Document:ViewParentDocument' | abpLocalization }}\n </button>\n }\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click)=\"viewSiblingDocuments()\">\n <i class=\"fas fa-sitemap me-1\"></i>{{ '::Document:ViewSiblingDocuments' | abpLocalization }}\n </button>\n </div>\n </div>\n }\n\n <!-- Review / processing banner. #395/#412: contextual remediation actions live here. The banner is\n reason-aware — the active review reasons drive both the badges and which CTA appears: classification\n is confirmed/assigned via the dialog, missing fields are completed in the fields card below, a\n suspected duplicate is released, and the document can always be rejected. -->\n @if (needsReview()) {\n <div class=\"alert alert-warning mb-3\" role=\"alert\">\n <div class=\"d-flex align-items-center flex-wrap gap-2\">\n <i class=\"fas fa-user-edit me-2\"></i>\n <span>{{ '::Document:PendingReviewMessage' | abpLocalization }}</span>\n <!-- #412: surface the concrete reason(s) right here, so the operator no longer has to read them off\n the metadata list in the right column. Blocking reasons are red, advisory ones amber. -->\n @for (detail of document()!.reviewReasonDetails; track detail.reason) {\n <span class=\"badge\"\n [class.bg-danger]=\"detail.isBlocking\"\n [class.bg-warning]=\"!detail.isBlocking\"\n [class.text-dark]=\"!detail.isBlocking\">\n {{ reviewReasonLabel(detail.reason) | abpLocalization }}\n </span>\n }\n @if (canEditFields) {\n <div class=\"ms-auto d-flex gap-2\">\n @if (needsClassification()) {\n <button type=\"button\" class=\"btn btn-sm btn-warning\" (click)=\"openClassifyDialog()\">\n <i class=\"fas fa-check me-1\"></i>\n {{ '::Document:ConfirmClassification' | abpLocalization }}\n </button>\n }\n <!-- #412: missing required fields are completed below — jump there and open the editor. -->\n @if (needsFieldCompletion()) {\n <button type=\"button\" class=\"btn btn-sm btn-warning\" (click)=\"completeFields()\">\n <i class=\"fas fa-pen me-1\"></i>\n {{ '::Document:Review:CompleteFields' | abpLocalization }}\n </button>\n }\n <!-- #411: resolve a suspected duplicate — \"Allow\" releases it (not a duplicate / acceptable re-upload);\n confirming the duplicate is the Delete action in the header toolbar. -->\n @if (needsDuplicateReview()) {\n <button type=\"button\" class=\"btn btn-sm btn-success\" (click)=\"allowDuplicate()\" [disabled]=\"isAllowingDuplicate()\">\n <i class=\"fas fa-check-double me-1\"></i>\n {{ '::Document:Review:AllowDuplicate' | abpLocalization }}\n </button>\n }\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger\" (click)=\"openRejectDialog()\">\n <i class=\"fas fa-ban me-1\"></i>\n {{ '::Document:Review:Reject' | abpLocalization }}\n </button>\n </div>\n }\n </div>\n <!-- #412: one remediation hint per active reason, so each reason states its own expected action\n instead of leaning on the single (reason-neutral) headline above. -->\n @for (detail of document()!.reviewReasonDetails; track detail.reason) {\n <div class=\"small text-muted mt-2\">\n {{ reviewReasonHint(detail.reason) | abpLocalization }}\n @if (detail.missingFieldNames?.length) {\n <span class=\"fw-semibold\">{{ detail.missingFieldNames!.join('、') }}</span>\n }\n </div>\n }\n </div>\n } @else if (isProcessing()) {\n <div class=\"alert alert-warning d-flex align-items-center mb-3\" role=\"alert\">\n <div class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></div>\n <span>{{ '::Document:ProcessingMessage' | abpLocalization }}</span>\n </div>\n }\n\n <div class=\"row g-3\">\n <!-- LEFT: Tab area: Markdown preview / source / original file (#274) -->\n <div class=\"col-lg-6\">\n <div class=\"card shadow-sm h-100 d-flex flex-column\">\n <div class=\"card-header p-0 pt-2\">\n <ul class=\"nav nav-tabs card-header-tabs mx-2 mb-0\">\n <li class=\"nav-item\">\n <button class=\"nav-link\" [class.active]=\"activeTab() === 'preview'\"\n (click)=\"selectTab('preview')\">\n <i class=\"fas fa-eye me-1\"></i>{{ '::Document:Tab:Preview' | abpLocalization }}\n </button>\n </li>\n <li class=\"nav-item\">\n <button class=\"nav-link\" [class.active]=\"activeTab() === 'source'\"\n (click)=\"selectTab('source')\">\n <i class=\"fas fa-code me-1\"></i>{{ '::Document:Tab:Source' | abpLocalization }}\n </button>\n </li>\n <!-- Sub-documents have no source blob (#306/#346); hide the Original File tab so the UI never\n calls GetBlobAsync for them (Extract:DocumentNoSourceBlob). -->\n @if (hasSourceFile()) {\n <li class=\"nav-item\">\n <button class=\"nav-link\" [class.active]=\"activeTab() === 'file'\"\n (click)=\"selectTab('file')\">\n <i class=\"fas fa-file me-1\"></i>{{ '::Document:OriginalFile' | abpLocalization }}\n </button>\n </li>\n }\n </ul>\n </div>\n <div class=\"card-body overflow-auto\" style=\"min-height: 400px; max-height: 80vh;\">\n @switch (activeTab()) {\n @case ('preview') {\n @if (document()!.markdown) {\n <div class=\"markdown-body\" [innerHTML]=\"renderedMarkdown()\"></div>\n } @else {\n <div class=\"text-center text-muted py-5\">\n <i class=\"fas fa-align-left fa-3x mb-3 d-block\"></i>\n <p class=\"mb-0\">{{ '::Document:Markdown:Empty' | abpLocalization }}</p>\n </div>\n }\n }\n @case ('source') {\n @if (document()!.markdown) {\n <pre class=\"ocr-text mb-0\">{{ document()!.markdown }}</pre>\n } @else {\n <div class=\"text-center text-muted py-5\">\n <p class=\"mb-0\">{{ '::Document:Markdown:Empty' | abpLocalization }}</p>\n </div>\n }\n }\n @case ('file') {\n @if (fileBlob.isLoading()) {\n <div class=\"text-center py-5\">\n <div class=\"spinner-border text-primary\" role=\"status\"></div>\n </div>\n } @else if (fileBlob.hasError()) {\n <div class=\"text-center text-muted py-5\">\n <i class=\"fas fa-exclamation-triangle fa-3x mb-3 d-block\"></i>\n <p class=\"mb-0\">{{ '::Document:PreviewUnavailable' | abpLocalization }}</p>\n </div>\n } @else if (isImage() && fileBlob.blobUrl()) {\n <div class=\"text-center\">\n <img\n [src]=\"fileBlob.blobUrl()\"\n (error)=\"onPreviewError()\"\n alt=\"{{ document()!.fileOrigin?.originalFileName }}\"\n class=\"img-fluid preview-image\"\n style=\"max-height: 70vh; object-fit: contain;\"\n />\n </div>\n } @else if (isPdf() && fileBlob.safeResourceUrl()) {\n <iframe\n [src]=\"fileBlob.safeResourceUrl()\"\n [title]=\"document()!.fileOrigin?.originalFileName\"\n class=\"w-100 border rounded\"\n style=\"min-height: 70vh;\"\n ></iframe>\n } @else {\n <div class=\"text-center text-muted py-5\">\n <i class=\"fas fa-image fa-3x mb-3 d-block\"></i>\n <p class=\"mb-0\">{{ '::Document:PreviewUnavailable' | abpLocalization }}</p>\n </div>\n }\n }\n }\n </div>\n <!-- Source-file footer (name / type / download): only for documents that actually have a source\n blob. A sub-document (#306/#346) has none, so hiding this avoids an empty footer and a Download\n button that would fail with Extract:DocumentNoSourceBlob. -->\n @if (hasSourceFile()) {\n <div class=\"card-footer text-muted small d-flex align-items-center gap-2\">\n <i class=\"fas fa-file\"></i>\n <span class=\"text-truncate\">{{ document()!.fileOrigin?.originalFileName }}</span>\n <span>·</span>\n <span class=\"flex-shrink-0\">{{ document()!.fileOrigin?.contentType }}</span>\n <button type=\"button\" (click)=\"downloadFile()\" [disabled]=\"fileBlob.isLoading()\"\n class=\"btn btn-sm btn-link p-0 ms-auto flex-shrink-0\">\n @if (fileBlob.isLoading()) {\n <span class=\"spinner-border spinner-border-sm me-1\" role=\"status\"></span>\n } @else {\n <i class=\"fas fa-download me-1\"></i>\n }\n {{ '::Document:DownloadFile' | abpLocalization }}\n </button>\n </div>\n }\n </div>\n </div>\n\n <!-- RIGHT: Document metadata, fields, pipelines -->\n <div class=\"col-lg-6 d-flex flex-column\">\n <div class=\"d-flex flex-column gap-3\">\n <div class=\"card shadow-sm\">\n <div class=\"card-body\">\n <dl class=\"row mb-0 small\">\n <dt class=\"col-sm-5 text-muted\">{{ '::Document:Type' | abpLocalization }}</dt>\n <dd class=\"col-sm-7\">\n @if (documentTypeDisplayName(); as typeName) {\n <span class=\"badge bg-info text-dark\">{{ typeName }}</span>\n } @else {\n <span class=\"text-muted\">—</span>\n }\n </dd>\n\n @if (canViewCabinets) {\n <dt class=\"col-sm-5 text-muted\">{{ '::Document:Cabinet' | abpLocalization }}</dt>\n <dd class=\"col-sm-7\">\n @if (isEditingCabinet()) {\n <div class=\"d-flex align-items-center gap-2 flex-wrap\">\n <select class=\"form-select form-select-sm w-auto\"\n [ngModel]=\"selectedCabinetId()\"\n (ngModelChange)=\"selectedCabinetId.set($event)\"\n [disabled]=\"isSavingCabinet()\">\n <option value=\"\">{{ '::Document:Unfiled' | abpLocalization }}</option>\n @for (c of cabinets(); track c.id) {\n <option [value]=\"c.id\">{{ c.name }}</option>\n }\n </select>\n <button type=\"button\" class=\"btn btn-sm btn-primary\"\n (click)=\"saveCabinet()\" [disabled]=\"isSavingCabinet()\">\n {{ '::Save' | abpLocalization }}\n </button>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"cancelEditCabinet()\" [disabled]=\"isSavingCabinet()\">\n {{ '::Cancel' | abpLocalization }}\n </button>\n </div>\n } @else {\n @if (cabinetName(); as name) {\n <span class=\"badge bg-body-tertiary border\">\n <i class=\"fas fa-folder text-warning me-1\"></i>{{ name }}\n </span>\n } @else {\n <span class=\"text-muted\">{{ '::Document:Unfiled' | abpLocalization }}</span>\n }\n <button type=\"button\" class=\"btn btn-sm btn-link p-0 ms-2\"\n (click)=\"startEditCabinet()\">\n {{ '::Edit' | abpLocalization }}\n </button>\n }\n </dd>\n }\n\n <dt class=\"col-sm-5 text-muted\">{{ '::Document:ClassificationConfidence' | abpLocalization }}</dt>\n <dd class=\"col-sm-7\">\n @if ((document()!.classificationConfidence ?? 0) > 0) {\n {{ ((document()!.classificationConfidence ?? 0) * 100).toFixed(0) }}%\n } @else {\n —\n }\n </dd>\n\n @if (document()!.reviewDisposition === DocumentReviewDisposition.Confirmed || document()!.reviewDisposition === DocumentReviewDisposition.Rejected) {\n <dt class=\"col-sm-5 text-muted\">{{ '::Document:ReviewStatus' | abpLocalization }}</dt>\n <dd class=\"col-sm-7\">\n @if (document()!.reviewDisposition === DocumentReviewDisposition.Confirmed) {\n <span class=\"badge bg-success\">\n {{ '::Document:ReviewDisposition:Confirmed' | abpLocalization }}\n </span>\n } @else {\n <span class=\"badge bg-danger\">\n {{ '::Document:ReviewDisposition:Rejected' | abpLocalization }}\n </span>\n }\n </dd>\n }\n\n @if (document()!.reviewReasonDetails?.length) {\n <dt class=\"col-sm-5 text-muted\">{{ '::Document:NeedsReview' | abpLocalization }}</dt>\n <dd class=\"col-sm-7\">\n @for (detail of document()!.reviewReasonDetails; track detail.reason) {\n <div class=\"small mb-1\">\n <span class=\"badge\"\n [class.bg-danger]=\"detail.isBlocking\"\n [class.bg-warning]=\"!detail.isBlocking\"\n [class.text-dark]=\"!detail.isBlocking\">\n {{ reviewReasonLabel(detail.reason) | abpLocalization }}\n </span>\n @if (detail.missingFieldNames?.length) {\n <span class=\"text-muted ms-1\">{{ detail.missingFieldNames!.join('、') }}</span>\n }\n <!-- #411: suspected-duplicate candidates — open each to compare before allowing / discarding. -->\n @if (detail.duplicateCandidates?.length) {\n <div class=\"text-muted ms-1 mt-1\">\n {{ '::Document:ReviewReason:DuplicateCandidates' | abpLocalization }}\n @for (candidate of detail.duplicateCandidates; track candidate.id) {\n <button type=\"button\" class=\"btn btn-link btn-sm p-0 ms-2 align-baseline text-start\"\n (click)=\"openDocument(candidate.id!)\">\n {{ candidate.title || candidate.fileName || candidate.id }}\n @if (candidate.creationTime) {\n <span class=\"text-muted\">· {{ candidate.creationTime | date:'yyyy-MM-dd' }}</span>\n }\n </button>\n }\n </div>\n }\n </div>\n }\n </dd>\n }\n\n @if (document()!.rejectionReason) {\n <dt class=\"col-sm-5 text-muted\">{{ '::Document:RejectionReason' | abpLocalization }}</dt>\n <dd class=\"col-sm-7 text-muted fst-italic small\">{{ document()!.rejectionReason }}</dd>\n }\n\n <dt class=\"col-sm-5 text-muted\">{{ '::Document:UploadedBy' | abpLocalization }}</dt>\n <dd class=\"col-sm-7\">{{ document()!.fileOrigin?.uploadedByUserName }}</dd>\n\n <dt class=\"col-sm-5 text-muted\">{{ '::Document:UploadedAt' | abpLocalization }}</dt>\n <dd class=\"col-sm-7\">{{ document()!.creationTime | date:'yyyy-MM-dd HH:mm' }}</dd>\n </dl>\n </div>\n </div>\n\n @if (showFieldsCard()) {\n <div class=\"card shadow-sm\" id=\"extracted-fields-card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0\">\n <i class=\"fas fa-table-list me-2\"></i>\n {{ '::Document:ExtractedFields' | abpLocalization }}\n </h6>\n @if (canEditFields && fieldDefinitions().length > 0 && !isEditingFields()) {\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"startEditFields()\">\n <i class=\"fas fa-pen me-1\"></i>{{ '::Edit' | abpLocalization }}\n </button>\n }\n </div>\n <div class=\"card-body\">\n @if (!isEditingFields()) {\n @if (extractedFieldEntries().length > 0) {\n <dl class=\"row mb-0 small\">\n @for (entry of extractedFieldEntries(); track entry.key) {\n <dt class=\"col-sm-5 text-muted text-truncate\" [title]=\"entry.key\">\n {{ entry.label }}\n </dt>\n <dd class=\"col-sm-7 mb-2\">\n @if (entry.isMarkdown) {\n <!-- #418: LongText values render as Markdown, sanitized on bind by Angular's\n DomSanitizer — the same pipeline as the left-column preview. -->\n <div class=\"markdown-body\" [innerHTML]=\"entry.renderedHtml\"></div>\n } @else {\n <span [title]=\"entry.value\">{{ entry.value }}</span>\n }\n </dd>\n }\n </dl>\n } @else {\n <p class=\"text-muted small mb-0\">{{ '::Document:NoFieldValues' | abpLocalization }}</p>\n }\n } @else {\n <abp-dynamic-form\n [fields]=\"extractedFieldFormFields()\"\n [submitInProgress]=\"isSavingFields()\"\n (onSubmit)=\"saveFields($event)\"\n >\n <div actions class=\"d-flex justify-content-end gap-2\">\n <button\n type=\"button\"\n class=\"btn btn-secondary btn-sm\"\n (click)=\"cancelEditFields()\"\n [disabled]=\"isSavingFields()\"\n >\n {{ '::Cancel' | abpLocalization }}\n </button>\n <button\n type=\"submit\"\n class=\"btn btn-primary btn-sm\"\n [disabled]=\"isSavingFields()\"\n >\n @if (isSavingFields()) {\n <span class=\"spinner-border spinner-border-sm me-1\"></span>\n }\n {{ '::Save' | abpLocalization }}\n </button>\n </div>\n </abp-dynamic-form>\n }\n </div>\n </div>\n }\n\n <div class=\"card shadow-sm\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"fas fa-stream me-2\"></i>\n {{ '::Document:Pipelines' | abpLocalization }}\n </h6>\n </div>\n <ul class=\"list-group list-group-flush small\">\n @for (row of pipelineRows(); track row.pipelineCode) {\n <li class=\"list-group-item\">\n <div class=\"d-flex align-items-center flex-wrap gap-2\">\n <span class=\"fw-semibold\">\n @if (row.isKnown) {\n {{ row.labelKey | abpLocalization }}\n } @else {\n {{ row.pipelineCode }}\n }\n </span>\n <span [class]=\"row.statusBadgeClass\">\n @if (row.inProgress) {\n <span class=\"spinner-border spinner-border-sm me-1\" role=\"status\"></span>\n }\n {{ row.statusLabel | abpLocalization }}\n </span>\n @if (row.run && (row.run.attemptNumber ?? 0) > 1) {\n <span class=\"badge bg-body-tertiary text-muted border\">\n {{ '::Document:Pipeline:Attempt' | abpLocalization }}\n #{{ row.run.attemptNumber }}\n </span>\n }\n @if (row.elapsedDisplay !== null) {\n <span class=\"text-muted ms-auto\">\n <i class=\"fas fa-clock me-1\"></i>{{ row.elapsedDisplay }}\n </span>\n }\n @if (row.retryable) {\n <button\n class=\"btn btn-outline-primary btn-sm\"\n [class.ms-auto]=\"row.elapsedDisplay === null\"\n [disabled]=\"retryingPipeline() !== null\"\n (click)=\"retryPipeline(row.pipelineCode)\">\n @if (retryingPipeline() === row.pipelineCode) {\n <span class=\"spinner-border spinner-border-sm me-1\" role=\"status\"></span>\n {{ '::Document:Pipeline:Retrying' | abpLocalization }}\n } @else {\n <i class=\"fas fa-redo me-1\"></i>\n {{ '::Document:Pipeline:Retry' | abpLocalization }}\n }\n </button>\n }\n </div>\n @if (row.run) {\n <div class=\"text-muted small mt-1\">\n @if (row.run.startedAt) {\n <span>\n <i class=\"fas fa-play me-1\"></i>{{ row.run.startedAt | date:'yyyy-MM-dd HH:mm:ss' }}\n </span>\n }\n @if (row.run.completedAt) {\n <span class=\"ms-3\">\n <i class=\"fas fa-flag-checkered me-1\"></i>{{ row.run.completedAt | date:'yyyy-MM-dd HH:mm:ss' }}\n </span>\n }\n </div>\n }\n @if (row.run?.statusMessage) {\n <div class=\"alert mt-2 mb-0 py-2 small\"\n [class.alert-danger]=\"row.run!.status === PipelineRunStatus.Failed\"\n [class.alert-secondary]=\"row.run!.status !== PipelineRunStatus.Failed\">\n {{ row.run!.statusMessage }}\n </div>\n }\n </li>\n }\n </ul>\n </div>\n </div>\n </div>\n </div>\n }\n</div>\n\n<!-- #395: Manual confirm / assign classification modal (relocated from the removed review queue). -->\n@if (showClassifyDialog()) {\n <div class=\"modal d-block\" tabindex=\"-1\" style=\"background:rgba(0,0,0,.4);\" (click)=\"closeClassifyDialog()\">\n <div class=\"modal-dialog modal-dialog-centered\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <h5 class=\"modal-title\">\n <i class=\"fas fa-user-edit me-2\"></i>\n {{ '::Document:ConfirmClassification' | abpLocalization }}\n </h5>\n <button type=\"button\" class=\"btn-close\" (click)=\"closeClassifyDialog()\"></button>\n </div>\n <div class=\"modal-body\">\n <p class=\"text-muted small mb-3\">\n {{ document()?.title || document()?.fileOrigin?.originalFileName }}\n </p>\n <label class=\"form-label fw-semibold\">{{ '::Document:SelectDocumentType' | abpLocalization }}</label>\n <select\n class=\"form-select\"\n [ngModel]=\"selectedTypeId()\"\n (ngModelChange)=\"selectedTypeId.set($event)\"\n >\n <option value=\"\" disabled>{{ '::Document:SelectDocumentType' | abpLocalization }}</option>\n @for (t of documentTypes(); track t.id) {\n <option [value]=\"t.id\">{{ t.displayName }} ({{ t.typeCode }})</option>\n }\n </select>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"closeClassifyDialog()\">\n {{ '::Cancel' | abpLocalization }}\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n [disabled]=\"!selectedTypeId() || isConfirmingClassification()\"\n (click)=\"submitClassify()\"\n >\n @if (isConfirmingClassification()) {\n <span class=\"spinner-border spinner-border-sm me-1\"></span>\n }\n {{ '::Document:Confirm' | abpLocalization }}\n </button>\n </div>\n </div>\n </div>\n </div>\n}\n\n<!-- #395: Reject modal (relocated from the removed review queue). -->\n@if (showRejectDialog()) {\n <div class=\"modal d-block\" tabindex=\"-1\" style=\"background:rgba(0,0,0,.4);\" (click)=\"closeRejectDialog()\">\n <div class=\"modal-dialog modal-dialog-centered\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <h5 class=\"modal-title\">\n <i class=\"fas fa-ban me-2 text-danger\"></i>\n {{ '::Document:Review:Reject' | abpLocalization }}\n </h5>\n <button type=\"button\" class=\"btn-close\" (click)=\"closeRejectDialog()\"></button>\n </div>\n <div class=\"modal-body\">\n <p class=\"text-muted small mb-3\">\n {{ document()?.title || document()?.fileOrigin?.originalFileName }}\n </p>\n <label class=\"form-label fw-semibold\">\n {{ '::Document:Review:RejectReason' | abpLocalization }} <span class=\"text-danger\">*</span>\n </label>\n <textarea\n class=\"form-control\"\n rows=\"3\"\n maxlength=\"2048\"\n [ngModel]=\"rejectReason()\"\n (ngModelChange)=\"rejectReason.set($event)\"\n placeholder=\"{{ '::Document:Review:RejectReasonHint' | abpLocalization }}\"\n ></textarea>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"closeRejectDialog()\">\n {{ '::Cancel' | abpLocalization }}\n </button>\n <button\n type=\"button\"\n class=\"btn btn-danger\"\n [disabled]=\"isRejecting()\"\n (click)=\"submitReject()\"\n >\n @if (isRejecting()) {\n <span class=\"spinner-border spinner-border-sm me-1\"></span>\n }\n {{ '::Document:Review:Reject' | abpLocalization }}\n </button>\n </div>\n </div>\n </div>\n </div>\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, DestroyRef, signal, computed, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
+ import { ActivatedRoute, Router } from '@angular/router';
5
+ import { CommonModule } from '@angular/common';
6
+ import { LocalizationPipe } from '@abp/ng.core';
7
+ import { DocumentService } from '@dignite/vault-extract';
8
+ import { D as DocumentFileBlobService, i as isImageContentType, a as isPdfContentType } from './dignite-vault-extract-documents-content-type-DjCs-s4E.mjs';
9
+
10
+ // File preview page (route documents/:id/file). Replaces the old detail-page openFile() blob new-tab
11
+ // shortcut with a readable /documents/{id}/file URL that includes the document ID. The file content is
12
+ // still fetched through DocumentService.getBlob with a Bearer token and embedded as a blob, so the token
13
+ // never enters the URL. Blob lifecycle is centralized in DocumentFileBlobService (#277).
14
+ class DocumentFilePreviewComponent {
15
+ constructor() {
16
+ this.route = inject(ActivatedRoute);
17
+ this.router = inject(Router);
18
+ this.documentService = inject(DocumentService);
19
+ this.destroyRef = inject(DestroyRef);
20
+ this.fileBlob = inject(DocumentFileBlobService);
21
+ this.document = signal(null, ...(ngDevMode ? [{ debugName: "document" }] : /* istanbul ignore next */ []));
22
+ this.isLoading = signal(true, ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
23
+ this.hasError = signal(false, ...(ngDevMode ? [{ debugName: "hasError" }] : /* istanbul ignore next */ []));
24
+ this.fileName = computed(() => this.document()?.fileOrigin?.originalFileName || this.document()?.title || '', ...(ngDevMode ? [{ debugName: "fileName" }] : /* istanbul ignore next */ []));
25
+ this.contentType = computed(() => this.document()?.fileOrigin?.contentType ?? '', ...(ngDevMode ? [{ debugName: "contentType" }] : /* istanbul ignore next */ []));
26
+ this.isImage = computed(() => isImageContentType(this.contentType()), ...(ngDevMode ? [{ debugName: "isImage" }] : /* istanbul ignore next */ []));
27
+ this.isPdf = computed(() => isPdfContentType(this.contentType()), ...(ngDevMode ? [{ debugName: "isPdf" }] : /* istanbul ignore next */ []));
28
+ // Sub-documents (#306/#346) carry no FileOrigin — there is no original file to fetch. Gate the blob fetch
29
+ // and the viewer on this so the page never calls GetBlobAsync for a blob-less document
30
+ // (Extract:DocumentNoSourceBlob); the template shows a neutral notice instead.
31
+ this.hasSourceFile = computed(() => !!this.document()?.fileOrigin, ...(ngDevMode ? [{ debugName: "hasSourceFile" }] : /* istanbul ignore next */ []));
32
+ }
33
+ ngOnInit() {
34
+ this.documentId = this.route.snapshot.paramMap.get('id');
35
+ this.load();
36
+ }
37
+ load() {
38
+ this.isLoading.set(true);
39
+ this.hasError.set(false);
40
+ // Fetch metadata first to get contentType and filename for rendering decisions, then let the service
41
+ // fetch the blob body.
42
+ this.documentService
43
+ .get(this.documentId)
44
+ .pipe(takeUntilDestroyed(this.destroyRef))
45
+ .subscribe({
46
+ next: doc => {
47
+ this.document.set(doc);
48
+ this.isLoading.set(false);
49
+ // A sub-document has no source blob; skip the fetch (would throw Extract:DocumentNoSourceBlob).
50
+ if (doc.fileOrigin) {
51
+ this.fileBlob.ensureLoaded(this.documentId);
52
+ }
53
+ },
54
+ error: () => {
55
+ this.isLoading.set(false);
56
+ this.hasError.set(true);
57
+ },
58
+ });
59
+ }
60
+ back() {
61
+ this.router.navigate(['/documents', this.documentId]);
62
+ }
63
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DocumentFilePreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
64
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: DocumentFilePreviewComponent, isStandalone: true, selector: "lib-document-file-preview", providers: [DocumentFileBlobService], ngImport: i0, template: "<div class=\"container-fluid py-3 d-flex flex-column\" style=\"min-height: calc(100vh - 1rem);\">\n <!-- Header -->\n <div class=\"d-flex align-items-center mb-3 gap-2\">\n <button class=\"btn btn-outline-secondary btn-sm\" (click)=\"back()\">\n <i class=\"fas fa-arrow-left me-1\"></i>\n {{ '::Back' | abpLocalization }}\n </button>\n <h6 class=\"mb-0 ms-2 text-truncate\" style=\"max-width: 60vw;\" [title]=\"fileName()\">\n <i class=\"fas fa-file me-1\"></i>{{ fileName() }}\n </h6>\n @if (fileBlob.blobUrl()) {\n <a\n class=\"btn btn-outline-primary btn-sm ms-auto\"\n [href]=\"fileBlob.blobUrl()\"\n [download]=\"fileName()\"\n target=\"_blank\"\n rel=\"noopener\"\n >\n <i class=\"fas fa-download me-1\"></i>{{ '::Document:Download' | abpLocalization }}\n </a>\n }\n </div>\n\n @if (isLoading() || fileBlob.isLoading()) {\n <div class=\"flex-grow-1 d-flex align-items-center justify-content-center py-5\">\n <div class=\"spinner-border text-primary\" role=\"status\"></div>\n </div>\n } @else if (hasError() || fileBlob.hasError()) {\n <div class=\"alert alert-danger\" role=\"alert\">\n <i class=\"fas fa-exclamation-triangle me-2\"></i>\n {{ '::Document:PreviewUnavailable' | abpLocalization }}\n </div>\n } @else if (!hasSourceFile()) {\n <!-- Sub-document (#306/#346): no source blob, so no file to show. Neutral notice, not an error. -->\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center py-5 text-muted\">\n <i class=\"fas fa-file-circle-xmark fa-3x mb-3 d-block\"></i>\n <p class=\"mb-0\">{{ '::Document:NoSourceFile' | abpLocalization }}</p>\n </div>\n </div>\n } @else if (isImage()) {\n <div class=\"flex-grow-1 d-flex align-items-center justify-content-center bg-body-tertiary rounded p-2\">\n <img\n [src]=\"fileBlob.blobUrl()\"\n [alt]=\"fileName()\"\n class=\"img-fluid\"\n style=\"max-height: 85vh; object-fit: contain;\"\n />\n </div>\n } @else if (isPdf() && fileBlob.safeResourceUrl()) {\n <iframe\n [src]=\"fileBlob.safeResourceUrl()\"\n [title]=\"fileName()\"\n class=\"flex-grow-1 w-100 border rounded\"\n style=\"min-height: 80vh;\"\n ></iframe>\n } @else {\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center py-5 text-muted\">\n <i class=\"fas fa-file-arrow-down fa-3x mb-3 d-block\"></i>\n <p class=\"mb-3\">{{ '::Document:PreviewNotSupported' | abpLocalization }}</p>\n @if (fileBlob.blobUrl()) {\n <a\n class=\"btn btn-primary\"\n [href]=\"fileBlob.blobUrl()\"\n [download]=\"fileName()\"\n target=\"_blank\"\n rel=\"noopener\"\n >\n <i class=\"fas fa-download me-1\"></i>{{ '::Document:Download' | abpLocalization }}\n </a>\n }\n </div>\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "pipe", type: LocalizationPipe, name: "abpLocalization" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
65
+ }
66
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DocumentFilePreviewComponent, decorators: [{
67
+ type: Component,
68
+ args: [{ selector: 'lib-document-file-preview', imports: [CommonModule, LocalizationPipe], providers: [DocumentFileBlobService], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"container-fluid py-3 d-flex flex-column\" style=\"min-height: calc(100vh - 1rem);\">\n <!-- Header -->\n <div class=\"d-flex align-items-center mb-3 gap-2\">\n <button class=\"btn btn-outline-secondary btn-sm\" (click)=\"back()\">\n <i class=\"fas fa-arrow-left me-1\"></i>\n {{ '::Back' | abpLocalization }}\n </button>\n <h6 class=\"mb-0 ms-2 text-truncate\" style=\"max-width: 60vw;\" [title]=\"fileName()\">\n <i class=\"fas fa-file me-1\"></i>{{ fileName() }}\n </h6>\n @if (fileBlob.blobUrl()) {\n <a\n class=\"btn btn-outline-primary btn-sm ms-auto\"\n [href]=\"fileBlob.blobUrl()\"\n [download]=\"fileName()\"\n target=\"_blank\"\n rel=\"noopener\"\n >\n <i class=\"fas fa-download me-1\"></i>{{ '::Document:Download' | abpLocalization }}\n </a>\n }\n </div>\n\n @if (isLoading() || fileBlob.isLoading()) {\n <div class=\"flex-grow-1 d-flex align-items-center justify-content-center py-5\">\n <div class=\"spinner-border text-primary\" role=\"status\"></div>\n </div>\n } @else if (hasError() || fileBlob.hasError()) {\n <div class=\"alert alert-danger\" role=\"alert\">\n <i class=\"fas fa-exclamation-triangle me-2\"></i>\n {{ '::Document:PreviewUnavailable' | abpLocalization }}\n </div>\n } @else if (!hasSourceFile()) {\n <!-- Sub-document (#306/#346): no source blob, so no file to show. Neutral notice, not an error. -->\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center py-5 text-muted\">\n <i class=\"fas fa-file-circle-xmark fa-3x mb-3 d-block\"></i>\n <p class=\"mb-0\">{{ '::Document:NoSourceFile' | abpLocalization }}</p>\n </div>\n </div>\n } @else if (isImage()) {\n <div class=\"flex-grow-1 d-flex align-items-center justify-content-center bg-body-tertiary rounded p-2\">\n <img\n [src]=\"fileBlob.blobUrl()\"\n [alt]=\"fileName()\"\n class=\"img-fluid\"\n style=\"max-height: 85vh; object-fit: contain;\"\n />\n </div>\n } @else if (isPdf() && fileBlob.safeResourceUrl()) {\n <iframe\n [src]=\"fileBlob.safeResourceUrl()\"\n [title]=\"fileName()\"\n class=\"flex-grow-1 w-100 border rounded\"\n style=\"min-height: 80vh;\"\n ></iframe>\n } @else {\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center py-5 text-muted\">\n <i class=\"fas fa-file-arrow-down fa-3x mb-3 d-block\"></i>\n <p class=\"mb-3\">{{ '::Document:PreviewNotSupported' | abpLocalization }}</p>\n @if (fileBlob.blobUrl()) {\n <a\n class=\"btn btn-primary\"\n [href]=\"fileBlob.blobUrl()\"\n [download]=\"fileName()\"\n target=\"_blank\"\n rel=\"noopener\"\n >\n <i class=\"fas fa-download me-1\"></i>{{ '::Document:Download' | abpLocalization }}\n </a>\n }\n </div>\n </div>\n }\n</div>\n" }]
69
+ }] });
70
+
71
+ export { DocumentFilePreviewComponent };
72
+ //# sourceMappingURL=dignite-vault-extract-documents-document-file-preview.component-CStXf8v9.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dignite-vault-extract-documents-document-file-preview.component-CStXf8v9.mjs","sources":["../../../packages/vault-extract/documents/src/lib/documents/document-file-preview/document-file-preview.component.ts","../../../packages/vault-extract/documents/src/lib/documents/document-file-preview/document-file-preview.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n OnInit,\n computed,\n inject,\n signal,\n} from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { CommonModule } from '@angular/common';\nimport { LocalizationPipe } from '@abp/ng.core';\nimport { DocumentDto, DocumentService } from '@dignite/vault-extract';\nimport { DocumentFileBlobService } from '../../shared/document-file-blob.service';\nimport { isImageContentType, isPdfContentType } from '../../shared/content-type';\n\n// File preview page (route documents/:id/file). Replaces the old detail-page openFile() blob new-tab\n// shortcut with a readable /documents/{id}/file URL that includes the document ID. The file content is\n// still fetched through DocumentService.getBlob with a Bearer token and embedded as a blob, so the token\n// never enters the URL. Blob lifecycle is centralized in DocumentFileBlobService (#277).\n@Component({\n selector: 'lib-document-file-preview',\n templateUrl: './document-file-preview.component.html',\n imports: [CommonModule, LocalizationPipe],\n providers: [DocumentFileBlobService],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class DocumentFilePreviewComponent implements OnInit {\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n private readonly documentService = inject(DocumentService);\n private readonly destroyRef = inject(DestroyRef);\n protected readonly fileBlob = inject(DocumentFileBlobService);\n\n document = signal<DocumentDto | null>(null);\n isLoading = signal(true);\n hasError = signal(false);\n\n private documentId!: string;\n\n readonly fileName = computed(\n () => this.document()?.fileOrigin?.originalFileName || this.document()?.title || '',\n );\n readonly contentType = computed(() => this.document()?.fileOrigin?.contentType ?? '');\n readonly isImage = computed(() => isImageContentType(this.contentType()));\n readonly isPdf = computed(() => isPdfContentType(this.contentType()));\n // Sub-documents (#306/#346) carry no FileOrigin — there is no original file to fetch. Gate the blob fetch\n // and the viewer on this so the page never calls GetBlobAsync for a blob-less document\n // (Extract:DocumentNoSourceBlob); the template shows a neutral notice instead.\n readonly hasSourceFile = computed(() => !!this.document()?.fileOrigin);\n\n ngOnInit(): void {\n this.documentId = this.route.snapshot.paramMap.get('id')!;\n this.load();\n }\n\n private load(): void {\n this.isLoading.set(true);\n this.hasError.set(false);\n // Fetch metadata first to get contentType and filename for rendering decisions, then let the service\n // fetch the blob body.\n this.documentService\n .get(this.documentId)\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe({\n next: doc => {\n this.document.set(doc);\n this.isLoading.set(false);\n // A sub-document has no source blob; skip the fetch (would throw Extract:DocumentNoSourceBlob).\n if (doc.fileOrigin) {\n this.fileBlob.ensureLoaded(this.documentId);\n }\n },\n error: () => {\n this.isLoading.set(false);\n this.hasError.set(true);\n },\n });\n }\n\n back(): void {\n this.router.navigate(['/documents', this.documentId]);\n }\n}\n","<div class=\"container-fluid py-3 d-flex flex-column\" style=\"min-height: calc(100vh - 1rem);\">\n <!-- Header -->\n <div class=\"d-flex align-items-center mb-3 gap-2\">\n <button class=\"btn btn-outline-secondary btn-sm\" (click)=\"back()\">\n <i class=\"fas fa-arrow-left me-1\"></i>\n {{ '::Back' | abpLocalization }}\n </button>\n <h6 class=\"mb-0 ms-2 text-truncate\" style=\"max-width: 60vw;\" [title]=\"fileName()\">\n <i class=\"fas fa-file me-1\"></i>{{ fileName() }}\n </h6>\n @if (fileBlob.blobUrl()) {\n <a\n class=\"btn btn-outline-primary btn-sm ms-auto\"\n [href]=\"fileBlob.blobUrl()\"\n [download]=\"fileName()\"\n target=\"_blank\"\n rel=\"noopener\"\n >\n <i class=\"fas fa-download me-1\"></i>{{ '::Document:Download' | abpLocalization }}\n </a>\n }\n </div>\n\n @if (isLoading() || fileBlob.isLoading()) {\n <div class=\"flex-grow-1 d-flex align-items-center justify-content-center py-5\">\n <div class=\"spinner-border text-primary\" role=\"status\"></div>\n </div>\n } @else if (hasError() || fileBlob.hasError()) {\n <div class=\"alert alert-danger\" role=\"alert\">\n <i class=\"fas fa-exclamation-triangle me-2\"></i>\n {{ '::Document:PreviewUnavailable' | abpLocalization }}\n </div>\n } @else if (!hasSourceFile()) {\n <!-- Sub-document (#306/#346): no source blob, so no file to show. Neutral notice, not an error. -->\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center py-5 text-muted\">\n <i class=\"fas fa-file-circle-xmark fa-3x mb-3 d-block\"></i>\n <p class=\"mb-0\">{{ '::Document:NoSourceFile' | abpLocalization }}</p>\n </div>\n </div>\n } @else if (isImage()) {\n <div class=\"flex-grow-1 d-flex align-items-center justify-content-center bg-body-tertiary rounded p-2\">\n <img\n [src]=\"fileBlob.blobUrl()\"\n [alt]=\"fileName()\"\n class=\"img-fluid\"\n style=\"max-height: 85vh; object-fit: contain;\"\n />\n </div>\n } @else if (isPdf() && fileBlob.safeResourceUrl()) {\n <iframe\n [src]=\"fileBlob.safeResourceUrl()\"\n [title]=\"fileName()\"\n class=\"flex-grow-1 w-100 border rounded\"\n style=\"min-height: 80vh;\"\n ></iframe>\n } @else {\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center py-5 text-muted\">\n <i class=\"fas fa-file-arrow-down fa-3x mb-3 d-block\"></i>\n <p class=\"mb-3\">{{ '::Document:PreviewNotSupported' | abpLocalization }}</p>\n @if (fileBlob.blobUrl()) {\n <a\n class=\"btn btn-primary\"\n [href]=\"fileBlob.blobUrl()\"\n [download]=\"fileName()\"\n target=\"_blank\"\n rel=\"noopener\"\n >\n <i class=\"fas fa-download me-1\"></i>{{ '::Document:Download' | abpLocalization }}\n </a>\n }\n </div>\n </div>\n }\n</div>\n"],"names":[],"mappings":"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"}