@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,642 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, DestroyRef, LOCALE_ID, signal, computed, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
+ import { formatDate, CommonModule } from '@angular/common';
5
+ import { Router, ActivatedRoute } from '@angular/router';
6
+ import * as i1 from '@angular/forms';
7
+ import { FormsModule } from '@angular/forms';
8
+ import { PermissionService, ListService, LocalizationService, escapeHtmlChars, LocalizationPipe } from '@abp/ng.core';
9
+ import { ExtensionsService, EntityProp, ExtensibleTableComponent, EXTENSIONS_IDENTIFIER } from '@abp/ng.components/extensible';
10
+ import { ConfirmationService, ToasterService, Confirmation } from '@abp/ng.theme.shared';
11
+ import * as i2 from '@ng-bootstrap/ng-bootstrap';
12
+ import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
13
+ import { of } from 'rxjs';
14
+ import { DocumentService, DocumentStatisticsService, DocumentTypeService, FieldDefinitionService, CabinetService, EXTRACT_PERMISSIONS, DocumentLifecycleStatus, DocumentReviewReasons } from '@dignite/vault-extract';
15
+ import { c as configureEntityTable, E as EXTRACT_TABLES } from './dignite-vault-extract-documents-extensible-table-DkLXuoWo.mjs';
16
+ import { f as formatExtractedFieldValue } from './dignite-vault-extract-documents-format-field-value-Xjb8lwzA.mjs';
17
+
18
+ class DocumentListComponent {
19
+ constructor() {
20
+ this.documentService = inject(DocumentService);
21
+ this.statisticsService = inject(DocumentStatisticsService);
22
+ this.documentTypeService = inject(DocumentTypeService);
23
+ this.fieldDefinitionService = inject(FieldDefinitionService);
24
+ this.cabinetService = inject(CabinetService);
25
+ this.router = inject(Router);
26
+ this.route = inject(ActivatedRoute);
27
+ this.confirmation = inject(ConfirmationService);
28
+ this.toaster = inject(ToasterService);
29
+ this.permissionService = inject(PermissionService);
30
+ this.destroyRef = inject(DestroyRef);
31
+ this.extensions = inject(ExtensionsService);
32
+ this.locale = inject(LOCALE_ID);
33
+ this.list = inject(ListService);
34
+ this.canDelete = this.permissionService.getGrantedPolicy(EXTRACT_PERMISSIONS.Documents.Delete);
35
+ this.canConfirm = this.permissionService.getGrantedPolicy(EXTRACT_PERMISSIONS.Documents.ConfirmClassification);
36
+ this.canUpload = this.permissionService.getGrantedPolicy(EXTRACT_PERMISSIONS.Documents.Upload);
37
+ this.canViewCabinets = this.permissionService.getGrantedPolicy(EXTRACT_PERMISSIONS.Cabinets.Default);
38
+ this.hasDocumentActions = this.canConfirm || this.canDelete;
39
+ this.documents = signal({ totalCount: 0, items: [] }, ...(ngDevMode ? [{ debugName: "documents" }] : /* istanbul ignore next */ []));
40
+ this.isLoading = signal(true, ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
41
+ // Bumped whenever the dynamic columns change. ExtensibleTableComponent snapshots its
42
+ // column list at construction, so the template keys the table on this value (@for …
43
+ // track key) to force a fresh instance — deterministic, no setTimeout/flicker.
44
+ this.tableKey = signal(0, ...(ngDevMode ? [{ debugName: "tableKey" }] : /* istanbul ignore next */ []));
45
+ this.typeFilter = signal('', ...(ngDevMode ? [{ debugName: "typeFilter" }] : /* istanbul ignore next */ []));
46
+ this.cabinetFilter = signal('', ...(ngDevMode ? [{ debugName: "cabinetFilter" }] : /* istanbul ignore next */ []));
47
+ this.lifecycleFilter = signal(undefined, ...(ngDevMode ? [{ debugName: "lifecycleFilter" }] : /* istanbul ignore next */ []));
48
+ // #395: needs-review filter. Replaces the standalone review-queue page — when on, the list shows only
49
+ // documents that still require operator attention (hasReviewReasons, the queue's RequiresAttention
50
+ // predicate). Seeded from the ?review=1 deep-link used by the overview needs-review entry points.
51
+ this.reviewFilter = signal(false, ...(ngDevMode ? [{ debugName: "reviewFilter" }] : /* istanbul ignore next */ []));
52
+ // #354: when set, the list shows only the sub-documents derived from this source document (a container's
53
+ // children). subDocumentsParent is the container itself (for the indicator banner); it is null when the filter
54
+ // was seeded from a deep-link query param and the parent row is not in hand.
55
+ this.originDocumentIdFilter = signal('', ...(ngDevMode ? [{ debugName: "originDocumentIdFilter" }] : /* istanbul ignore next */ []));
56
+ this.subDocumentsParent = signal(null, ...(ngDevMode ? [{ debugName: "subDocumentsParent" }] : /* istanbul ignore next */ []));
57
+ this.confirmingDoc = signal(null, ...(ngDevMode ? [{ debugName: "confirmingDoc" }] : /* istanbul ignore next */ []));
58
+ this.documentTypes = signal([], ...(ngDevMode ? [{ debugName: "documentTypes" }] : /* istanbul ignore next */ []));
59
+ this.cabinets = signal([], ...(ngDevMode ? [{ debugName: "cabinets" }] : /* istanbul ignore next */ []));
60
+ // Dynamic ExtractedFields columns — populated only while a single documentTypeCode
61
+ // filter is active (then the page shares one field schema). Empty for no-type /
62
+ // mixed-type views, so the columns disappear. Driven off the type's field
63
+ // definitions (not the union of extractedFields keys) so headers stay stable and
64
+ // friendly even for fields no document in the page happened to fill.
65
+ this.extractedFieldColumns = signal([], ...(ngDevMode ? [{ debugName: "extractedFieldColumns" }] : /* istanbul ignore next */ []));
66
+ this.selectedTypeId = signal('', ...(ngDevMode ? [{ debugName: "selectedTypeId" }] : /* istanbul ignore next */ []));
67
+ this.isConfirming = signal(false, ...(ngDevMode ? [{ debugName: "isConfirming" }] : /* istanbul ignore next */ []));
68
+ // #284 review-queue gateway: the toolbar badge shows the canonical needs-review total for the current
69
+ // layer (DocumentStatisticsDto.NeedsReviewCount — same RequiresAttention predicate the review queue runs,
70
+ // #333), not a page-local count, so it stays correct across pagination and once the list is unfiltered.
71
+ this.reviewQueueCount = signal(0, ...(ngDevMode ? [{ debugName: "reviewQueueCount" }] : /* istanbul ignore next */ []));
72
+ // #354: render the row actions column when the user has confirm/delete actions OR any row is a container
73
+ // (exposes "view sub-documents") OR any row is a sub-document (exposes "view parent / view siblings") —
74
+ // these provenance actions are available regardless of confirm/delete permissions.
75
+ this.showActionsColumn = computed(() => this.hasDocumentActions || this.documents().items.some(d => d.isContainer || d.originDocumentId), ...(ngDevMode ? [{ debugName: "showActionsColumn" }] : /* istanbul ignore next */ []));
76
+ this.DocumentLifecycleStatus = DocumentLifecycleStatus;
77
+ this.DocumentReviewReasons = DocumentReviewReasons;
78
+ this.rebuildTableProps([]);
79
+ }
80
+ ngOnInit() {
81
+ // Seed filters from query params first so the overview cards (#335) can deep-link into a
82
+ // cabinet- or type-filtered list before the initial load runs.
83
+ this.applyQueryParamFilters();
84
+ // Seed page + sorting into the ListService BEFORE hookToQuery so the initial fetch uses them (the
85
+ // query$ pipe debounces, so these synchronous seeds coalesce with the constructor's default into a
86
+ // single request). This is what makes Back restore the exact page/sort the operator left, not just the
87
+ // filters.
88
+ this.applyQueryParamPaging();
89
+ this.hookListQuery();
90
+ // Review-queue badge total — only fetched/shown for operators who can open the queue (#284).
91
+ this.loadReviewQueueCount();
92
+ // Document types drive the type filter, the dynamic extracted-field columns, and
93
+ // the confirm-classification picker. Every Documents.Default user needs them, and
94
+ // the read is now decoupled from schema-admin permission (#223 — GetVisible no longer
95
+ // requires DocumentTypes.Default), so load unconditionally; the error fallback keeps
96
+ // the list usable if it ever 403s.
97
+ this.loadDocumentTypes();
98
+ // Cabinet getList is gated by Cabinets.Default; only fetch when granted to
99
+ // avoid a 403 for users without cabinet access (cabinet filter/labels hidden).
100
+ if (this.canViewCabinets) {
101
+ this.loadCabinets();
102
+ }
103
+ }
104
+ refresh() {
105
+ this.list.getWithoutPageReset();
106
+ }
107
+ // Overview deep-links (#335) carry cabinetId / documentTypeCode in the URL, and every filter change now
108
+ // writes the active filter set back via writeFiltersToUrl(). Seed the matching filter signals once on
109
+ // load; the top dropdowns are bound to the same signals so they reflect the applied value, and
110
+ // loadDocumentTypes() picks up a seeded typeFilter to load its field columns. Because the filters live in
111
+ // the URL, opening a document and navigating Back restores the exact filtered view instead of resetting
112
+ // to the unfiltered default.
113
+ applyQueryParamFilters() {
114
+ const params = this.route.snapshot.queryParamMap;
115
+ const cabinetId = params.get('cabinetId');
116
+ if (cabinetId) {
117
+ this.cabinetFilter.set(cabinetId);
118
+ }
119
+ const typeCode = params.get('documentTypeCode');
120
+ if (typeCode) {
121
+ this.typeFilter.set(typeCode);
122
+ }
123
+ const lifecycleStatus = params.get('lifecycleStatus');
124
+ if (lifecycleStatus) {
125
+ const parsed = Number(lifecycleStatus);
126
+ if (!Number.isNaN(parsed)) {
127
+ this.lifecycleFilter.set(parsed);
128
+ }
129
+ }
130
+ // #354: deep-link into a container's sub-documents (the parent row may not be loaded, so the banner falls
131
+ // back to showing the id until/unless the operator navigated via the in-list "view sub-documents" action).
132
+ const originDocumentId = params.get('originDocumentId');
133
+ if (originDocumentId) {
134
+ this.originDocumentIdFilter.set(originDocumentId);
135
+ }
136
+ // #395: overview needs-review entry points deep-link here with ?review=1.
137
+ if (params.get('review')) {
138
+ this.reviewFilter.set(true);
139
+ }
140
+ }
141
+ // Seed the ListService's page + sorting from the URL. ABP's ListService has no built-in URL binding, but
142
+ // page / sortKey / sortOrder are public read/write, so writing them here makes the first fetch (and the
143
+ // restored view after Back) start on the right page and ordering. The header sort arrow is owned by the
144
+ // table and not re-seeded, so only the data ordering is restored — acceptable, and the only sortable
145
+ // column is the default creationTime.
146
+ applyQueryParamPaging() {
147
+ const params = this.route.snapshot.queryParamMap;
148
+ const page = Number(params.get('page'));
149
+ if (Number.isInteger(page) && page > 0) {
150
+ this.list.page = page;
151
+ }
152
+ const sorting = params.get('sorting');
153
+ if (sorting) {
154
+ const [key, order] = sorting.split(' ');
155
+ if (key && (order === 'asc' || order === 'desc')) {
156
+ this.list.sortKey = key;
157
+ this.list.sortOrder = order;
158
+ }
159
+ }
160
+ }
161
+ onLifecycleFilterChange(value) {
162
+ this.lifecycleFilter.set(value);
163
+ this.refreshListFromFirstPage();
164
+ }
165
+ onTypeFilterChange(value) {
166
+ this.typeFilter.set(value);
167
+ this.updateExtractedFieldColumns([]);
168
+ if (value) {
169
+ this.loadExtractedFieldColumns(value);
170
+ }
171
+ this.refreshListFromFirstPage();
172
+ }
173
+ onCabinetFilterChange(value) {
174
+ this.cabinetFilter.set(value);
175
+ this.refreshListFromFirstPage();
176
+ }
177
+ // #395: toggle the needs-review filter (the former review-queue gateway). Refreshes from page 1 so the
178
+ // filtered count and pagination stay consistent.
179
+ toggleReviewFilter() {
180
+ this.reviewFilter.update(on => !on);
181
+ this.refreshListFromFirstPage();
182
+ }
183
+ // #354: focus the list on a container's sub-documents (those whose OriginDocumentId is this container).
184
+ viewSubDocuments(doc, event) {
185
+ event?.stopPropagation();
186
+ if (!doc.id) {
187
+ return;
188
+ }
189
+ this.subDocumentsParent.set(doc);
190
+ this.originDocumentIdFilter.set(doc.id);
191
+ this.refreshListFromFirstPage();
192
+ }
193
+ clearSubDocumentsFilter() {
194
+ this.subDocumentsParent.set(null);
195
+ this.originDocumentIdFilter.set('');
196
+ this.refreshListFromFirstPage();
197
+ }
198
+ // #354: from a sub-document row, open its source (container) document.
199
+ openParentDocument(doc, event) {
200
+ event?.stopPropagation();
201
+ if (!doc.originDocumentId) {
202
+ return;
203
+ }
204
+ this.router.navigate(['/documents', doc.originDocumentId]);
205
+ }
206
+ // #354: from a sub-document row, focus the list on its siblings (all sub-documents of the same source,
207
+ // including this one). The parent row may not be in hand, so the banner falls back to showing the id.
208
+ viewSiblingDocuments(doc, event) {
209
+ event?.stopPropagation();
210
+ if (!doc.originDocumentId) {
211
+ return;
212
+ }
213
+ this.subDocumentsParent.set(null);
214
+ this.originDocumentIdFilter.set(doc.originDocumentId);
215
+ this.refreshListFromFirstPage();
216
+ }
217
+ hookListQuery() {
218
+ this.list.requestStatus$
219
+ .pipe(takeUntilDestroyed(this.destroyRef))
220
+ .subscribe(status => {
221
+ if (status === 'idle' && this.isLoading() && this.documents().items.length === 0)
222
+ return;
223
+ this.isLoading.set(status === 'loading');
224
+ });
225
+ // query$ is the single funnel for every page / sort / filter change (the table writes page+sort here
226
+ // directly; our filter handlers reach it via refreshListFromFirstPage). Persist the full view state to
227
+ // the URL on each emit so table-driven pagination and sorting survive a round-trip to a document and
228
+ // Back, alongside the filters. (It is debounced inside ListService, so this does not fire per keystroke.)
229
+ this.list.query$
230
+ .pipe(takeUntilDestroyed(this.destroyRef))
231
+ .subscribe(() => this.writeStateToUrl());
232
+ this.list
233
+ .hookToQuery(query => this.documentService.getList({
234
+ ...this.buildFilter(),
235
+ maxResultCount: query.maxResultCount,
236
+ skipCount: query.skipCount,
237
+ sorting: query.sorting || 'creationTime desc',
238
+ }))
239
+ .pipe(takeUntilDestroyed(this.destroyRef))
240
+ .subscribe(result => {
241
+ this.documents.set({
242
+ totalCount: result.totalCount ?? 0,
243
+ items: result.items ?? [],
244
+ });
245
+ });
246
+ }
247
+ refreshListFromFirstPage() {
248
+ // Reset to page 1 first (the setter triggers the refetch), then persist — so the URL records page 0,
249
+ // not the page the operator was on before changing the filter. The debounced query$ subscription will
250
+ // also fire and re-persist the same state; writing here too keeps the URL correct synchronously, so a
251
+ // filter-then-immediately-open-a-document sequence still records the new filters before navigating away.
252
+ if (this.list.page === 0) {
253
+ this.list.get();
254
+ }
255
+ else {
256
+ this.list.page = 0;
257
+ }
258
+ this.writeStateToUrl();
259
+ }
260
+ // Mirror the active filters AND the ListService's page/sorting into the URL query string. replaceUrl keeps
261
+ // rapid changes from piling up Back-button steps (each replaces the current /documents/list entry rather
262
+ // than pushing a new one); null values are dropped by the merge handling, so cleared filters and the
263
+ // default page/sort leave a clean URL. applyQueryParamFilters + applyQueryParamPaging re-seed from here on
264
+ // Back, restoring the exact view.
265
+ writeStateToUrl() {
266
+ const sorting = this.list.sortOrder
267
+ ? `${this.list.sortKey} ${this.list.sortOrder}`
268
+ : null;
269
+ this.router.navigate([], {
270
+ relativeTo: this.route,
271
+ queryParams: {
272
+ lifecycleStatus: this.lifecycleFilter() ?? null,
273
+ documentTypeCode: this.typeFilter() || null,
274
+ cabinetId: this.cabinetFilter() || null,
275
+ originDocumentId: this.originDocumentIdFilter() || null,
276
+ review: this.reviewFilter() ? 1 : null,
277
+ page: this.list.page > 0 ? this.list.page : null,
278
+ sorting,
279
+ },
280
+ queryParamsHandling: 'merge',
281
+ replaceUrl: true,
282
+ });
283
+ }
284
+ buildFilter() {
285
+ return {
286
+ documentTypeCode: this.typeFilter() || undefined,
287
+ cabinetId: this.cabinetFilter() || undefined,
288
+ originDocumentId: this.originDocumentIdFilter() || undefined,
289
+ lifecycleStatus: this.lifecycleFilter(),
290
+ // #395: same RequiresAttention predicate the old review queue ran.
291
+ hasReviewReasons: this.reviewFilter() || undefined,
292
+ };
293
+ }
294
+ rebuildTableProps(fields = this.extractedFieldColumns()) {
295
+ configureEntityTable(this.extensions, EXTRACT_TABLES.Documents, this.createTableProps(fields));
296
+ // Force a fresh ExtensibleTableComponent so it re-reads the just-configured columns
297
+ // (it snapshots its column list at construction). The @for key swap recreates it
298
+ // synchronously within the same change-detection pass.
299
+ this.tableKey.update(v => v + 1);
300
+ }
301
+ createTableProps(fields) {
302
+ return [
303
+ EntityProp.create({
304
+ type: "string" /* ePropType.String */,
305
+ name: 'fileName',
306
+ displayName: '::Document:FileName',
307
+ sortable: false,
308
+ columnWidth: 340,
309
+ valueResolver: data => {
310
+ const doc = data.record;
311
+ const localization = data.getInjected(LocalizationService);
312
+ const fileName = doc.title || doc.fileOrigin?.originalFileName || '-';
313
+ const iconClass = this.isImage(doc)
314
+ ? 'fas fa-file-image fa-lg text-primary'
315
+ : 'fas fa-file-pdf fa-lg text-danger';
316
+ // #350: a container is a bundle of sub-documents and is not itself a business record. Flag it
317
+ // with a badge so operators don't mistake it for a normal document. isContainer is a
318
+ // system-controlled signal carried on the list DTO. #354: the row's "view sub-documents" action
319
+ // (containers only) drills into its children via the originDocumentId filter.
320
+ const bundleBadge = doc.isContainer
321
+ ? ` <span class="badge bg-dark">${escapeHtmlChars(localization.instant('::Document:Bundle'))}</span>`
322
+ : '';
323
+ // #354: mirror of the container Bundle badge on the child side — a sub-document carries
324
+ // originDocumentId (its source) and is flagged so operators can tell it apart from a
325
+ // normally-uploaded document; the row's "view parent / view siblings" actions drill the relationship.
326
+ const subDocBadge = doc.originDocumentId
327
+ ? ` <span class="badge bg-secondary">${escapeHtmlChars(localization.instant('::Document:SubDocument'))}</span>`
328
+ : '';
329
+ return of(`<span class="document-file-cell"><i class="${iconClass} me-2"></i><span class="fw-semibold text-truncate">${escapeHtmlChars(fileName)}</span>${bundleBadge}${subDocBadge}</span>`);
330
+ },
331
+ }),
332
+ EntityProp.create({
333
+ type: "string" /* ePropType.String */,
334
+ name: 'documentType',
335
+ displayName: '::Document:Type',
336
+ sortable: false,
337
+ columnWidth: 180,
338
+ valueResolver: data => {
339
+ const typeName = this.documentTypeDisplayName(data.record.documentTypeCode);
340
+ return of(typeName
341
+ ? `<span class="badge bg-info text-dark">${escapeHtmlChars(typeName)}</span>`
342
+ : '<span class="text-muted">-</span>');
343
+ },
344
+ }),
345
+ ...fields.map(field => this.createExtractedFieldProp(field)),
346
+ EntityProp.create({
347
+ type: "string" /* ePropType.String */,
348
+ name: 'status',
349
+ displayName: '::Document:Status',
350
+ sortable: false,
351
+ columnWidth: 190,
352
+ valueResolver: data => {
353
+ const localization = data.getInjected(LocalizationService);
354
+ const doc = data.record;
355
+ const spinner = this.isProcessingDocument(doc)
356
+ ? '<span class="spinner-border spinner-border-sm me-1" role="status"></span>'
357
+ : '';
358
+ // #284: two badges may stack: lifecycle on the availability axis plus conditional review badge
359
+ // on the review axis. They do not overwrite each other.
360
+ const lifecycle = `<span class="${this.getStatusBadgeClass(doc.lifecycleStatus)}">${spinner}${escapeHtmlChars(localization.instant(this.getStatusLabel(doc.lifecycleStatus)))}</span>`;
361
+ const review = doc.requiresReview
362
+ ? ` <span class="badge bg-warning text-dark">${escapeHtmlChars(localization.instant(this.reviewBadgeLabel(doc)))}</span>`
363
+ : '';
364
+ return of(lifecycle + review);
365
+ },
366
+ }),
367
+ EntityProp.create({
368
+ type: "string" /* ePropType.String */,
369
+ name: 'creationTime',
370
+ displayName: '::Document:UploadedAt',
371
+ sortable: true,
372
+ columnWidth: 180,
373
+ valueResolver: data => of(`<span class="text-muted small">${escapeHtmlChars(this.formatCreationTime(data.record.creationTime))}</span>`),
374
+ }),
375
+ ];
376
+ }
377
+ createExtractedFieldProp(field) {
378
+ const fieldName = field.name ?? '';
379
+ const propName = `extracted_${field.id || fieldName}`.replace(/[^A-Za-z0-9_]/g, '_');
380
+ return EntityProp.create({
381
+ type: "string" /* ePropType.String */,
382
+ name: propName,
383
+ displayName: field.displayName || field.name || '',
384
+ sortable: false,
385
+ columnWidth: 220,
386
+ valueResolver: data => {
387
+ const text = formatExtractedFieldValue(data.record.extractedFields?.[fieldName]);
388
+ const value = escapeHtmlChars(text);
389
+ return of(`<span class="document-field-cell" title="${value}">${value}</span>`);
390
+ },
391
+ });
392
+ }
393
+ formatCreationTime(value) {
394
+ if (!value)
395
+ return '-';
396
+ try {
397
+ return formatDate(value, 'yyyy-MM-dd HH:mm', this.locale);
398
+ }
399
+ catch {
400
+ return value;
401
+ }
402
+ }
403
+ // Visible document types for the current layer (Host admin → Host types;
404
+ // tenant admin → that tenant's types). Drives the confirm-classification picker.
405
+ loadDocumentTypes() {
406
+ this.documentTypeService.getVisible()
407
+ .pipe(takeUntilDestroyed(this.destroyRef))
408
+ .subscribe({
409
+ next: types => {
410
+ this.documentTypes.set(types);
411
+ if (this.typeFilter()) {
412
+ this.loadExtractedFieldColumns(this.typeFilter());
413
+ return;
414
+ }
415
+ this.rebuildTableProps([]);
416
+ },
417
+ error: () => {
418
+ this.documentTypes.set([]);
419
+ this.updateExtractedFieldColumns([]);
420
+ },
421
+ });
422
+ }
423
+ // Visible cabinets for the current layer — drives the cabinet filter and the
424
+ // cabinet-name label column (list DTO carries only cabinetId; we map id → name).
425
+ loadCabinets() {
426
+ this.cabinetService.getList()
427
+ .pipe(takeUntilDestroyed(this.destroyRef))
428
+ .subscribe({
429
+ next: list => this.cabinets.set(list),
430
+ error: () => this.cabinets.set([]),
431
+ });
432
+ }
433
+ // The list carries only documentTypeCode as the output contract. Map it to the displayName of a type
434
+ // visible in the current layer; fall back to code when cross-layer or deleted types cannot be resolved.
435
+ // cabinets() remains available for the top filter dropdown.
436
+ documentTypeDisplayName(code) {
437
+ if (!code)
438
+ return null;
439
+ return this.documentTypes().find(t => t.typeCode === code)?.displayName ?? code;
440
+ }
441
+ // Load the selected type's field definitions and turn them into dynamic columns
442
+ // (ordered by displayOrder). Cleared when no single type is selected. Errors fall
443
+ // back to no columns rather than breaking the list (mirrors loadDocumentTypes).
444
+ // The type filter is keyed by typeCode (Document exit contract); the field-definition
445
+ // API is keyed by immutable DocumentTypeId (#207), so we resolve code → id via the
446
+ // already-loaded visible types before querying.
447
+ loadExtractedFieldColumns(typeCode) {
448
+ const documentTypeId = this.documentTypes().find(t => t.typeCode === typeCode)?.id;
449
+ if (!documentTypeId) {
450
+ if (this.typeFilter() === typeCode) {
451
+ this.updateExtractedFieldColumns([]);
452
+ }
453
+ return;
454
+ }
455
+ this.fieldDefinitionService.getList({ documentTypeId })
456
+ .pipe(takeUntilDestroyed(this.destroyRef))
457
+ .subscribe({
458
+ next: fields => {
459
+ if (this.typeFilter() !== typeCode)
460
+ return;
461
+ this.updateExtractedFieldColumns([...fields].sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0)));
462
+ },
463
+ error: () => {
464
+ if (this.typeFilter() !== typeCode)
465
+ return;
466
+ this.updateExtractedFieldColumns([]);
467
+ },
468
+ });
469
+ }
470
+ updateExtractedFieldColumns(fields) {
471
+ this.extractedFieldColumns.set(fields);
472
+ this.rebuildTableProps(fields);
473
+ }
474
+ onTableActivate(event) {
475
+ if (event.type !== 'click' || !event.row)
476
+ return;
477
+ this.openDetail(event.row);
478
+ }
479
+ openDetail(doc) {
480
+ this.router.navigate(['/documents', doc.id]);
481
+ }
482
+ uploadNew() {
483
+ this.router.navigate(['/documents']);
484
+ }
485
+ delete(doc, event) {
486
+ event.stopPropagation();
487
+ this.confirmation
488
+ .warn('::Document:AreYouSureToDelete', '::AreYouSure')
489
+ .pipe(takeUntilDestroyed(this.destroyRef))
490
+ .subscribe(status => {
491
+ if (status === Confirmation.Status.confirm) {
492
+ this.documentService.delete(doc.id)
493
+ .pipe(takeUntilDestroyed(this.destroyRef))
494
+ .subscribe({
495
+ next: () => {
496
+ this.toaster.success('::Document:DeletedSuccessfully', '::Success');
497
+ this.list.getWithoutPageReset();
498
+ // A deleted document leaves the (soft-delete-filtered) review queue — refresh the badge.
499
+ this.loadReviewQueueCount();
500
+ },
501
+ error: () => this.toaster.error('::Document:DeleteFailed', '::Error'),
502
+ });
503
+ }
504
+ });
505
+ }
506
+ // Canonical needs-review total for the toolbar badge. Gated on canConfirm so non-reviewers (who don't
507
+ // see the gateway button) never fire the call. The statistics endpoint shares the review queue's
508
+ // RequiresAttention predicate (#333), so the badge and the queue never drift.
509
+ loadReviewQueueCount() {
510
+ if (!this.canConfirm)
511
+ return;
512
+ this.statisticsService.get()
513
+ .pipe(takeUntilDestroyed(this.destroyRef))
514
+ .subscribe({
515
+ next: stats => this.reviewQueueCount.set(stats.needsReviewCount ?? 0),
516
+ error: () => this.reviewQueueCount.set(0),
517
+ });
518
+ }
519
+ // #284: show the confirm-classification button only when the document still requires attention
520
+ // (requiresReview, with disposition already considered server-side so rejected documents no longer
521
+ // require attention) and classification is unresolved. Missing required fields are completed on the
522
+ // detail page.
523
+ needsConfirmation(doc) {
524
+ return doc.requiresReview === true &&
525
+ ((doc.reviewReasons ?? DocumentReviewReasons.None) & DocumentReviewReasons.UnresolvedClassification)
526
+ !== DocumentReviewReasons.None;
527
+ }
528
+ // #284: pure availability axis. Removed the old review-mixed judgment; after the two axes became
529
+ // orthogonal, the two badges render independently and are no longer mutually exclusive.
530
+ isProcessingDocument(doc) {
531
+ return doc.lifecycleStatus === DocumentLifecycleStatus.Processing ||
532
+ doc.lifecycleStatus === DocumentLifecycleStatus.Uploaded;
533
+ }
534
+ openConfirmDialog(doc, event) {
535
+ event.stopPropagation();
536
+ this.confirmingDoc.set(doc);
537
+ // Pre-select the document's current (low-confidence) classification when present,
538
+ // so the operator usually just confirms; otherwise force an explicit choice. The
539
+ // confirm command is keyed by immutable DocumentTypeId (#207), so resolve the
540
+ // document's exit-contract typeCode → id via the already-loaded visible types.
541
+ this.selectedTypeId.set(this.documentTypes().find(t => t.typeCode === doc.documentTypeCode)?.id ?? '');
542
+ }
543
+ closeConfirmDialog() {
544
+ this.confirmingDoc.set(null);
545
+ this.selectedTypeId.set('');
546
+ }
547
+ submitConfirmation() {
548
+ const doc = this.confirmingDoc();
549
+ if (!doc || !this.selectedTypeId())
550
+ return;
551
+ this.isConfirming.set(true);
552
+ this.documentService.confirmClassification(doc.id, { documentTypeId: this.selectedTypeId() })
553
+ .pipe(takeUntilDestroyed(this.destroyRef))
554
+ .subscribe({
555
+ next: () => {
556
+ this.isConfirming.set(false);
557
+ this.closeConfirmDialog();
558
+ this.toaster.success('::Document:ClassificationConfirmed', '::Success');
559
+ this.list.getWithoutPageReset();
560
+ // Confirming a classification clears its review reason — keep the badge in step with the queue.
561
+ this.loadReviewQueueCount();
562
+ },
563
+ error: () => {
564
+ this.isConfirming.set(false);
565
+ this.toaster.error('::Document:ConfirmFailed', '::Error');
566
+ },
567
+ });
568
+ }
569
+ getStatusBadgeClass(status) {
570
+ switch (status) {
571
+ case DocumentLifecycleStatus.Uploaded:
572
+ return 'badge bg-secondary';
573
+ case DocumentLifecycleStatus.Processing:
574
+ return 'badge bg-warning text-dark';
575
+ case DocumentLifecycleStatus.Ready:
576
+ return 'badge bg-success';
577
+ case DocumentLifecycleStatus.Failed:
578
+ return 'badge bg-danger';
579
+ default:
580
+ return 'badge bg-secondary';
581
+ }
582
+ }
583
+ // #284: review badge text follows the reason: classification confirmation pending or required fields
584
+ // missing. The client renders only reviewReasons provided by the server.
585
+ reviewBadgeLabel(doc) {
586
+ const reasons = doc.reviewReasons ?? DocumentReviewReasons.None;
587
+ if ((reasons & DocumentReviewReasons.UnresolvedClassification) !== DocumentReviewReasons.None) {
588
+ return '::Document:ReviewReason:UnresolvedClassification';
589
+ }
590
+ if ((reasons & DocumentReviewReasons.MissingRequiredFields) !== DocumentReviewReasons.None) {
591
+ return '::Document:ReviewReason:MissingRequiredFields';
592
+ }
593
+ if ((reasons & DocumentReviewReasons.SegmentationIncomplete) !== DocumentReviewReasons.None) {
594
+ return '::Document:ReviewReason:SegmentationIncomplete';
595
+ }
596
+ return '::Document:NeedsReview';
597
+ }
598
+ getStatusLabel(status) {
599
+ switch (status) {
600
+ case DocumentLifecycleStatus.Uploaded:
601
+ return '::Document:Status:Uploaded';
602
+ case DocumentLifecycleStatus.Processing:
603
+ return '::Document:Status:Processing';
604
+ case DocumentLifecycleStatus.Ready:
605
+ return '::Document:Status:Ready';
606
+ case DocumentLifecycleStatus.Failed:
607
+ return '::Document:Status:Failed';
608
+ default:
609
+ return '::Document:Status:Unknown';
610
+ }
611
+ }
612
+ isImage(doc) {
613
+ return doc.fileOrigin?.contentType?.startsWith('image/') ?? false;
614
+ }
615
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DocumentListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
616
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: DocumentListComponent, isStandalone: true, selector: "lib-document-list", providers: [
617
+ ListService,
618
+ {
619
+ provide: EXTENSIONS_IDENTIFIER,
620
+ useValue: EXTRACT_TABLES.Documents,
621
+ },
622
+ ], ngImport: i0, template: "<div class=\"container-fluid py-4\">\n <!-- Header -->\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <h4 class=\"mb-0\">\n <i class=\"fas fa-file-alt me-2\"></i>\n {{ '::Document:Documents' | abpLocalization }}\n </h4>\n <div class=\"d-flex gap-2\">\n <button\n class=\"btn btn-outline-secondary\"\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 @if (canConfirm) {\n <button\n class=\"btn\"\n [class.btn-warning]=\"reviewFilter()\"\n [class.btn-outline-warning]=\"!reviewFilter()\"\n [attr.aria-pressed]=\"reviewFilter()\"\n (click)=\"toggleReviewFilter()\"\n title=\"{{ '::Document:NeedsReview' | abpLocalization }}\"\n >\n <i class=\"fas fa-clipboard-check me-1\"></i>\n {{ '::Document:NeedsReview' | abpLocalization }}\n @if (reviewQueueCount() > 0) {\n <span class=\"badge bg-danger ms-1\">{{ reviewQueueCount() }}</span>\n }\n </button>\n }\n @if (canUpload) {\n <button class=\"btn btn-primary\" (click)=\"uploadNew()\">\n <i class=\"fas fa-upload me-1\"></i>\n {{ '::Document:Upload' | abpLocalization }}\n </button>\n }\n </div>\n </div>\n\n <!-- Filters -->\n <div class=\"d-flex flex-wrap gap-2 mb-3\">\n <select\n class=\"form-select form-select-sm w-auto\"\n [ngModel]=\"lifecycleFilter()\"\n (ngModelChange)=\"onLifecycleFilterChange($event)\"\n >\n <option [ngValue]=\"undefined\">{{ '::Document:Filter:AllStatuses' | abpLocalization }}</option>\n <option [ngValue]=\"DocumentLifecycleStatus.Uploaded\">{{ '::Document:Status:Uploaded' | abpLocalization }}</option>\n <option [ngValue]=\"DocumentLifecycleStatus.Processing\">{{ '::Document:Status:Processing' | abpLocalization }}</option>\n <option [ngValue]=\"DocumentLifecycleStatus.Ready\">{{ '::Document:Status:Ready' | abpLocalization }}</option>\n <option [ngValue]=\"DocumentLifecycleStatus.Failed\">{{ '::Document:Status:Failed' | abpLocalization }}</option>\n </select>\n @if (documentTypes().length > 0) {\n <select\n class=\"form-select form-select-sm w-auto\"\n [ngModel]=\"typeFilter()\"\n (ngModelChange)=\"onTypeFilterChange($event)\"\n >\n <option value=\"\">{{ '::Document:Filter:AllTypes' | abpLocalization }}</option>\n @for (t of documentTypes(); track t.id) {\n <option [value]=\"t.typeCode\">{{ t.displayName }}</option>\n }\n </select>\n }\n @if (canViewCabinets && cabinets().length > 0) {\n <select\n class=\"form-select form-select-sm w-auto\"\n [ngModel]=\"cabinetFilter()\"\n (ngModelChange)=\"onCabinetFilterChange($event)\"\n >\n <option value=\"\">{{ '::Document:Filter:AllCabinets' | abpLocalization }}</option>\n @for (c of cabinets(); track c.id) {\n <option [value]=\"c.id\">{{ c.name }}</option>\n }\n </select>\n }\n </div>\n\n <!-- Sub-documents filter indicator (#354) -->\n @if (originDocumentIdFilter()) {\n <div class=\"alert alert-info d-flex justify-content-between align-items-center py-2 mb-3\">\n <span>\n <i class=\"fas fa-sitemap me-2\"></i>\n {{ '::Document:ViewingSubDocumentsOf' | abpLocalization }}\n <strong>{{\n subDocumentsParent()?.title ||\n subDocumentsParent()?.fileOrigin?.originalFileName ||\n originDocumentIdFilter()\n }}</strong>\n </span>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click)=\"clearSubDocumentsFilter()\">\n <i class=\"fas fa-times me-1\"></i>\n {{ '::Document:ShowAllDocuments' | abpLocalization }}\n </button>\n </div>\n }\n\n <!-- Loading spinner -->\n @if (isLoading() && documents().items.length === 0) {\n <div class=\"text-center py-5\">\n <div class=\"spinner-border text-primary\" role=\"status\"></div>\n </div>\n }\n\n <!-- Empty state: message only \u2014 the \"upload your first document\" CTA was removed; upload lives in the\n header toolbar. -->\n @if (!isLoading() && documents().totalCount === 0) {\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center py-5\">\n <i class=\"fas fa-inbox fa-3x text-muted mb-3 d-block\"></i>\n <p class=\"text-muted mb-0\">{{ '::Document:NoDocuments' | abpLocalization }}</p>\n </div>\n </div>\n }\n\n <!-- Document list -->\n @if (documents().totalCount > 0) {\n <div class=\"card shadow-sm document-list-table\">\n <div class=\"card-body p-0\">\n <ng-template #actionsTemplate let-doc>\n @if ((needsConfirmation(doc) && canConfirm) || canDelete || doc.isContainer || doc.originDocumentId) {\n <div ngbDropdown container=\"body\" class=\"d-inline-block\" (click)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"btn btn-sm btn-primary\" ngbDropdownToggle>\n {{ 'AbpUi::Actions' | abpLocalization }}\n </button>\n <div ngbDropdownMenu>\n @if (doc.isContainer) {\n <button type=\"button\" ngbDropdownItem (click)=\"viewSubDocuments(doc, $event)\">\n <i class=\"fas fa-sitemap me-2\"></i>\n {{ '::Document:ViewSubDocuments' | abpLocalization }}\n </button>\n }\n @if (doc.originDocumentId) {\n <button type=\"button\" ngbDropdownItem (click)=\"openParentDocument(doc, $event)\">\n <i class=\"fas fa-arrow-up me-2\"></i>\n {{ '::Document:ViewParentDocument' | abpLocalization }}\n </button>\n <button type=\"button\" ngbDropdownItem (click)=\"viewSiblingDocuments(doc, $event)\">\n <i class=\"fas fa-sitemap me-2\"></i>\n {{ '::Document:ViewSiblingDocuments' | abpLocalization }}\n </button>\n }\n @if (needsConfirmation(doc) && canConfirm) {\n <button type=\"button\" ngbDropdownItem (click)=\"openConfirmDialog(doc, $event)\">\n <i class=\"fas fa-check me-2\"></i>\n {{ '::Document:ConfirmClassification' | abpLocalization }}\n </button>\n }\n @if (canDelete) {\n <button type=\"button\" ngbDropdownItem (click)=\"delete(doc, $event)\">\n <i class=\"fas fa-trash me-2\"></i>\n {{ '::Delete' | abpLocalization }}\n </button>\n }\n </div>\n </div>\n }\n </ng-template>\n\n @for (key of [tableKey()]; track key) {\n @if (showActionsColumn()) {\n <abp-extensible-table\n [data]=\"documents().items\"\n [recordsTotal]=\"documents().totalCount\"\n [list]=\"list\"\n [actionsTemplate]=\"actionsTemplate\"\n actionsText=\"AbpUi::Actions\"\n [actionsColumnWidth]=\"150\"\n (tableActivate)=\"onTableActivate($event)\"\n />\n } @else {\n <abp-extensible-table\n [data]=\"documents().items\"\n [recordsTotal]=\"documents().totalCount\"\n [list]=\"list\"\n (tableActivate)=\"onTableActivate($event)\"\n />\n }\n }\n </div>\n </div>\n }\n</div>\n\n<!-- Manual Classification Confirm Modal -->\n@if (confirmingDoc(); as doc) {\n <div class=\"modal d-block\" tabindex=\"-1\" style=\"background:rgba(0,0,0,.4);\" (click)=\"closeConfirmDialog()\">\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)=\"closeConfirmDialog()\"></button>\n </div>\n <div class=\"modal-body\">\n <p class=\"text-muted small mb-3\">\n {{ doc.title || doc.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)=\"closeConfirmDialog()\">\n {{ '::Cancel' | abpLocalization }}\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n [disabled]=\"!selectedTypeId() || isConfirming()\"\n (click)=\"submitConfirmation()\"\n >\n @if (isConfirming()) {\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", styles: [".cursor-pointer{cursor:pointer}:host ::ng-deep .document-list-table .datatable-body-row{cursor:pointer}:host ::ng-deep .document-list-table .datatable-body-row:hover{background-color:#00000006}:host ::ng-deep .document-file-cell{align-items:center;display:inline-flex;max-width:100%;min-width:0}:host ::ng-deep .document-file-cell .text-truncate,:host ::ng-deep .document-field-cell{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom;white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ExtensibleTableComponent, selector: "abp-extensible-table", inputs: ["actionsText", "data", "list", "recordsTotal", "actionsColumnWidth", "actionsTemplate", "selectable", "selectionType", "selected", "infiniteScroll", "isLoading", "scrollThreshold", "tableHeight", "rowDetailTemplate", "rowDetailHeight"], outputs: ["tableActivate", "selectionChange", "loadMore", "rowDetailToggle"], exportAs: ["abpExtensibleTable"] }, { kind: "ngmodule", type: NgbDropdownModule }, { kind: "directive", type: i2.NgbDropdown, selector: "[ngbDropdown]", inputs: ["autoClose", "dropdownClass", "open", "placement", "popperOptions", "container", "display"], outputs: ["openChange"], exportAs: ["ngbDropdown"] }, { kind: "directive", type: i2.NgbDropdownToggle, selector: "[ngbDropdownToggle]" }, { kind: "directive", type: i2.NgbDropdownMenu, selector: "[ngbDropdownMenu]" }, { kind: "directive", type: i2.NgbDropdownItem, selector: "[ngbDropdownItem]", inputs: ["tabindex", "disabled"] }, { kind: "directive", type: i2.NgbDropdownButtonItem, selector: "button[ngbDropdownItem]" }, { kind: "pipe", type: LocalizationPipe, name: "abpLocalization" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
623
+ }
624
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DocumentListComponent, decorators: [{
625
+ type: Component,
626
+ args: [{ selector: 'lib-document-list', imports: [
627
+ CommonModule,
628
+ FormsModule,
629
+ LocalizationPipe,
630
+ ExtensibleTableComponent,
631
+ NgbDropdownModule,
632
+ ], providers: [
633
+ ListService,
634
+ {
635
+ provide: EXTENSIONS_IDENTIFIER,
636
+ useValue: EXTRACT_TABLES.Documents,
637
+ },
638
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"container-fluid py-4\">\n <!-- Header -->\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <h4 class=\"mb-0\">\n <i class=\"fas fa-file-alt me-2\"></i>\n {{ '::Document:Documents' | abpLocalization }}\n </h4>\n <div class=\"d-flex gap-2\">\n <button\n class=\"btn btn-outline-secondary\"\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 @if (canConfirm) {\n <button\n class=\"btn\"\n [class.btn-warning]=\"reviewFilter()\"\n [class.btn-outline-warning]=\"!reviewFilter()\"\n [attr.aria-pressed]=\"reviewFilter()\"\n (click)=\"toggleReviewFilter()\"\n title=\"{{ '::Document:NeedsReview' | abpLocalization }}\"\n >\n <i class=\"fas fa-clipboard-check me-1\"></i>\n {{ '::Document:NeedsReview' | abpLocalization }}\n @if (reviewQueueCount() > 0) {\n <span class=\"badge bg-danger ms-1\">{{ reviewQueueCount() }}</span>\n }\n </button>\n }\n @if (canUpload) {\n <button class=\"btn btn-primary\" (click)=\"uploadNew()\">\n <i class=\"fas fa-upload me-1\"></i>\n {{ '::Document:Upload' | abpLocalization }}\n </button>\n }\n </div>\n </div>\n\n <!-- Filters -->\n <div class=\"d-flex flex-wrap gap-2 mb-3\">\n <select\n class=\"form-select form-select-sm w-auto\"\n [ngModel]=\"lifecycleFilter()\"\n (ngModelChange)=\"onLifecycleFilterChange($event)\"\n >\n <option [ngValue]=\"undefined\">{{ '::Document:Filter:AllStatuses' | abpLocalization }}</option>\n <option [ngValue]=\"DocumentLifecycleStatus.Uploaded\">{{ '::Document:Status:Uploaded' | abpLocalization }}</option>\n <option [ngValue]=\"DocumentLifecycleStatus.Processing\">{{ '::Document:Status:Processing' | abpLocalization }}</option>\n <option [ngValue]=\"DocumentLifecycleStatus.Ready\">{{ '::Document:Status:Ready' | abpLocalization }}</option>\n <option [ngValue]=\"DocumentLifecycleStatus.Failed\">{{ '::Document:Status:Failed' | abpLocalization }}</option>\n </select>\n @if (documentTypes().length > 0) {\n <select\n class=\"form-select form-select-sm w-auto\"\n [ngModel]=\"typeFilter()\"\n (ngModelChange)=\"onTypeFilterChange($event)\"\n >\n <option value=\"\">{{ '::Document:Filter:AllTypes' | abpLocalization }}</option>\n @for (t of documentTypes(); track t.id) {\n <option [value]=\"t.typeCode\">{{ t.displayName }}</option>\n }\n </select>\n }\n @if (canViewCabinets && cabinets().length > 0) {\n <select\n class=\"form-select form-select-sm w-auto\"\n [ngModel]=\"cabinetFilter()\"\n (ngModelChange)=\"onCabinetFilterChange($event)\"\n >\n <option value=\"\">{{ '::Document:Filter:AllCabinets' | abpLocalization }}</option>\n @for (c of cabinets(); track c.id) {\n <option [value]=\"c.id\">{{ c.name }}</option>\n }\n </select>\n }\n </div>\n\n <!-- Sub-documents filter indicator (#354) -->\n @if (originDocumentIdFilter()) {\n <div class=\"alert alert-info d-flex justify-content-between align-items-center py-2 mb-3\">\n <span>\n <i class=\"fas fa-sitemap me-2\"></i>\n {{ '::Document:ViewingSubDocumentsOf' | abpLocalization }}\n <strong>{{\n subDocumentsParent()?.title ||\n subDocumentsParent()?.fileOrigin?.originalFileName ||\n originDocumentIdFilter()\n }}</strong>\n </span>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click)=\"clearSubDocumentsFilter()\">\n <i class=\"fas fa-times me-1\"></i>\n {{ '::Document:ShowAllDocuments' | abpLocalization }}\n </button>\n </div>\n }\n\n <!-- Loading spinner -->\n @if (isLoading() && documents().items.length === 0) {\n <div class=\"text-center py-5\">\n <div class=\"spinner-border text-primary\" role=\"status\"></div>\n </div>\n }\n\n <!-- Empty state: message only \u2014 the \"upload your first document\" CTA was removed; upload lives in the\n header toolbar. -->\n @if (!isLoading() && documents().totalCount === 0) {\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center py-5\">\n <i class=\"fas fa-inbox fa-3x text-muted mb-3 d-block\"></i>\n <p class=\"text-muted mb-0\">{{ '::Document:NoDocuments' | abpLocalization }}</p>\n </div>\n </div>\n }\n\n <!-- Document list -->\n @if (documents().totalCount > 0) {\n <div class=\"card shadow-sm document-list-table\">\n <div class=\"card-body p-0\">\n <ng-template #actionsTemplate let-doc>\n @if ((needsConfirmation(doc) && canConfirm) || canDelete || doc.isContainer || doc.originDocumentId) {\n <div ngbDropdown container=\"body\" class=\"d-inline-block\" (click)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"btn btn-sm btn-primary\" ngbDropdownToggle>\n {{ 'AbpUi::Actions' | abpLocalization }}\n </button>\n <div ngbDropdownMenu>\n @if (doc.isContainer) {\n <button type=\"button\" ngbDropdownItem (click)=\"viewSubDocuments(doc, $event)\">\n <i class=\"fas fa-sitemap me-2\"></i>\n {{ '::Document:ViewSubDocuments' | abpLocalization }}\n </button>\n }\n @if (doc.originDocumentId) {\n <button type=\"button\" ngbDropdownItem (click)=\"openParentDocument(doc, $event)\">\n <i class=\"fas fa-arrow-up me-2\"></i>\n {{ '::Document:ViewParentDocument' | abpLocalization }}\n </button>\n <button type=\"button\" ngbDropdownItem (click)=\"viewSiblingDocuments(doc, $event)\">\n <i class=\"fas fa-sitemap me-2\"></i>\n {{ '::Document:ViewSiblingDocuments' | abpLocalization }}\n </button>\n }\n @if (needsConfirmation(doc) && canConfirm) {\n <button type=\"button\" ngbDropdownItem (click)=\"openConfirmDialog(doc, $event)\">\n <i class=\"fas fa-check me-2\"></i>\n {{ '::Document:ConfirmClassification' | abpLocalization }}\n </button>\n }\n @if (canDelete) {\n <button type=\"button\" ngbDropdownItem (click)=\"delete(doc, $event)\">\n <i class=\"fas fa-trash me-2\"></i>\n {{ '::Delete' | abpLocalization }}\n </button>\n }\n </div>\n </div>\n }\n </ng-template>\n\n @for (key of [tableKey()]; track key) {\n @if (showActionsColumn()) {\n <abp-extensible-table\n [data]=\"documents().items\"\n [recordsTotal]=\"documents().totalCount\"\n [list]=\"list\"\n [actionsTemplate]=\"actionsTemplate\"\n actionsText=\"AbpUi::Actions\"\n [actionsColumnWidth]=\"150\"\n (tableActivate)=\"onTableActivate($event)\"\n />\n } @else {\n <abp-extensible-table\n [data]=\"documents().items\"\n [recordsTotal]=\"documents().totalCount\"\n [list]=\"list\"\n (tableActivate)=\"onTableActivate($event)\"\n />\n }\n }\n </div>\n </div>\n }\n</div>\n\n<!-- Manual Classification Confirm Modal -->\n@if (confirmingDoc(); as doc) {\n <div class=\"modal d-block\" tabindex=\"-1\" style=\"background:rgba(0,0,0,.4);\" (click)=\"closeConfirmDialog()\">\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)=\"closeConfirmDialog()\"></button>\n </div>\n <div class=\"modal-body\">\n <p class=\"text-muted small mb-3\">\n {{ doc.title || doc.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)=\"closeConfirmDialog()\">\n {{ '::Cancel' | abpLocalization }}\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n [disabled]=\"!selectedTypeId() || isConfirming()\"\n (click)=\"submitConfirmation()\"\n >\n @if (isConfirming()) {\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", styles: [".cursor-pointer{cursor:pointer}:host ::ng-deep .document-list-table .datatable-body-row{cursor:pointer}:host ::ng-deep .document-list-table .datatable-body-row:hover{background-color:#00000006}:host ::ng-deep .document-file-cell{align-items:center;display:inline-flex;max-width:100%;min-width:0}:host ::ng-deep .document-file-cell .text-truncate,:host ::ng-deep .document-field-cell{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom;white-space:nowrap}\n"] }]
639
+ }], ctorParameters: () => [] });
640
+
641
+ export { DocumentListComponent };
642
+ //# sourceMappingURL=dignite-vault-extract-documents-document-list.component-jThR5cct.mjs.map