@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.
- package/README.md +17 -0
- package/fesm2022/dignite-vault-extract-config.mjs +82 -0
- package/fesm2022/dignite-vault-extract-config.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-cabinet-list.component-Ch0gpSCc.mjs +184 -0
- package/fesm2022/dignite-vault-extract-documents-cabinet-list.component-Ch0gpSCc.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-content-type-DjCs-s4E.mjs +115 -0
- package/fesm2022/dignite-vault-extract-documents-content-type-DjCs-s4E.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-document-detail.component-DHs42DWJ.mjs +1146 -0
- package/fesm2022/dignite-vault-extract-documents-document-detail.component-DHs42DWJ.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-document-file-preview.component-CStXf8v9.mjs +72 -0
- package/fesm2022/dignite-vault-extract-documents-document-file-preview.component-CStXf8v9.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-document-list.component-jThR5cct.mjs +642 -0
- package/fesm2022/dignite-vault-extract-documents-document-list.component-jThR5cct.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-document-overview.component-BHUUUIVr.mjs +318 -0
- package/fesm2022/dignite-vault-extract-documents-document-overview.component-BHUUUIVr.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-document-recycle-bin.component-dqeBrw22.mjs +178 -0
- package/fesm2022/dignite-vault-extract-documents-document-recycle-bin.component-dqeBrw22.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-document-type-list.component-C8kXFJGb.mjs +464 -0
- package/fesm2022/dignite-vault-extract-documents-document-type-list.component-C8kXFJGb.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-export-template-list.component-DlmZFFF1.mjs +361 -0
- package/fesm2022/dignite-vault-extract-documents-export-template-list.component-DlmZFFF1.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-extensible-table-DkLXuoWo.mjs +53 -0
- package/fesm2022/dignite-vault-extract-documents-extensible-table-DkLXuoWo.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-field-definition-list.component-ClmWkRun.mjs +530 -0
- package/fesm2022/dignite-vault-extract-documents-field-definition-list.component-ClmWkRun.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-field-reextraction-modal.component-D7OOycv9.mjs +163 -0
- package/fesm2022/dignite-vault-extract-documents-field-reextraction-modal.component-D7OOycv9.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-format-bytes-Cd3QwfQZ.mjs +19 -0
- package/fesm2022/dignite-vault-extract-documents-format-bytes-Cd3QwfQZ.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents-format-field-value-Xjb8lwzA.mjs +22 -0
- package/fesm2022/dignite-vault-extract-documents-format-field-value-Xjb8lwzA.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract-documents.mjs +71 -0
- package/fesm2022/dignite-vault-extract-documents.mjs.map +1 -0
- package/fesm2022/dignite-vault-extract.mjs +522 -0
- package/fesm2022/dignite-vault-extract.mjs.map +1 -0
- package/package.json +38 -0
- package/types/dignite-vault-extract-config.d.ts +5 -0
- package/types/dignite-vault-extract-documents.d.ts +5 -0
- 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
|