@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,530 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, DestroyRef, signal, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
4
|
+
import { CommonModule } from '@angular/common';
|
|
5
|
+
import { ActivatedRoute, Router } from '@angular/router';
|
|
6
|
+
import * as i1 from '@angular/forms';
|
|
7
|
+
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
|
|
8
|
+
import { PermissionService, ListService, escapeHtmlChars, LocalizationService, 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 { Subject, of, map, takeUntil } from 'rxjs';
|
|
14
|
+
import { FieldDefinitionService, DocumentTypeService, SlugSuggestionService, FieldDraftSuggestionService, EXTRACT_PERMISSIONS, fieldDataTypeOptions, FieldDataType } from '@dignite/vault-extract';
|
|
15
|
+
import { c as configureEntityTable, E as EXTRACT_TABLES, p as pageClientItems } from './dignite-vault-extract-documents-extensible-table-DkLXuoWo.mjs';
|
|
16
|
+
import { w as wireSlugSuggestion, F as FieldReextractionModalComponent } from './dignite-vault-extract-documents-field-reextraction-modal.component-D7OOycv9.mjs';
|
|
17
|
+
|
|
18
|
+
// Mirrors FieldDefinitionConsts (Domain.Shared): Name whitelist + length caps.
|
|
19
|
+
const NAME_PATTERN = /^[A-Za-z0-9_\-]{1,64}$/;
|
|
20
|
+
const MAX_NAME_LENGTH = 64;
|
|
21
|
+
const MAX_DISPLAY_NAME_LENGTH = 128;
|
|
22
|
+
const MAX_PROMPT_LENGTH = 1024;
|
|
23
|
+
const FIELD_DEFINITION_SORTS = {
|
|
24
|
+
displayOrder: field => field.displayOrder,
|
|
25
|
+
name: field => field.name,
|
|
26
|
+
displayName: field => field.displayName,
|
|
27
|
+
dataType: field => field.dataType,
|
|
28
|
+
isRequired: field => field.isRequired,
|
|
29
|
+
prompt: field => field.prompt,
|
|
30
|
+
};
|
|
31
|
+
class FieldDefinitionListComponent {
|
|
32
|
+
constructor() {
|
|
33
|
+
this.route = inject(ActivatedRoute);
|
|
34
|
+
this.router = inject(Router);
|
|
35
|
+
this.service = inject(FieldDefinitionService);
|
|
36
|
+
this.documentTypeService = inject(DocumentTypeService);
|
|
37
|
+
this.slugService = inject(SlugSuggestionService);
|
|
38
|
+
this.draftService = inject(FieldDraftSuggestionService);
|
|
39
|
+
this.fb = inject(FormBuilder);
|
|
40
|
+
this.confirmation = inject(ConfirmationService);
|
|
41
|
+
this.toaster = inject(ToasterService);
|
|
42
|
+
this.permissionService = inject(PermissionService);
|
|
43
|
+
this.destroyRef = inject(DestroyRef);
|
|
44
|
+
this.extensions = inject(ExtensionsService);
|
|
45
|
+
this.list = inject(ListService);
|
|
46
|
+
// Create/edit/delete buttons require any FieldDefinitions write grant (#217); the route's
|
|
47
|
+
// FieldDefinitions.Default only lists. ABP evaluates the `||` policy expression.
|
|
48
|
+
this.canManage = this.permissionService.getGrantedPolicy(`${EXTRACT_PERMISSIONS.FieldDefinitions.Create} || ${EXTRACT_PERMISSIONS.FieldDefinitions.Update} || ${EXTRACT_PERMISSIONS.FieldDefinitions.Delete}`);
|
|
49
|
+
// Bulk field re-extraction entry point (#289): admin-level and independent from field CRUD
|
|
50
|
+
// permissions.
|
|
51
|
+
this.canReextractFields = this.permissionService.getGrantedPolicy(EXTRACT_PERMISSIONS.Documents.Reprocessing.FieldExtraction);
|
|
52
|
+
// null/false means the re-extraction modal is closed.
|
|
53
|
+
this.showReextract = signal(false, ...(ngDevMode ? [{ debugName: "showReextract" }] : /* istanbul ignore next */ []));
|
|
54
|
+
this.dataTypeOptions = fieldDataTypeOptions;
|
|
55
|
+
this.FieldDataType = FieldDataType;
|
|
56
|
+
// Route binding uses immutable DocumentTypeId (#207). The header badge primarily shows the
|
|
57
|
+
// user-friendly DisplayName (#261), while TypeCode is demoted to hover text. Both are resolved by id
|
|
58
|
+
// from types visible in the current layer, so renames are pierced.
|
|
59
|
+
this.documentTypeId = '';
|
|
60
|
+
this.documentTypeDisplayName = signal('', ...(ngDevMode ? [{ debugName: "documentTypeDisplayName" }] : /* istanbul ignore next */ []));
|
|
61
|
+
this.documentTypeCode = signal('', ...(ngDevMode ? [{ debugName: "documentTypeCode" }] : /* istanbul ignore next */ []));
|
|
62
|
+
this.allFields = signal([], ...(ngDevMode ? [{ debugName: "allFields" }] : /* istanbul ignore next */ []));
|
|
63
|
+
this.fields = signal({ totalCount: 0, items: [] }, ...(ngDevMode ? [{ debugName: "fields" }] : /* istanbul ignore next */ []));
|
|
64
|
+
this.isLoading = signal(true, ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
|
|
65
|
+
this.showDeleted = signal(false, ...(ngDevMode ? [{ debugName: "showDeleted" }] : /* istanbul ignore next */ []));
|
|
66
|
+
this.editing = signal(null, ...(ngDevMode ? [{ debugName: "editing" }] : /* istanbul ignore next */ []));
|
|
67
|
+
this.isSubmitting = signal(false, ...(ngDevMode ? [{ debugName: "isSubmitting" }] : /* istanbul ignore next */ []));
|
|
68
|
+
this.isSuggesting = signal(false, ...(ngDevMode ? [{ debugName: "isSuggesting" }] : /* istanbul ignore next */ []));
|
|
69
|
+
// #264: "draft from prompt" is in progress / just completed once. Drives the spinner and "review the
|
|
70
|
+
// draft" notice.
|
|
71
|
+
this.isDrafting = signal(false, ...(ngDevMode ? [{ debugName: "isDrafting" }] : /* istanbul ignore next */ []));
|
|
72
|
+
this.justDrafted = signal(false, ...(ngDevMode ? [{ debugName: "justDrafted" }] : /* istanbul ignore next */ []));
|
|
73
|
+
this.tableQuery = {};
|
|
74
|
+
// #264: signal that cancels in-flight draft requests. Emit when closing the modal so a late draft does
|
|
75
|
+
// not overwrite a reopened form for an unrelated field.
|
|
76
|
+
// The component-level destroyRef does not fire when the modal closes, because the modal only sets
|
|
77
|
+
// editing=null and the component is not destroyed. Therefore a separate per-modal cancellation gate is
|
|
78
|
+
// needed.
|
|
79
|
+
this.draftCancelled$ = new Subject();
|
|
80
|
+
this.form = this.fb.nonNullable.group({
|
|
81
|
+
name: [
|
|
82
|
+
'',
|
|
83
|
+
[Validators.required, Validators.maxLength(MAX_NAME_LENGTH), Validators.pattern(NAME_PATTERN)],
|
|
84
|
+
],
|
|
85
|
+
displayName: ['', [Validators.required, Validators.maxLength(MAX_DISPLAY_NAME_LENGTH)]],
|
|
86
|
+
// Extraction instruction is optional based on measured feedback: remove Validators.required and keep
|
|
87
|
+
// only the length cap. Backend NormalizePrompt converges blank values to null.
|
|
88
|
+
prompt: ['', [Validators.maxLength(MAX_PROMPT_LENGTH)]],
|
|
89
|
+
dataType: [FieldDataType.Text, [Validators.required]],
|
|
90
|
+
displayOrder: [0, [Validators.required]],
|
|
91
|
+
isRequired: [false],
|
|
92
|
+
// #212: multiple values are valid only for text, mirroring the backend
|
|
93
|
+
// FieldDefinition.ValidateMultiValue invariant. For non-text fields, applyAllowMultiplePolicy forces
|
|
94
|
+
// false and disables the control; getRawValue still returns false before submit.
|
|
95
|
+
allowMultiple: [false],
|
|
96
|
+
// #411: whether this field participates in the type's duplicate-detection unique key.
|
|
97
|
+
isUniqueKey: [false],
|
|
98
|
+
});
|
|
99
|
+
// Drives the template: allow checking "multiple values" only when dataType === Text.
|
|
100
|
+
this.isTextType = signal(true, ...(ngDevMode ? [{ debugName: "isTextType" }] : /* istanbul ignore next */ []));
|
|
101
|
+
// Backdrop close guard: close only when both mousedown and click occur on the backdrop itself, not
|
|
102
|
+
// inside the dialog.
|
|
103
|
+
// Otherwise, dragging selected text inside an input and releasing over the backdrop can make the
|
|
104
|
+
// browser fire click on the backdrop, the nearest common ancestor of mousedown/mouseup, closing the
|
|
105
|
+
// modal and losing entered content. Recording the mousedown origin is the only reliable way to know
|
|
106
|
+
// whether this click truly started from the backdrop.
|
|
107
|
+
this.backdropMouseDownOnSelf = false;
|
|
108
|
+
configureEntityTable(this.extensions, EXTRACT_TABLES.FieldDefinitions, [
|
|
109
|
+
EntityProp.create({
|
|
110
|
+
type: "number" /* ePropType.Number */,
|
|
111
|
+
name: 'displayOrder',
|
|
112
|
+
displayName: '::FieldDefinition:DisplayOrder',
|
|
113
|
+
sortable: true,
|
|
114
|
+
columnWidth: 120,
|
|
115
|
+
}),
|
|
116
|
+
EntityProp.create({
|
|
117
|
+
type: "string" /* ePropType.String */,
|
|
118
|
+
name: 'name',
|
|
119
|
+
displayName: '::FieldDefinition:Name',
|
|
120
|
+
sortable: true,
|
|
121
|
+
columnWidth: 180,
|
|
122
|
+
valueResolver: data => of(`<code>${escapeHtmlChars(data.record.name)}</code>`),
|
|
123
|
+
}),
|
|
124
|
+
EntityProp.create({
|
|
125
|
+
type: "string" /* ePropType.String */,
|
|
126
|
+
name: 'displayName',
|
|
127
|
+
displayName: '::FieldDefinition:DisplayName',
|
|
128
|
+
sortable: true,
|
|
129
|
+
}),
|
|
130
|
+
EntityProp.create({
|
|
131
|
+
type: "string" /* ePropType.String */,
|
|
132
|
+
name: 'dataType',
|
|
133
|
+
displayName: '::FieldDefinition:DataType',
|
|
134
|
+
sortable: true,
|
|
135
|
+
columnWidth: 170,
|
|
136
|
+
valueResolver: data => {
|
|
137
|
+
const localization = data.getInjected(LocalizationService);
|
|
138
|
+
const label = fieldDataTypeOptions.find(o => o.value === data.record.dataType)?.key ?? String(data.record.dataType);
|
|
139
|
+
const suffix = data.record.allowMultiple ? '[]' : '';
|
|
140
|
+
return of(`<span class="badge bg-light text-dark border">${escapeHtmlChars(localization.instant('::FieldDataType:' + label))}${suffix}</span>`);
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
EntityProp.create({
|
|
144
|
+
type: "string" /* ePropType.String */,
|
|
145
|
+
name: 'isRequired',
|
|
146
|
+
displayName: '::FieldDefinition:Required',
|
|
147
|
+
sortable: true,
|
|
148
|
+
columnWidth: 150,
|
|
149
|
+
valueResolver: data => {
|
|
150
|
+
const localization = data.getInjected(LocalizationService);
|
|
151
|
+
return of(data.record.isRequired
|
|
152
|
+
? `<span class="badge bg-warning text-dark">${escapeHtmlChars(localization.instant('::FieldDefinition:Required'))}</span>`
|
|
153
|
+
: '<span class="text-muted">-</span>');
|
|
154
|
+
},
|
|
155
|
+
}),
|
|
156
|
+
EntityProp.create({
|
|
157
|
+
type: "string" /* ePropType.String */,
|
|
158
|
+
name: 'isUniqueKey',
|
|
159
|
+
displayName: '::FieldDefinition:IsUniqueKey',
|
|
160
|
+
sortable: true,
|
|
161
|
+
columnWidth: 150,
|
|
162
|
+
valueResolver: data => {
|
|
163
|
+
const localization = data.getInjected(LocalizationService);
|
|
164
|
+
return of(data.record.isUniqueKey
|
|
165
|
+
? `<span class="badge bg-primary">${escapeHtmlChars(localization.instant('::FieldDefinition:IsUniqueKey'))}</span>`
|
|
166
|
+
: '<span class="text-muted">-</span>');
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
EntityProp.create({
|
|
170
|
+
type: "string" /* ePropType.String */,
|
|
171
|
+
name: 'prompt',
|
|
172
|
+
displayName: '::FieldDefinition:Prompt',
|
|
173
|
+
sortable: true,
|
|
174
|
+
columnWidth: 320,
|
|
175
|
+
valueResolver: data => {
|
|
176
|
+
const prompt = data.record.prompt;
|
|
177
|
+
return of(prompt
|
|
178
|
+
? `<span class="d-inline-block text-truncate" style="max-width:280px" title="${escapeHtmlChars(prompt)}">${escapeHtmlChars(prompt)}</span>`
|
|
179
|
+
: '<span class="text-muted">-</span>');
|
|
180
|
+
},
|
|
181
|
+
}),
|
|
182
|
+
]);
|
|
183
|
+
}
|
|
184
|
+
ngOnInit() {
|
|
185
|
+
this.hookTableQuery();
|
|
186
|
+
this.documentTypeId = this.route.snapshot.paramMap.get('typeId') ?? '';
|
|
187
|
+
this.resolveDocumentType();
|
|
188
|
+
this.slugHandle = wireSlugSuggestion({
|
|
189
|
+
displayName: this.form.controls.displayName,
|
|
190
|
+
target: this.form.controls.name,
|
|
191
|
+
suggest: text => this.slugService.suggest({ label: text }, undefined).pipe(map(r => r.slug ?? '')),
|
|
192
|
+
fallback: () => this.nextFieldSlug(),
|
|
193
|
+
destroyRef: this.destroyRef,
|
|
194
|
+
onPending: pending => this.isSuggesting.set(pending),
|
|
195
|
+
});
|
|
196
|
+
// #212: apply the "multiple values only for text" policy whenever dataType changes, mirroring the
|
|
197
|
+
// backend invariant and preventing illegal combinations from failing loudly on submit.
|
|
198
|
+
this.form.controls.dataType.valueChanges
|
|
199
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
200
|
+
.subscribe(dataType => this.applyAllowMultiplePolicy(dataType));
|
|
201
|
+
this.load();
|
|
202
|
+
}
|
|
203
|
+
// Non-text fields force allowMultiple=false and disable the checkbox. Switching back to text re-enables
|
|
204
|
+
// it while preserving the current value.
|
|
205
|
+
// Text plus multiple values is the only combination allowed by the backend entity layer
|
|
206
|
+
// (FieldDefinition.MultiValueRequiresStringType), and the client mirrors that constraint for UX
|
|
207
|
+
// guardrails.
|
|
208
|
+
applyAllowMultiplePolicy(dataType) {
|
|
209
|
+
const isText = dataType === FieldDataType.Text;
|
|
210
|
+
this.isTextType.set(isText);
|
|
211
|
+
const control = this.form.controls.allowMultiple;
|
|
212
|
+
if (isText) {
|
|
213
|
+
control.enable({ emitEvent: false });
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
control.setValue(false, { emitEvent: false });
|
|
217
|
+
control.disable({ emitEvent: false });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// For the header badge: resolve the current type by immutable id from types visible in the current
|
|
221
|
+
// layer. DisplayName is primary, and TypeCode is hover text, piercing renames.
|
|
222
|
+
resolveDocumentType() {
|
|
223
|
+
this.documentTypeService.getVisible()
|
|
224
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
225
|
+
.subscribe({
|
|
226
|
+
next: types => {
|
|
227
|
+
const type = types.find(t => t.id === this.documentTypeId);
|
|
228
|
+
this.documentTypeDisplayName.set(type?.displayName ?? '');
|
|
229
|
+
this.documentTypeCode.set(type?.typeCode ?? '');
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
// Local fallback when the LLM is unavailable or does not translate: choose the smallest field_{n} that
|
|
234
|
+
// does not conflict with existing field names.
|
|
235
|
+
nextFieldSlug() {
|
|
236
|
+
const existing = new Set(this.allFields().map(f => f.name));
|
|
237
|
+
let i = 1;
|
|
238
|
+
while (existing.has(`field_${i}`))
|
|
239
|
+
i++;
|
|
240
|
+
return `field_${i}`;
|
|
241
|
+
}
|
|
242
|
+
refresh() {
|
|
243
|
+
this.load();
|
|
244
|
+
}
|
|
245
|
+
toggleDeleted() {
|
|
246
|
+
this.showDeleted.update(v => !v);
|
|
247
|
+
this.load();
|
|
248
|
+
}
|
|
249
|
+
goBack() {
|
|
250
|
+
this.router.navigate(['/documents/types']);
|
|
251
|
+
}
|
|
252
|
+
openReextractFields() {
|
|
253
|
+
if (this.documentTypeId) {
|
|
254
|
+
this.showReextract.set(true);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
load() {
|
|
258
|
+
this.isLoading.set(true);
|
|
259
|
+
const source$ = this.service.getList({
|
|
260
|
+
documentTypeId: this.documentTypeId,
|
|
261
|
+
onlyDeleted: this.showDeleted(),
|
|
262
|
+
});
|
|
263
|
+
source$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
264
|
+
next: list => {
|
|
265
|
+
this.allFields.set([...list].sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0)));
|
|
266
|
+
this.list.totalCount = list.length;
|
|
267
|
+
this.applyTableQuery();
|
|
268
|
+
this.isLoading.set(false);
|
|
269
|
+
},
|
|
270
|
+
error: () => {
|
|
271
|
+
this.allFields.set([]);
|
|
272
|
+
this.fields.set({ totalCount: 0, items: [] });
|
|
273
|
+
this.list.totalCount = 0;
|
|
274
|
+
this.isLoading.set(false);
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
hookTableQuery() {
|
|
279
|
+
this.list.query$
|
|
280
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
281
|
+
.subscribe(query => this.applyTableQuery(query));
|
|
282
|
+
}
|
|
283
|
+
applyTableQuery(query = this.tableQuery) {
|
|
284
|
+
this.tableQuery = query;
|
|
285
|
+
this.fields.set(pageClientItems(this.allFields(), query, FIELD_DEFINITION_SORTS));
|
|
286
|
+
}
|
|
287
|
+
openCreate() {
|
|
288
|
+
const nextOrder = this.allFields().reduce((max, f) => Math.max(max, f.displayOrder ?? 0), -1) + 1;
|
|
289
|
+
this.form.reset({
|
|
290
|
+
name: '',
|
|
291
|
+
displayName: '',
|
|
292
|
+
prompt: '',
|
|
293
|
+
dataType: FieldDataType.Text,
|
|
294
|
+
displayOrder: nextOrder,
|
|
295
|
+
isRequired: false,
|
|
296
|
+
allowMultiple: false,
|
|
297
|
+
isUniqueKey: false,
|
|
298
|
+
});
|
|
299
|
+
this.form.controls.name.enable();
|
|
300
|
+
this.applyAllowMultiplePolicy(FieldDataType.Text);
|
|
301
|
+
// Must be called after form.reset()/enable(): both trigger valueChanges that can be misread as
|
|
302
|
+
// "manual edit". reset() clears that marker and resets suggestion state, including the spinner.
|
|
303
|
+
this.slugHandle?.reset();
|
|
304
|
+
this.justDrafted.set(false);
|
|
305
|
+
this.isDrafting.set(false);
|
|
306
|
+
this.editing.set('create');
|
|
307
|
+
}
|
|
308
|
+
openEdit(field) {
|
|
309
|
+
// Disable before reset so slug auto-suggestion sees edit-mode reset as not automatically managed and
|
|
310
|
+
// does not clear the existing name as a stale key. See wireSlugSuggestion comments.
|
|
311
|
+
this.form.controls.name.disable();
|
|
312
|
+
this.form.reset({
|
|
313
|
+
name: field.name,
|
|
314
|
+
displayName: field.displayName,
|
|
315
|
+
prompt: field.prompt ?? '',
|
|
316
|
+
dataType: field.dataType,
|
|
317
|
+
displayOrder: field.displayOrder,
|
|
318
|
+
isRequired: field.isRequired,
|
|
319
|
+
allowMultiple: field.allowMultiple,
|
|
320
|
+
isUniqueKey: field.isUniqueKey ?? false,
|
|
321
|
+
});
|
|
322
|
+
this.form.controls.name.enable();
|
|
323
|
+
this.applyAllowMultiplePolicy(field.dataType ?? FieldDataType.Text);
|
|
324
|
+
this.slugHandle?.markManual();
|
|
325
|
+
this.justDrafted.set(false);
|
|
326
|
+
this.isDrafting.set(false);
|
|
327
|
+
this.editing.set(field);
|
|
328
|
+
}
|
|
329
|
+
// #264: draft field metadata from the prompt. The prompt is the primary input; one LLM call drafts the
|
|
330
|
+
// remaining fields, applies them as a group, and lets the user review or modify each item.
|
|
331
|
+
draft() {
|
|
332
|
+
const prompt = (this.form.controls.prompt.value ?? '').trim();
|
|
333
|
+
if (!prompt || this.isDrafting())
|
|
334
|
+
return;
|
|
335
|
+
// forNewField controls whether the backend also suggests the machine key Name. When editing an
|
|
336
|
+
// existing field, Name is a contract-level frozen identity key and is not overwritten by drafting
|
|
337
|
+
// (guardrail 1).
|
|
338
|
+
const forNewField = this.editing() === 'create';
|
|
339
|
+
this.isDrafting.set(true);
|
|
340
|
+
this.draftService.draft({ prompt, forNewField }, undefined)
|
|
341
|
+
// takeUntil(draftCancelled$): cancel when the modal closes, so late responses do not write into a
|
|
342
|
+
// new form (#264 review #1).
|
|
343
|
+
.pipe(takeUntil(this.draftCancelled$), takeUntilDestroyed(this.destroyRef))
|
|
344
|
+
.subscribe({
|
|
345
|
+
next: draft => {
|
|
346
|
+
this.applyDraft(draft, forNewField);
|
|
347
|
+
this.isDrafting.set(false);
|
|
348
|
+
},
|
|
349
|
+
error: () => {
|
|
350
|
+
this.isDrafting.set(false);
|
|
351
|
+
// No draft was produced this time. Reset the "review draft" banner to avoid contradicting the
|
|
352
|
+
// "draft unavailable" hint on the same screen (#264 review2 #1, aligned with the empty-draft
|
|
353
|
+
// branch).
|
|
354
|
+
this.justDrafted.set(false);
|
|
355
|
+
this.toaster.warn('::FieldDefinition:DraftUnavailable', '::Warning');
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
// Apply the corresponding controls as a group, the landing behavior confirmed in issue #264.
|
|
360
|
+
// emitEvent:false avoids triggering the displayName-to-slug wiring and clearing the just-drafted name.
|
|
361
|
+
applyDraft(draft, forNewField) {
|
|
362
|
+
// Backend draft failure or timeout falls back to a conservative empty draft. Empty DisplayName means
|
|
363
|
+
// unavailable: keep user-entered content, show a manual-entry hint, and do not overwrite.
|
|
364
|
+
if (!draft.displayName) {
|
|
365
|
+
// Reset the "review draft" banner: this run produced no draft, avoiding a contradiction between a
|
|
366
|
+
// previous success banner and the "draft unavailable" hint (#264 review #6).
|
|
367
|
+
this.justDrafted.set(false);
|
|
368
|
+
this.toaster.info('::FieldDefinition:DraftUnavailable', '::Info');
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const dataType = draft.dataType ?? FieldDataType.Text;
|
|
372
|
+
this.form.controls.displayName.setValue(draft.displayName, { emitEvent: false });
|
|
373
|
+
this.form.controls.dataType.setValue(dataType, { emitEvent: false });
|
|
374
|
+
this.form.controls.isRequired.setValue(draft.isRequired ?? false, { emitEvent: false });
|
|
375
|
+
// setValue(dataType) used emitEvent:false, so valueChanges does not fire; manually apply the
|
|
376
|
+
// "multiple values only for text" policy to enable/disable the checkbox.
|
|
377
|
+
this.applyAllowMultiplePolicy(dataType);
|
|
378
|
+
this.form.controls.allowMultiple.setValue(dataType === FieldDataType.Text && (draft.allowMultiple ?? false), { emitEvent: false });
|
|
379
|
+
if (forNewField) {
|
|
380
|
+
// Create mode: overwrite the machine key as part of the group. Use the suggested value, or fall
|
|
381
|
+
// back to local placeholder field_{n} when missing, such as when pure CJK sanitizes to empty after
|
|
382
|
+
// no translation.
|
|
383
|
+
// Never leave behind a stale key based on the previous display name (#264 review #2). Mark it as
|
|
384
|
+
// manually retained so later displayName blur does not overwrite this drafted/reviewed key with a
|
|
385
|
+
// slug; the user may still edit name manually.
|
|
386
|
+
this.form.controls.name.setValue(draft.name || this.nextFieldSlug(), { emitEvent: false });
|
|
387
|
+
this.slugHandle?.markManual();
|
|
388
|
+
}
|
|
389
|
+
this.form.markAsDirty();
|
|
390
|
+
this.justDrafted.set(true);
|
|
391
|
+
}
|
|
392
|
+
// Display-name blur triggers slug auto-suggestion. Measured feedback changed this from pause debounce
|
|
393
|
+
// to blur trigger.
|
|
394
|
+
onDisplayNameBlur() {
|
|
395
|
+
// Do not trigger the blur slug path while drafting is in flight; otherwise two LLM responses compete
|
|
396
|
+
// to write name, and the last landing response is random (#264 review #2).
|
|
397
|
+
// Drafting itself applies the group and markManual name, so the blur path does not need to supplement it.
|
|
398
|
+
if (this.isDrafting())
|
|
399
|
+
return;
|
|
400
|
+
this.slugHandle?.notifyDisplayNameBlur();
|
|
401
|
+
}
|
|
402
|
+
onBackdropMouseDown(event) {
|
|
403
|
+
this.backdropMouseDownOnSelf = event.target === event.currentTarget;
|
|
404
|
+
}
|
|
405
|
+
onBackdropClick(event) {
|
|
406
|
+
if (this.backdropMouseDownOnSelf && event.target === event.currentTarget) {
|
|
407
|
+
this.closeModal();
|
|
408
|
+
}
|
|
409
|
+
this.backdropMouseDownOnSelf = false;
|
|
410
|
+
}
|
|
411
|
+
closeModal() {
|
|
412
|
+
// Cancel any in-flight draft request and clear the spinner, preventing late drafts from contaminating
|
|
413
|
+
// the next opened form or leaving the draft button permanently disabled (#264 review #1).
|
|
414
|
+
this.draftCancelled$.next();
|
|
415
|
+
this.isDrafting.set(false);
|
|
416
|
+
this.justDrafted.set(false);
|
|
417
|
+
this.editing.set(null);
|
|
418
|
+
}
|
|
419
|
+
submit() {
|
|
420
|
+
if (this.form.invalid) {
|
|
421
|
+
this.form.markAllAsTouched();
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const mode = this.editing();
|
|
425
|
+
if (mode === null)
|
|
426
|
+
return;
|
|
427
|
+
this.isSubmitting.set(true);
|
|
428
|
+
const raw = this.form.getRawValue();
|
|
429
|
+
if (mode === 'create') {
|
|
430
|
+
const input = {
|
|
431
|
+
documentTypeId: this.documentTypeId,
|
|
432
|
+
name: raw.name,
|
|
433
|
+
displayName: raw.displayName,
|
|
434
|
+
prompt: raw.prompt,
|
|
435
|
+
dataType: raw.dataType,
|
|
436
|
+
displayOrder: raw.displayOrder,
|
|
437
|
+
isRequired: raw.isRequired,
|
|
438
|
+
// For non-text fields the control is disabled, but getRawValue still carries it back after the
|
|
439
|
+
// policy has set it to false.
|
|
440
|
+
allowMultiple: raw.allowMultiple,
|
|
441
|
+
isUniqueKey: raw.isUniqueKey,
|
|
442
|
+
};
|
|
443
|
+
this.service.create(input)
|
|
444
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
445
|
+
.subscribe({
|
|
446
|
+
next: () => this.onSaved('::FieldDefinition:CreatedSuccessfully'),
|
|
447
|
+
error: () => this.isSubmitting.set(false),
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
this.service.update(mode.id, {
|
|
452
|
+
name: raw.name,
|
|
453
|
+
displayName: raw.displayName,
|
|
454
|
+
prompt: raw.prompt,
|
|
455
|
+
dataType: raw.dataType,
|
|
456
|
+
displayOrder: raw.displayOrder,
|
|
457
|
+
isRequired: raw.isRequired,
|
|
458
|
+
allowMultiple: raw.allowMultiple,
|
|
459
|
+
isUniqueKey: raw.isUniqueKey,
|
|
460
|
+
})
|
|
461
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
462
|
+
.subscribe({
|
|
463
|
+
next: () => this.onSaved('::FieldDefinition:UpdatedSuccessfully'),
|
|
464
|
+
error: () => this.isSubmitting.set(false),
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
onSaved(messageKey) {
|
|
469
|
+
this.isSubmitting.set(false);
|
|
470
|
+
this.closeModal();
|
|
471
|
+
this.toaster.success(messageKey, '::Success');
|
|
472
|
+
this.load();
|
|
473
|
+
}
|
|
474
|
+
delete(field) {
|
|
475
|
+
this.confirmation
|
|
476
|
+
.warn('::FieldDefinition:AreYouSureToDelete', '::AreYouSure')
|
|
477
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
478
|
+
.subscribe(status => {
|
|
479
|
+
if (status !== Confirmation.Status.confirm)
|
|
480
|
+
return;
|
|
481
|
+
this.service.delete(field.id)
|
|
482
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
483
|
+
.subscribe({
|
|
484
|
+
next: () => {
|
|
485
|
+
this.toaster.success('::FieldDefinition:DeletedSuccessfully', '::Success');
|
|
486
|
+
this.load();
|
|
487
|
+
},
|
|
488
|
+
error: () => this.toaster.error('::FieldDefinition:DeleteFailed', '::Error'),
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
restore(field) {
|
|
493
|
+
this.service.restore(field.id)
|
|
494
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
495
|
+
.subscribe({
|
|
496
|
+
next: () => {
|
|
497
|
+
this.toaster.success('::FieldDefinition:RestoredSuccessfully', '::Success');
|
|
498
|
+
this.load();
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: FieldDefinitionListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
503
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: FieldDefinitionListComponent, isStandalone: true, selector: "lib-field-definition-list", providers: [
|
|
504
|
+
ListService,
|
|
505
|
+
{
|
|
506
|
+
provide: EXTENSIONS_IDENTIFIER,
|
|
507
|
+
useValue: EXTRACT_TABLES.FieldDefinitions,
|
|
508
|
+
},
|
|
509
|
+
], ngImport: i0, template: "<div class=\"container-fluid py-4\">\n <!-- Header -->\n <div class=\"d-flex align-items-center mb-3 gap-2 flex-wrap\">\n <button class=\"btn btn-outline-secondary btn-sm\" (click)=\"goBack()\">\n <i class=\"fas fa-arrow-left me-1\"></i>\n {{ '::Back' | abpLocalization }}\n </button>\n <h5 class=\"mb-0 ms-2\">\n <i class=\"fas fa-list-ul me-2\"></i>\n {{ '::FieldDefinition:Title' | abpLocalization }}\n </h5>\n <span class=\"badge bg-info text-dark ms-1\" [title]=\"documentTypeCode()\">{{ documentTypeDisplayName() }}</span>\n <div class=\"ms-auto 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 <button\n class=\"btn\"\n [class.btn-secondary]=\"showDeleted()\"\n [class.btn-outline-secondary]=\"!showDeleted()\"\n (click)=\"toggleDeleted()\"\n >\n <i class=\"fas fa-trash-can me-1\"></i>\n {{ '::FieldDefinition:ShowDeleted' | abpLocalization }}\n </button>\n @if (canReextractFields && !showDeleted()) {\n <button class=\"btn btn-outline-primary\" (click)=\"openReextractFields()\" [disabled]=\"isLoading() || !documentTypeId\">\n <i class=\"fas fa-wand-magic-sparkles me-1\"></i>\n {{ '::Document:Reprocess:FieldExtraction:Action' | abpLocalization }}\n </button>\n }\n @if (canManage && !showDeleted()) {\n <button class=\"btn btn-primary\" (click)=\"openCreate()\">\n <i class=\"fas fa-plus me-1\"></i>\n {{ '::FieldDefinition:New' | abpLocalization }}\n </button>\n }\n </div>\n </div>\n\n <!-- Loading spinner -->\n @if (isLoading()) {\n <div class=\"text-center py-5\">\n <div class=\"spinner-border text-primary\" role=\"status\"></div>\n </div>\n }\n\n <!-- Empty state -->\n @if (!isLoading() && fields().totalCount === 0) {\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center py-5\">\n <i class=\"fas fa-list-ul fa-3x text-muted mb-3 d-block\"></i>\n <p class=\"text-muted mb-0\">\n {{ (showDeleted() ? '::FieldDefinition:RecycleBinEmpty' : '::FieldDefinition:Empty') | abpLocalization }}\n </p>\n </div>\n </div>\n }\n\n <!-- Fields table -->\n @if (!isLoading() && fields().totalCount > 0) {\n <div class=\"card shadow-sm\">\n <div class=\"card-body p-0\">\n <ng-template #actionsTemplate let-field>\n @if (showDeleted()) {\n @if (canManage) {\n <div ngbDropdown container=\"body\" class=\"d-inline-block\">\n <button type=\"button\" class=\"btn btn-sm btn-primary\" ngbDropdownToggle>\n {{ 'AbpUi::Actions' | abpLocalization }}\n </button>\n <div ngbDropdownMenu>\n <button type=\"button\" ngbDropdownItem (click)=\"restore(field)\">\n <i class=\"fas fa-rotate-left me-2\"></i>\n {{ '::FieldDefinition:Restore' | abpLocalization }}\n </button>\n </div>\n </div>\n }\n } @else if (canManage) {\n <div ngbDropdown container=\"body\" class=\"d-inline-block\">\n <button type=\"button\" class=\"btn btn-sm btn-primary\" ngbDropdownToggle>\n {{ 'AbpUi::Actions' | abpLocalization }}\n </button>\n <div ngbDropdownMenu>\n <button type=\"button\" ngbDropdownItem (click)=\"openEdit(field)\">\n <i class=\"fas fa-pen me-2\"></i>\n {{ '::Edit' | abpLocalization }}\n </button>\n <button type=\"button\" ngbDropdownItem (click)=\"delete(field)\">\n <i class=\"fas fa-trash me-2\"></i>\n {{ '::Delete' | abpLocalization }}\n </button>\n </div>\n </div>\n }\n </ng-template>\n\n <abp-extensible-table\n [data]=\"fields().items\"\n [recordsTotal]=\"fields().totalCount\"\n [list]=\"list\"\n [actionsTemplate]=\"actionsTemplate\"\n actionsText=\"AbpUi::Actions\"\n [actionsColumnWidth]=\"150\"\n />\n </div>\n </div>\n }\n</div>\n\n<!-- Create / Edit modal -->\n@if (editing(); as mode) {\n <div\n class=\"modal d-block\"\n tabindex=\"-1\"\n style=\"background:rgba(0,0,0,.4);\"\n (mousedown)=\"onBackdropMouseDown($event)\"\n (click)=\"onBackdropClick($event)\"\n >\n <div class=\"modal-dialog modal-dialog-centered modal-lg\">\n <div class=\"modal-content\">\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\">\n <div class=\"modal-header\">\n <h5 class=\"modal-title\">\n <i class=\"fas fa-list-ul me-2\"></i>\n {{ (mode === 'create' ? '::FieldDefinition:New' : '::FieldDefinition:Edit') | abpLocalization }}\n </h5>\n <button type=\"button\" class=\"btn-close\" (click)=\"closeModal()\"></button>\n </div>\n <div class=\"modal-body\">\n <!-- #264: Prompt as the primary input. Admins focus on the extraction prompt, then use AI to draft the remaining editable field metadata. -->\n <div class=\"mb-3\">\n <div class=\"d-flex align-items-center gap-2 mb-1\">\n <label class=\"form-label mb-0\">{{ '::FieldDefinition:Prompt' | abpLocalization }}</label>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-primary ms-auto\"\n (click)=\"draft()\"\n [disabled]=\"!form.controls.prompt.value.trim() || isDrafting()\"\n title=\"{{ '::FieldDefinition:DraftHint' | abpLocalization }}\"\n >\n @if (isDrafting()) {\n <span class=\"spinner-border spinner-border-sm me-1\"></span>\n } @else {\n <i class=\"fas fa-wand-magic-sparkles me-1\"></i>\n }\n {{ '::FieldDefinition:DraftFromPrompt' | abpLocalization }}\n </button>\n </div>\n <textarea\n class=\"form-control\"\n rows=\"3\"\n formControlName=\"prompt\"\n [class.is-invalid]=\"form.controls.prompt.touched && form.controls.prompt.invalid\"\n placeholder=\"{{ '::FieldDefinition:PromptHint' | abpLocalization }}\"\n ></textarea>\n <div class=\"form-text\">{{ '::FieldDefinition:DraftHint' | abpLocalization }}</div>\n </div>\n\n @if (justDrafted()) {\n <div class=\"alert alert-info py-2 px-3 small d-flex align-items-center gap-2\" role=\"alert\">\n <i class=\"fas fa-circle-info\"></i>\n <span>{{ '::FieldDefinition:DraftReviewNotice' | abpLocalization }}</span>\n </div>\n }\n\n <div class=\"row g-3\">\n <div class=\"col-md-6\">\n <label class=\"form-label\">{{ '::FieldDefinition:DisplayName' | abpLocalization }}</label>\n <input\n type=\"text\"\n class=\"form-control\"\n formControlName=\"displayName\"\n (blur)=\"onDisplayNameBlur()\"\n [class.is-invalid]=\"form.controls.displayName.touched && form.controls.displayName.invalid\"\n />\n </div>\n <div class=\"col-md-6\">\n <label class=\"form-label d-flex align-items-center gap-2\">\n <span>{{ '::FieldDefinition:Name' | abpLocalization }}</span>\n @if (isSuggesting()) {\n <span class=\"text-muted small fw-normal\">\n <span class=\"spinner-border spinner-border-sm me-1\" style=\"width:.75rem;height:.75rem;\"></span>\n {{ '::FieldDefinition:NameSuggesting' | abpLocalization }}\n </span>\n }\n </label>\n <input\n type=\"text\"\n class=\"form-control\"\n formControlName=\"name\"\n [class.is-invalid]=\"form.controls.name.touched && form.controls.name.invalid\"\n placeholder=\"party_name\"\n />\n <div class=\"form-text\">{{ '::FieldDefinition:NameHint' | abpLocalization }}</div>\n @if (form.controls.name.touched && form.controls.name.invalid) {\n <div class=\"invalid-feedback d-block\">{{ '::FieldDefinition:NameInvalid' | abpLocalization }}</div>\n }\n </div>\n </div>\n\n <div class=\"row g-3 align-items-start mt-3\">\n <div class=\"col-md-5\">\n <label class=\"form-label\">{{ '::FieldDefinition:DataType' | abpLocalization }}</label>\n <select class=\"form-select\" formControlName=\"dataType\">\n @for (opt of dataTypeOptions; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ '::FieldDataType:' + opt.key | abpLocalization }}</option>\n }\n </select>\n </div>\n <div class=\"col-md-3\">\n <label class=\"form-label\">{{ '::FieldDefinition:DisplayOrder' | abpLocalization }}</label>\n <input type=\"number\" class=\"form-control\" formControlName=\"displayOrder\" step=\"1\" />\n </div>\n <div class=\"col-md-4\">\n <div class=\"form-check mt-2\">\n <input type=\"checkbox\" class=\"form-check-input\" id=\"isRequired\" formControlName=\"isRequired\" />\n <label class=\"form-check-label\" for=\"isRequired\">\n {{ '::FieldDefinition:Required' | abpLocalization }}\n </label>\n </div>\n <!-- #212: Multiple values are valid only for text. Non-text controls are disabled by applyAllowMultiplePolicy, and the persistent hint explains the constraint, matching backend FieldDefinition.MultiValueRequiresStringType. -->\n <div class=\"form-check\">\n <input type=\"checkbox\" class=\"form-check-input\" id=\"allowMultiple\" formControlName=\"allowMultiple\" />\n <label class=\"form-check-label\" for=\"allowMultiple\">\n {{ '::FieldDefinition:AllowMultiple' | abpLocalization }}\n </label>\n </div>\n <div class=\"form-text\">{{ '::FieldDefinition:AllowMultipleHint' | abpLocalization }}</div>\n <!-- #411: unique-key fields form the duplicate-detection fingerprint. -->\n <div class=\"form-check mt-2\">\n <input type=\"checkbox\" class=\"form-check-input\" id=\"isUniqueKey\" formControlName=\"isUniqueKey\" />\n <label class=\"form-check-label\" for=\"isUniqueKey\">\n {{ '::FieldDefinition:IsUniqueKey' | abpLocalization }}\n </label>\n </div>\n <div class=\"form-text\">{{ '::FieldDefinition:IsUniqueKeyHint' | abpLocalization }}</div>\n </div>\n </div>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"closeModal()\">\n {{ '::Cancel' | abpLocalization }}\n </button>\n <button type=\"submit\" class=\"btn btn-primary\" [disabled]=\"form.invalid || isSubmitting()\">\n @if (isSubmitting()) {\n <span class=\"spinner-border spinner-border-sm me-1\"></span>\n }\n {{ '::Save' | abpLocalization }}\n </button>\n </div>\n </form>\n </div>\n </div>\n </div>\n}\n\n<!-- Bulk field re-extraction modal (#289), scoped to the current document type -->\n@if (showReextract()) {\n <lib-field-reextraction-modal\n [documentTypeId]=\"documentTypeId\"\n [documentTypeDisplayName]=\"documentTypeDisplayName()\"\n (closed)=\"showReextract.set(false)\"\n />\n}\n", styles: ["tbody tr:hover{background-color:#00000006}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { 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.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { 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: "component", type: FieldReextractionModalComponent, selector: "lib-field-reextraction-modal", inputs: ["documentTypeId", "documentTypeDisplayName"], outputs: ["closed"] }, { kind: "pipe", type: LocalizationPipe, name: "abpLocalization" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
510
|
+
}
|
|
511
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: FieldDefinitionListComponent, decorators: [{
|
|
512
|
+
type: Component,
|
|
513
|
+
args: [{ selector: 'lib-field-definition-list', imports: [
|
|
514
|
+
CommonModule,
|
|
515
|
+
ReactiveFormsModule,
|
|
516
|
+
LocalizationPipe,
|
|
517
|
+
ExtensibleTableComponent,
|
|
518
|
+
NgbDropdownModule,
|
|
519
|
+
FieldReextractionModalComponent,
|
|
520
|
+
], providers: [
|
|
521
|
+
ListService,
|
|
522
|
+
{
|
|
523
|
+
provide: EXTENSIONS_IDENTIFIER,
|
|
524
|
+
useValue: EXTRACT_TABLES.FieldDefinitions,
|
|
525
|
+
},
|
|
526
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"container-fluid py-4\">\n <!-- Header -->\n <div class=\"d-flex align-items-center mb-3 gap-2 flex-wrap\">\n <button class=\"btn btn-outline-secondary btn-sm\" (click)=\"goBack()\">\n <i class=\"fas fa-arrow-left me-1\"></i>\n {{ '::Back' | abpLocalization }}\n </button>\n <h5 class=\"mb-0 ms-2\">\n <i class=\"fas fa-list-ul me-2\"></i>\n {{ '::FieldDefinition:Title' | abpLocalization }}\n </h5>\n <span class=\"badge bg-info text-dark ms-1\" [title]=\"documentTypeCode()\">{{ documentTypeDisplayName() }}</span>\n <div class=\"ms-auto 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 <button\n class=\"btn\"\n [class.btn-secondary]=\"showDeleted()\"\n [class.btn-outline-secondary]=\"!showDeleted()\"\n (click)=\"toggleDeleted()\"\n >\n <i class=\"fas fa-trash-can me-1\"></i>\n {{ '::FieldDefinition:ShowDeleted' | abpLocalization }}\n </button>\n @if (canReextractFields && !showDeleted()) {\n <button class=\"btn btn-outline-primary\" (click)=\"openReextractFields()\" [disabled]=\"isLoading() || !documentTypeId\">\n <i class=\"fas fa-wand-magic-sparkles me-1\"></i>\n {{ '::Document:Reprocess:FieldExtraction:Action' | abpLocalization }}\n </button>\n }\n @if (canManage && !showDeleted()) {\n <button class=\"btn btn-primary\" (click)=\"openCreate()\">\n <i class=\"fas fa-plus me-1\"></i>\n {{ '::FieldDefinition:New' | abpLocalization }}\n </button>\n }\n </div>\n </div>\n\n <!-- Loading spinner -->\n @if (isLoading()) {\n <div class=\"text-center py-5\">\n <div class=\"spinner-border text-primary\" role=\"status\"></div>\n </div>\n }\n\n <!-- Empty state -->\n @if (!isLoading() && fields().totalCount === 0) {\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-center py-5\">\n <i class=\"fas fa-list-ul fa-3x text-muted mb-3 d-block\"></i>\n <p class=\"text-muted mb-0\">\n {{ (showDeleted() ? '::FieldDefinition:RecycleBinEmpty' : '::FieldDefinition:Empty') | abpLocalization }}\n </p>\n </div>\n </div>\n }\n\n <!-- Fields table -->\n @if (!isLoading() && fields().totalCount > 0) {\n <div class=\"card shadow-sm\">\n <div class=\"card-body p-0\">\n <ng-template #actionsTemplate let-field>\n @if (showDeleted()) {\n @if (canManage) {\n <div ngbDropdown container=\"body\" class=\"d-inline-block\">\n <button type=\"button\" class=\"btn btn-sm btn-primary\" ngbDropdownToggle>\n {{ 'AbpUi::Actions' | abpLocalization }}\n </button>\n <div ngbDropdownMenu>\n <button type=\"button\" ngbDropdownItem (click)=\"restore(field)\">\n <i class=\"fas fa-rotate-left me-2\"></i>\n {{ '::FieldDefinition:Restore' | abpLocalization }}\n </button>\n </div>\n </div>\n }\n } @else if (canManage) {\n <div ngbDropdown container=\"body\" class=\"d-inline-block\">\n <button type=\"button\" class=\"btn btn-sm btn-primary\" ngbDropdownToggle>\n {{ 'AbpUi::Actions' | abpLocalization }}\n </button>\n <div ngbDropdownMenu>\n <button type=\"button\" ngbDropdownItem (click)=\"openEdit(field)\">\n <i class=\"fas fa-pen me-2\"></i>\n {{ '::Edit' | abpLocalization }}\n </button>\n <button type=\"button\" ngbDropdownItem (click)=\"delete(field)\">\n <i class=\"fas fa-trash me-2\"></i>\n {{ '::Delete' | abpLocalization }}\n </button>\n </div>\n </div>\n }\n </ng-template>\n\n <abp-extensible-table\n [data]=\"fields().items\"\n [recordsTotal]=\"fields().totalCount\"\n [list]=\"list\"\n [actionsTemplate]=\"actionsTemplate\"\n actionsText=\"AbpUi::Actions\"\n [actionsColumnWidth]=\"150\"\n />\n </div>\n </div>\n }\n</div>\n\n<!-- Create / Edit modal -->\n@if (editing(); as mode) {\n <div\n class=\"modal d-block\"\n tabindex=\"-1\"\n style=\"background:rgba(0,0,0,.4);\"\n (mousedown)=\"onBackdropMouseDown($event)\"\n (click)=\"onBackdropClick($event)\"\n >\n <div class=\"modal-dialog modal-dialog-centered modal-lg\">\n <div class=\"modal-content\">\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\">\n <div class=\"modal-header\">\n <h5 class=\"modal-title\">\n <i class=\"fas fa-list-ul me-2\"></i>\n {{ (mode === 'create' ? '::FieldDefinition:New' : '::FieldDefinition:Edit') | abpLocalization }}\n </h5>\n <button type=\"button\" class=\"btn-close\" (click)=\"closeModal()\"></button>\n </div>\n <div class=\"modal-body\">\n <!-- #264: Prompt as the primary input. Admins focus on the extraction prompt, then use AI to draft the remaining editable field metadata. -->\n <div class=\"mb-3\">\n <div class=\"d-flex align-items-center gap-2 mb-1\">\n <label class=\"form-label mb-0\">{{ '::FieldDefinition:Prompt' | abpLocalization }}</label>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-primary ms-auto\"\n (click)=\"draft()\"\n [disabled]=\"!form.controls.prompt.value.trim() || isDrafting()\"\n title=\"{{ '::FieldDefinition:DraftHint' | abpLocalization }}\"\n >\n @if (isDrafting()) {\n <span class=\"spinner-border spinner-border-sm me-1\"></span>\n } @else {\n <i class=\"fas fa-wand-magic-sparkles me-1\"></i>\n }\n {{ '::FieldDefinition:DraftFromPrompt' | abpLocalization }}\n </button>\n </div>\n <textarea\n class=\"form-control\"\n rows=\"3\"\n formControlName=\"prompt\"\n [class.is-invalid]=\"form.controls.prompt.touched && form.controls.prompt.invalid\"\n placeholder=\"{{ '::FieldDefinition:PromptHint' | abpLocalization }}\"\n ></textarea>\n <div class=\"form-text\">{{ '::FieldDefinition:DraftHint' | abpLocalization }}</div>\n </div>\n\n @if (justDrafted()) {\n <div class=\"alert alert-info py-2 px-3 small d-flex align-items-center gap-2\" role=\"alert\">\n <i class=\"fas fa-circle-info\"></i>\n <span>{{ '::FieldDefinition:DraftReviewNotice' | abpLocalization }}</span>\n </div>\n }\n\n <div class=\"row g-3\">\n <div class=\"col-md-6\">\n <label class=\"form-label\">{{ '::FieldDefinition:DisplayName' | abpLocalization }}</label>\n <input\n type=\"text\"\n class=\"form-control\"\n formControlName=\"displayName\"\n (blur)=\"onDisplayNameBlur()\"\n [class.is-invalid]=\"form.controls.displayName.touched && form.controls.displayName.invalid\"\n />\n </div>\n <div class=\"col-md-6\">\n <label class=\"form-label d-flex align-items-center gap-2\">\n <span>{{ '::FieldDefinition:Name' | abpLocalization }}</span>\n @if (isSuggesting()) {\n <span class=\"text-muted small fw-normal\">\n <span class=\"spinner-border spinner-border-sm me-1\" style=\"width:.75rem;height:.75rem;\"></span>\n {{ '::FieldDefinition:NameSuggesting' | abpLocalization }}\n </span>\n }\n </label>\n <input\n type=\"text\"\n class=\"form-control\"\n formControlName=\"name\"\n [class.is-invalid]=\"form.controls.name.touched && form.controls.name.invalid\"\n placeholder=\"party_name\"\n />\n <div class=\"form-text\">{{ '::FieldDefinition:NameHint' | abpLocalization }}</div>\n @if (form.controls.name.touched && form.controls.name.invalid) {\n <div class=\"invalid-feedback d-block\">{{ '::FieldDefinition:NameInvalid' | abpLocalization }}</div>\n }\n </div>\n </div>\n\n <div class=\"row g-3 align-items-start mt-3\">\n <div class=\"col-md-5\">\n <label class=\"form-label\">{{ '::FieldDefinition:DataType' | abpLocalization }}</label>\n <select class=\"form-select\" formControlName=\"dataType\">\n @for (opt of dataTypeOptions; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ '::FieldDataType:' + opt.key | abpLocalization }}</option>\n }\n </select>\n </div>\n <div class=\"col-md-3\">\n <label class=\"form-label\">{{ '::FieldDefinition:DisplayOrder' | abpLocalization }}</label>\n <input type=\"number\" class=\"form-control\" formControlName=\"displayOrder\" step=\"1\" />\n </div>\n <div class=\"col-md-4\">\n <div class=\"form-check mt-2\">\n <input type=\"checkbox\" class=\"form-check-input\" id=\"isRequired\" formControlName=\"isRequired\" />\n <label class=\"form-check-label\" for=\"isRequired\">\n {{ '::FieldDefinition:Required' | abpLocalization }}\n </label>\n </div>\n <!-- #212: Multiple values are valid only for text. Non-text controls are disabled by applyAllowMultiplePolicy, and the persistent hint explains the constraint, matching backend FieldDefinition.MultiValueRequiresStringType. -->\n <div class=\"form-check\">\n <input type=\"checkbox\" class=\"form-check-input\" id=\"allowMultiple\" formControlName=\"allowMultiple\" />\n <label class=\"form-check-label\" for=\"allowMultiple\">\n {{ '::FieldDefinition:AllowMultiple' | abpLocalization }}\n </label>\n </div>\n <div class=\"form-text\">{{ '::FieldDefinition:AllowMultipleHint' | abpLocalization }}</div>\n <!-- #411: unique-key fields form the duplicate-detection fingerprint. -->\n <div class=\"form-check mt-2\">\n <input type=\"checkbox\" class=\"form-check-input\" id=\"isUniqueKey\" formControlName=\"isUniqueKey\" />\n <label class=\"form-check-label\" for=\"isUniqueKey\">\n {{ '::FieldDefinition:IsUniqueKey' | abpLocalization }}\n </label>\n </div>\n <div class=\"form-text\">{{ '::FieldDefinition:IsUniqueKeyHint' | abpLocalization }}</div>\n </div>\n </div>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"closeModal()\">\n {{ '::Cancel' | abpLocalization }}\n </button>\n <button type=\"submit\" class=\"btn btn-primary\" [disabled]=\"form.invalid || isSubmitting()\">\n @if (isSubmitting()) {\n <span class=\"spinner-border spinner-border-sm me-1\"></span>\n }\n {{ '::Save' | abpLocalization }}\n </button>\n </div>\n </form>\n </div>\n </div>\n </div>\n}\n\n<!-- Bulk field re-extraction modal (#289), scoped to the current document type -->\n@if (showReextract()) {\n <lib-field-reextraction-modal\n [documentTypeId]=\"documentTypeId\"\n [documentTypeDisplayName]=\"documentTypeDisplayName()\"\n (closed)=\"showReextract.set(false)\"\n />\n}\n", styles: ["tbody tr:hover{background-color:#00000006}\n"] }]
|
|
527
|
+
}], ctorParameters: () => [] });
|
|
528
|
+
|
|
529
|
+
export { FieldDefinitionListComponent };
|
|
530
|
+
//# sourceMappingURL=dignite-vault-extract-documents-field-definition-list.component-ClmWkRun.mjs.map
|