@genesislcap/foundation-forms 14.397.2 → 14.398.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/dist/custom-elements.json +389 -4
- package/dist/dts/form.d.ts +100 -1
- package/dist/dts/form.d.ts.map +1 -1
- package/dist/dts/form.styles.d.ts.map +1 -1
- package/dist/dts/form.template.d.ts.map +1 -1
- package/dist/dts/jsonforms/json-forms.d.ts +13 -0
- package/dist/dts/jsonforms/json-forms.d.ts.map +1 -1
- package/dist/dts/jsonforms/renderers/ArrayListWrapperRenderer.d.ts +5 -0
- package/dist/dts/jsonforms/renderers/ArrayListWrapperRenderer.d.ts.map +1 -1
- package/dist/dts/jsonforms/renderers/BooleanControlRenderer.d.ts.map +1 -1
- package/dist/dts/jsonforms/renderers/ConnectedMultiselectControlRenderer.d.ts.map +1 -1
- package/dist/dts/jsonforms/renderers/ControlWrapperRenderer.d.ts.map +1 -1
- package/dist/dts/jsonforms/renderers/EnumControlRenderer.d.ts.map +1 -1
- package/dist/dts/jsonforms/renderers/LayoutFormGridRenderer.d.ts +3 -0
- package/dist/dts/jsonforms/renderers/LayoutFormGridRenderer.d.ts.map +1 -0
- package/dist/dts/jsonforms/renderers/RenderersRanks.d.ts +1 -0
- package/dist/dts/jsonforms/renderers/RenderersRanks.d.ts.map +1 -1
- package/dist/dts/jsonforms/testers/isOneOfOptionMultiselect.d.ts.map +1 -1
- package/dist/dts/types.d.ts +89 -2
- package/dist/dts/types.d.ts.map +1 -1
- package/dist/dts/utils/csv-parser.d.ts +85 -0
- package/dist/dts/utils/csv-parser.d.ts.map +1 -0
- package/dist/dts/utils/index.d.ts +1 -0
- package/dist/dts/utils/index.d.ts.map +1 -1
- package/dist/dts/utils/schema-utils.d.ts +46 -0
- package/dist/dts/utils/schema-utils.d.ts.map +1 -0
- package/dist/dts/utils/validation.d.ts +2 -0
- package/dist/dts/utils/validation.d.ts.map +1 -1
- package/dist/esm/form.js +423 -5
- package/dist/esm/form.styles.js +41 -1
- package/dist/esm/form.template.js +33 -1
- package/dist/esm/jsonforms/json-forms.js +30 -0
- package/dist/esm/jsonforms/renderers/ArrayListWrapperRenderer.js +223 -22
- package/dist/esm/jsonforms/renderers/BooleanControlRenderer.js +1 -2
- package/dist/esm/jsonforms/renderers/ConnectedMultiselectControlRenderer.js +13 -2
- package/dist/esm/jsonforms/renderers/ControlWrapperRenderer.js +25 -4
- package/dist/esm/jsonforms/renderers/EnumControlRenderer.js +14 -5
- package/dist/esm/jsonforms/renderers/LayoutFormGridRenderer.js +39 -0
- package/dist/esm/jsonforms/renderers/RenderersRanks.js +1 -0
- package/dist/esm/jsonforms/testers/isOneOfOptionMultiselect.js +1 -1
- package/dist/esm/utils/csv-parser.js +486 -0
- package/dist/esm/utils/index.js +1 -0
- package/dist/esm/utils/schema-utils.js +120 -0
- package/dist/esm/utils/validation.js +2 -0
- package/dist/foundation-forms.api.json +1028 -34
- package/dist/foundation-forms.d.ts +285 -2
- package/docs/api/foundation-forms.arrayrendereroptions.md +2 -2
- package/docs/api/foundation-forms.bulkrowstatus.md +22 -0
- package/docs/api/foundation-forms.bulkrowsubmitstatus.md +13 -0
- package/docs/api/foundation-forms.bulksubmitfaileditem.md +20 -0
- package/docs/api/foundation-forms.bulksubmitresult.md +18 -0
- package/docs/api/foundation-forms.bulksubmitsuccessitem.md +17 -0
- package/docs/api/foundation-forms.childuischemaresolver.md +15 -0
- package/docs/api/foundation-forms.csvmappingresult.mappedrows.md +13 -0
- package/docs/api/foundation-forms.csvmappingresult.md +77 -0
- package/docs/api/foundation-forms.csvmappingresult.unmappedcolumns.md +13 -0
- package/docs/api/foundation-forms.csvparseresult.errors.md +13 -0
- package/docs/api/foundation-forms.csvparseresult.headers.md +13 -0
- package/docs/api/foundation-forms.csvparseresult.md +96 -0
- package/docs/api/foundation-forms.csvparseresult.rows.md +13 -0
- package/docs/api/foundation-forms.downloadcsvtemplate.md +74 -0
- package/docs/api/foundation-forms.form.bulkinsert.md +13 -0
- package/docs/api/foundation-forms.form.bulkinsertmaxitems.md +13 -0
- package/docs/api/foundation-forms.form.bulkinsertminitems.md +13 -0
- package/docs/api/foundation-forms.form.clearrowsubmitstatuses.md +17 -0
- package/docs/api/foundation-forms.form.downloadcsvtemplate.md +17 -0
- package/docs/api/foundation-forms.form.handlecsvfileselected.md +54 -0
- package/docs/api/foundation-forms.form.md +132 -0
- package/docs/api/foundation-forms.form.rowsubmitstatuses.md +13 -0
- package/docs/api/foundation-forms.form.submitsinglerow.md +56 -0
- package/docs/api/foundation-forms.generatecsvtemplate.md +104 -0
- package/docs/api/foundation-forms.mapcsvtoschema.md +88 -0
- package/docs/api/foundation-forms.md +147 -0
- package/docs/api/foundation-forms.parsecsv.md +56 -0
- package/docs/api/foundation-forms.uischemaelementtype.md +1 -1
- package/docs/api-report.md.api.md +87 -4
- package/package.json +19 -17
package/dist/esm/form.js
CHANGED
|
@@ -20,13 +20,15 @@ import { LayoutGroupRendererEntry } from './jsonforms/renderers/LayoutGroupRende
|
|
|
20
20
|
import { LayoutHorizontalEntry } from './jsonforms/renderers/LayoutHorizontalRenderer';
|
|
21
21
|
import { LayoutRendererEntry } from './jsonforms/renderers/LayoutRenderer';
|
|
22
22
|
import { LayoutStepperRendererEntry } from './jsonforms/renderers/LayoutStepperRenderer';
|
|
23
|
+
import { LayoutFormGridEntry } from './jsonforms/renderers/LayoutFormGridRenderer';
|
|
23
24
|
import { LayoutVertical2ColumnsEntry } from './jsonforms/renderers/LayoutVertical2ColumnsRenderer';
|
|
24
25
|
import { NumberControlRendererEntry } from './jsonforms/renderers/NumberControlRenderer';
|
|
25
26
|
import { StringArrayEntry } from './jsonforms/renderers/StringArrayControlRenderer';
|
|
26
27
|
import { StringControlRendererTemplate } from './jsonforms/renderers/StringControlRenderer';
|
|
27
|
-
import { logger } from './utils';
|
|
28
|
+
import { downloadCsvTemplate as downloadCsvFile, extractFieldsFromUiSchema, generateCsvTemplate, logger, mapCsvToSchema, parseCsv, } from './utils';
|
|
28
29
|
import { findModalParent, showConfirmationDialog } from './utils/confirmation-dialog-utils';
|
|
29
30
|
import { removeDataPropertiesNotInSchema } from './utils/form-utils';
|
|
31
|
+
import { generateBulkUiSchema, isBulkUiSchema } from './utils/schema-utils';
|
|
30
32
|
const stringEntry = {
|
|
31
33
|
renderer: StringControlRendererTemplate,
|
|
32
34
|
tester: rankWith(2, isStringControl),
|
|
@@ -50,6 +52,7 @@ export const renderers = [
|
|
|
50
52
|
LayoutCategorizationRendererEntry,
|
|
51
53
|
LayoutGroupRendererEntry,
|
|
52
54
|
LayoutHorizontalEntry,
|
|
55
|
+
LayoutFormGridEntry,
|
|
53
56
|
LayoutVertical2ColumnsEntry,
|
|
54
57
|
LayoutRendererEntry,
|
|
55
58
|
LayoutStepperRendererEntry,
|
|
@@ -113,11 +116,23 @@ let Form = class Form extends LifecycleMixin(FoundationElement) {
|
|
|
113
116
|
* @public
|
|
114
117
|
*/
|
|
115
118
|
this.data = {};
|
|
119
|
+
/**
|
|
120
|
+
* Minimum number of items required in bulk insert mode.
|
|
121
|
+
* @public
|
|
122
|
+
*/
|
|
123
|
+
this.bulkInsertMinItems = 1;
|
|
124
|
+
/**
|
|
125
|
+
* Tracks the submission status for each row in bulk insert mode.
|
|
126
|
+
* Key is the row index, value is the status object.
|
|
127
|
+
* @public
|
|
128
|
+
*/
|
|
129
|
+
this.rowSubmitStatuses = new Map();
|
|
116
130
|
}
|
|
117
131
|
resourceNameChanged() {
|
|
118
132
|
return __awaiter(this, void 0, void 0, function* () {
|
|
119
133
|
var _a, _b;
|
|
120
134
|
this.jsonSchema = undefined;
|
|
135
|
+
this.originalDetailsSchema = undefined;
|
|
121
136
|
if (this.resourceName) {
|
|
122
137
|
const jsonSchemaResponse = yield this.connect.getJSONSchema(this.resourceName);
|
|
123
138
|
if (!jsonSchemaResponse) {
|
|
@@ -126,6 +141,8 @@ let Form = class Form extends LifecycleMixin(FoundationElement) {
|
|
|
126
141
|
const refResolver = new JsonSchemaDereferencer(jsonSchemaResponse.INBOUND);
|
|
127
142
|
const detailsSchema = (_a = (yield refResolver.resolve()).properties) === null || _a === void 0 ? void 0 : _a.DETAILS;
|
|
128
143
|
const approvalMessageSchema = (_b = (yield refResolver.resolve()).properties) === null || _b === void 0 ? void 0 : _b.APPROVAL_MESSAGE;
|
|
144
|
+
// Store the original details schema for bulk insert transformations
|
|
145
|
+
this.originalDetailsSchema = detailsSchema;
|
|
129
146
|
// If setApprovalMessage is enabled, wrap the DETAILS schema with APPROVAL_MESSAGE field
|
|
130
147
|
if (this.setApprovalMessage && detailsSchema) {
|
|
131
148
|
this.jsonSchema = Object.assign(Object.assign({}, detailsSchema), { properties: Object.assign(Object.assign({}, detailsSchema.properties), { APPROVAL_MESSAGE: approvalMessageSchema }) });
|
|
@@ -133,9 +150,65 @@ let Form = class Form extends LifecycleMixin(FoundationElement) {
|
|
|
133
150
|
else {
|
|
134
151
|
this.jsonSchema = detailsSchema;
|
|
135
152
|
}
|
|
153
|
+
// Transform schema for bulk insert mode
|
|
154
|
+
if (this.bulkInsert && this.jsonSchema) {
|
|
155
|
+
this.transformSchemaForBulkInsert();
|
|
156
|
+
// Initialize data after a DOM update to ensure schema is applied first
|
|
157
|
+
yield DOM.nextUpdate();
|
|
158
|
+
this.initializeBulkInsertData();
|
|
159
|
+
}
|
|
136
160
|
}
|
|
137
161
|
});
|
|
138
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Initializes the data with default empty items for bulk insert mode.
|
|
165
|
+
* @internal
|
|
166
|
+
*/
|
|
167
|
+
initializeBulkInsertData() {
|
|
168
|
+
var _a;
|
|
169
|
+
// Only initialize if data doesn't already have items
|
|
170
|
+
if (!((_a = this.data) === null || _a === void 0 ? void 0 : _a.items) || this.data.items.length === 0) {
|
|
171
|
+
const initialItems = [];
|
|
172
|
+
// Add the minimum number of items (default is 1)
|
|
173
|
+
for (let i = 0; i < this.bulkInsertMinItems; i += 1) {
|
|
174
|
+
initialItems.push({});
|
|
175
|
+
}
|
|
176
|
+
// Set data with new reference to trigger reactivity
|
|
177
|
+
this.data = Object.assign(Object.assign({}, this.data), { items: initialItems });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Transforms the JSON schema to support bulk insert mode by wrapping it in an array.
|
|
182
|
+
* @internal
|
|
183
|
+
*/
|
|
184
|
+
transformSchemaForBulkInsert() {
|
|
185
|
+
if (!this.originalDetailsSchema) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const arraySchema = {
|
|
189
|
+
type: 'array',
|
|
190
|
+
items: this.originalDetailsSchema,
|
|
191
|
+
minItems: this.bulkInsertMinItems,
|
|
192
|
+
};
|
|
193
|
+
if (this.bulkInsertMaxItems !== undefined) {
|
|
194
|
+
arraySchema.maxItems = this.bulkInsertMaxItems;
|
|
195
|
+
}
|
|
196
|
+
this.jsonSchema = {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {
|
|
199
|
+
items: arraySchema,
|
|
200
|
+
},
|
|
201
|
+
required: ['items'],
|
|
202
|
+
};
|
|
203
|
+
// Store user-provided UI schema and generate bulk UI schema if not provided
|
|
204
|
+
if (!this.userProvidedUiSchema) {
|
|
205
|
+
this.userProvidedUiSchema = this.uischema;
|
|
206
|
+
}
|
|
207
|
+
// Generate bulk UI schema if user hasn't provided one
|
|
208
|
+
if (!this.uischema || !isBulkUiSchema(this.uischema)) {
|
|
209
|
+
this.uischema = generateBulkUiSchema(this.userProvidedUiSchema);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
139
212
|
/**
|
|
140
213
|
* @internal
|
|
141
214
|
*/
|
|
@@ -176,6 +249,11 @@ let Form = class Form extends LifecycleMixin(FoundationElement) {
|
|
|
176
249
|
yield DOM.nextUpdate();
|
|
177
250
|
}
|
|
178
251
|
this.submitted = true;
|
|
252
|
+
// Route to bulk submit if in bulk insert mode
|
|
253
|
+
if (this.bulkInsert) {
|
|
254
|
+
yield this._submitBulk();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
179
257
|
const payload = Object.assign({}, this.data);
|
|
180
258
|
const commitPayload = this.buildCommitPayload(payload);
|
|
181
259
|
logger.debug({ payload, errors: this.errors });
|
|
@@ -202,16 +280,211 @@ let Form = class Form extends LifecycleMixin(FoundationElement) {
|
|
|
202
280
|
}
|
|
203
281
|
});
|
|
204
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Handles bulk insert submission by iterating through items and submitting each separately.
|
|
285
|
+
* Updates rowSubmitStatuses to provide row-level feedback.
|
|
286
|
+
* @internal
|
|
287
|
+
*/
|
|
288
|
+
_submitBulk() {
|
|
289
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
290
|
+
var _a;
|
|
291
|
+
const items = ((_a = this.data) === null || _a === void 0 ? void 0 : _a.items) || [];
|
|
292
|
+
// Validate minimum items
|
|
293
|
+
if (items.length < this.bulkInsertMinItems) {
|
|
294
|
+
const error = {
|
|
295
|
+
CODE: 'BULK_INSERT_MIN_ITEMS',
|
|
296
|
+
TEXT: `At least ${this.bulkInsertMinItems} item(s) required`,
|
|
297
|
+
};
|
|
298
|
+
this.$emit('submit-failure', { payload: this.data, errors: [error] });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
// Validate maximum items if set
|
|
302
|
+
if (this.bulkInsertMaxItems !== undefined && items.length > this.bulkInsertMaxItems) {
|
|
303
|
+
const error = {
|
|
304
|
+
CODE: 'BULK_INSERT_MAX_ITEMS',
|
|
305
|
+
TEXT: `Maximum ${this.bulkInsertMaxItems} item(s) allowed`,
|
|
306
|
+
};
|
|
307
|
+
this.$emit('submit-failure', { payload: this.data, errors: [error] });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
logger.debug({ bulkItems: items, errors: this.errors });
|
|
311
|
+
this.$emit('submit', { payload: this.data, errors: this.errors });
|
|
312
|
+
if (items.length === 0 || this.errors.length || !this.resourceName) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
this.submitting = true;
|
|
316
|
+
// Initialize rows as pending, but preserve already successful rows
|
|
317
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
318
|
+
const currentStatus = this.rowSubmitStatuses.get(i);
|
|
319
|
+
// Don't reset rows that were already successfully submitted
|
|
320
|
+
if ((currentStatus === null || currentStatus === void 0 ? void 0 : currentStatus.status) !== 'success') {
|
|
321
|
+
this.rowSubmitStatuses.set(i, { status: 'pending' });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Trigger reactivity by reassigning and emit event
|
|
325
|
+
this.rowSubmitStatuses = new Map(this.rowSubmitStatuses);
|
|
326
|
+
this.$emit('row-status-changed', { statuses: this.rowSubmitStatuses });
|
|
327
|
+
const results = {
|
|
328
|
+
successful: [],
|
|
329
|
+
failed: [],
|
|
330
|
+
};
|
|
331
|
+
// Submit each item sequentially, skipping already successful rows
|
|
332
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
333
|
+
// Skip rows that were already successfully submitted
|
|
334
|
+
const currentStatus = this.rowSubmitStatuses.get(i);
|
|
335
|
+
if ((currentStatus === null || currentStatus === void 0 ? void 0 : currentStatus.status) === 'success') {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const item = items[i];
|
|
339
|
+
// Filter data to only include fields from UI schema
|
|
340
|
+
const commitPayload = this.buildCommitPayload(item, true);
|
|
341
|
+
// Update status to submitting
|
|
342
|
+
this.updateRowStatus(i, { status: 'submitting' });
|
|
343
|
+
try {
|
|
344
|
+
// eslint-disable-next-line no-await-in-loop
|
|
345
|
+
const response = yield this.connect.commitEvent(this.resourceName, commitPayload);
|
|
346
|
+
if (response.ERROR) {
|
|
347
|
+
results.failed.push({ item, index: i, errors: response.ERROR });
|
|
348
|
+
this.updateRowStatus(i, { status: 'failed', errors: response.ERROR });
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
results.successful.push({ item, index: i, response });
|
|
352
|
+
this.updateRowStatus(i, { status: 'success', response });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
const errors = [{ CODE: 'SUBMIT_ERROR', TEXT: String(error) }];
|
|
357
|
+
results.failed.push({ item, index: i, errors });
|
|
358
|
+
this.updateRowStatus(i, { status: 'failed', errors });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
this.submitting = false;
|
|
362
|
+
// Emit bulk-specific event with aggregated results
|
|
363
|
+
this.$emit('bulk-submit-complete', results);
|
|
364
|
+
// Also emit standard events based on overall result
|
|
365
|
+
if (results.failed.length === 0) {
|
|
366
|
+
this.$emit('submit-success', { payload: this.data, results });
|
|
367
|
+
}
|
|
368
|
+
else if (results.successful.length === 0) {
|
|
369
|
+
this.$emit('submit-failure', {
|
|
370
|
+
payload: this.data,
|
|
371
|
+
errors: results.failed.flatMap((f) => f.errors),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
// Partial success - emit both events
|
|
376
|
+
this.$emit('submit-partial-success', { payload: this.data, results });
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Updates the submit status for a specific row and triggers reactivity.
|
|
382
|
+
* @param index - The row index
|
|
383
|
+
* @param status - The new status object
|
|
384
|
+
* @internal
|
|
385
|
+
*/
|
|
386
|
+
updateRowStatus(index, status) {
|
|
387
|
+
this.rowSubmitStatuses.set(index, status);
|
|
388
|
+
// Create a new Map to trigger FAST Element reactivity
|
|
389
|
+
this.rowSubmitStatuses = new Map(this.rowSubmitStatuses);
|
|
390
|
+
// Emit event for child components to update
|
|
391
|
+
this.$emit('row-status-changed', { statuses: this.rowSubmitStatuses, index, status });
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Clears all row submit statuses, typically called when resetting the form.
|
|
395
|
+
* @public
|
|
396
|
+
*/
|
|
397
|
+
clearRowSubmitStatuses() {
|
|
398
|
+
this.rowSubmitStatuses = new Map();
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Submits a single row in bulk insert mode.
|
|
402
|
+
* @param index - The index of the row to submit
|
|
403
|
+
* @returns Promise that resolves when submission is complete
|
|
404
|
+
* @public
|
|
405
|
+
*/
|
|
406
|
+
submitSingleRow(index) {
|
|
407
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
408
|
+
var _a;
|
|
409
|
+
if (!this.bulkInsert) {
|
|
410
|
+
logger.warn('submitSingleRow called but bulkInsert is not enabled');
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const items = ((_a = this.data) === null || _a === void 0 ? void 0 : _a.items) || [];
|
|
414
|
+
const item = items[index];
|
|
415
|
+
if (!item) {
|
|
416
|
+
logger.warn(`submitSingleRow: No item found at index ${index}`);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
// Check if already submitted successfully
|
|
420
|
+
const currentStatus = this.rowSubmitStatuses.get(index);
|
|
421
|
+
if ((currentStatus === null || currentStatus === void 0 ? void 0 : currentStatus.status) === 'success') {
|
|
422
|
+
logger.debug(`Row ${index} already submitted successfully`);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (!this.resourceName) {
|
|
426
|
+
logger.warn('submitSingleRow: No resourceName configured');
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
// Update status to submitting
|
|
430
|
+
this.updateRowStatus(index, { status: 'submitting' });
|
|
431
|
+
// Filter data to only include fields from UI schema
|
|
432
|
+
const commitPayload = this.buildCommitPayload(item, true);
|
|
433
|
+
try {
|
|
434
|
+
const response = yield this.connect.commitEvent(this.resourceName, commitPayload);
|
|
435
|
+
if (response.ERROR) {
|
|
436
|
+
this.updateRowStatus(index, { status: 'failed', errors: response.ERROR });
|
|
437
|
+
this.$emit('row-submit-failure', { index, item, errors: response.ERROR });
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
this.updateRowStatus(index, { status: 'success', response });
|
|
441
|
+
this.$emit('row-submit-success', { index, item, response });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
const errors = [{ CODE: 'SUBMIT_ERROR', TEXT: String(error) }];
|
|
446
|
+
this.updateRowStatus(index, { status: 'failed', errors });
|
|
447
|
+
this.$emit('row-submit-failure', { index, item, errors });
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Filters item data to only include fields defined in the UI schema.
|
|
453
|
+
* This ensures we don't submit default values for fields not shown in the UI.
|
|
454
|
+
* @param item - The item data to filter
|
|
455
|
+
* @returns Filtered item data containing only UI schema fields
|
|
456
|
+
* @internal
|
|
457
|
+
*/
|
|
458
|
+
filterDataByUiSchema(item) {
|
|
459
|
+
if (!this.userProvidedUiSchema) {
|
|
460
|
+
return item;
|
|
461
|
+
}
|
|
462
|
+
const uiSchemaFields = extractFieldsFromUiSchema(this.userProvidedUiSchema);
|
|
463
|
+
// Get only visible (non-hidden) field names
|
|
464
|
+
const visibleFieldNames = uiSchemaFields.filter((f) => !f.isHidden).map((f) => f.fieldName);
|
|
465
|
+
if (visibleFieldNames.length === 0) {
|
|
466
|
+
return item;
|
|
467
|
+
}
|
|
468
|
+
// Filter the item to only include fields from the UI schema
|
|
469
|
+
const filteredItem = {};
|
|
470
|
+
for (const fieldName of visibleFieldNames) {
|
|
471
|
+
if (fieldName in item) {
|
|
472
|
+
filteredItem[fieldName] = item[fieldName];
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return filteredItem;
|
|
476
|
+
}
|
|
205
477
|
/**
|
|
206
478
|
* Builds the commit payload for the form submission.
|
|
207
479
|
* @internal
|
|
208
480
|
*/
|
|
209
|
-
buildCommitPayload(data) {
|
|
481
|
+
buildCommitPayload(data, filterByUiSchema = false) {
|
|
482
|
+
const details = filterByUiSchema ? this.filterDataByUiSchema(data) : data;
|
|
210
483
|
if (this.setApprovalMessage) {
|
|
211
|
-
const { APPROVAL_MESSAGE } =
|
|
212
|
-
return { DETAILS:
|
|
484
|
+
const { APPROVAL_MESSAGE } = details, rest = __rest(details, ["APPROVAL_MESSAGE"]);
|
|
485
|
+
return { DETAILS: rest, APPROVAL_MESSAGE };
|
|
213
486
|
}
|
|
214
|
-
return { DETAILS:
|
|
487
|
+
return { DETAILS: details };
|
|
215
488
|
}
|
|
216
489
|
/**
|
|
217
490
|
* Controls the visibility of the submit button.
|
|
@@ -256,6 +529,8 @@ let Form = class Form extends LifecycleMixin(FoundationElement) {
|
|
|
256
529
|
if (event.detail.additionalErrors.length) {
|
|
257
530
|
this.errors = [...this.errors, ...event.detail.additionalErrors];
|
|
258
531
|
}
|
|
532
|
+
// Re-emit so parent components can react to form data changes (e.g. for dynamic criteria)
|
|
533
|
+
this.$emit('data-change', event.detail);
|
|
259
534
|
}
|
|
260
535
|
}
|
|
261
536
|
/**
|
|
@@ -271,6 +546,7 @@ let Form = class Form extends LifecycleMixin(FoundationElement) {
|
|
|
271
546
|
*/
|
|
272
547
|
reset(clearData = true) {
|
|
273
548
|
this.submitted = false;
|
|
549
|
+
this.clearRowSubmitStatuses();
|
|
274
550
|
if (clearData) {
|
|
275
551
|
this.data = {};
|
|
276
552
|
}
|
|
@@ -328,6 +604,124 @@ let Form = class Form extends LifecycleMixin(FoundationElement) {
|
|
|
328
604
|
}
|
|
329
605
|
});
|
|
330
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* Handles CSV file selection for bulk import.
|
|
609
|
+
* Parses the CSV content and appends it to existing form items.
|
|
610
|
+
* @param event - The file input change event
|
|
611
|
+
* @public
|
|
612
|
+
*/
|
|
613
|
+
handleCsvFileSelected(event) {
|
|
614
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
615
|
+
var _a, _b;
|
|
616
|
+
const file = (_a = event.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
617
|
+
if (!file) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
const content = yield file.text();
|
|
622
|
+
const parseResult = parseCsv(content);
|
|
623
|
+
if (parseResult.errors.length > 0) {
|
|
624
|
+
this.$emit('csv-parse-error', { errors: parseResult.errors, fileName: file.name });
|
|
625
|
+
logger.warn('CSV parse errors:', parseResult.errors);
|
|
626
|
+
this.clearCsvFileInput();
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (parseResult.rows.length === 0) {
|
|
630
|
+
this.$emit('csv-parse-error', {
|
|
631
|
+
errors: ['CSV file contains no data rows'],
|
|
632
|
+
fileName: file.name,
|
|
633
|
+
});
|
|
634
|
+
this.clearCsvFileInput();
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
// Map CSV data to schema fields (uiSchema enables label->field mapping for import)
|
|
638
|
+
const { mappedRows, unmappedColumns } = mapCsvToSchema(parseResult.rows, this.originalDetailsSchema, this.uischema);
|
|
639
|
+
if (mappedRows.length === 0) {
|
|
640
|
+
this.$emit('csv-parse-error', {
|
|
641
|
+
errors: ['No CSV columns could be matched to form fields'],
|
|
642
|
+
fileName: file.name,
|
|
643
|
+
});
|
|
644
|
+
this.clearCsvFileInput();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
// Check if importing would exceed maximum items
|
|
648
|
+
const existingItems = ((_b = this.data) === null || _b === void 0 ? void 0 : _b.items) || [];
|
|
649
|
+
let rowsToImport = mappedRows;
|
|
650
|
+
if (this.bulkInsertMaxItems !== undefined) {
|
|
651
|
+
const availableSlots = this.bulkInsertMaxItems - existingItems.length;
|
|
652
|
+
if (availableSlots <= 0) {
|
|
653
|
+
this.$emit('csv-parse-error', {
|
|
654
|
+
errors: [`Maximum of ${this.bulkInsertMaxItems} items already reached`],
|
|
655
|
+
fileName: file.name,
|
|
656
|
+
});
|
|
657
|
+
this.clearCsvFileInput();
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (mappedRows.length > availableSlots) {
|
|
661
|
+
rowsToImport = mappedRows.slice(0, availableSlots);
|
|
662
|
+
logger.warn(`Truncated CSV import from ${mappedRows.length} to ${availableSlots} rows due to max items limit`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// Append to existing items
|
|
666
|
+
this.data = { items: [...existingItems, ...rowsToImport] };
|
|
667
|
+
this.$emit('csv-imported', {
|
|
668
|
+
rowCount: rowsToImport.length,
|
|
669
|
+
totalRowsInFile: mappedRows.length,
|
|
670
|
+
unmappedColumns,
|
|
671
|
+
fileName: file.name,
|
|
672
|
+
});
|
|
673
|
+
logger.debug('CSV imported successfully', {
|
|
674
|
+
rowCount: rowsToImport.length,
|
|
675
|
+
unmappedColumns,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
this.$emit('csv-parse-error', {
|
|
680
|
+
errors: [`Failed to read CSV file: ${String(error)}`],
|
|
681
|
+
fileName: file.name,
|
|
682
|
+
});
|
|
683
|
+
logger.error('CSV import error:', error);
|
|
684
|
+
}
|
|
685
|
+
this.clearCsvFileInput();
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Clears the CSV file input to allow re-selection of the same file.
|
|
690
|
+
* @internal
|
|
691
|
+
*/
|
|
692
|
+
clearCsvFileInput() {
|
|
693
|
+
if (this.csvFileInput) {
|
|
694
|
+
this.csvFileInput.value = '';
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Downloads a CSV template file with headers and sample data based on the schema.
|
|
699
|
+
* If a UI schema is provided, it will be used to determine which fields to include
|
|
700
|
+
* and in what order. Hidden fields will be excluded from the template.
|
|
701
|
+
* @public
|
|
702
|
+
*/
|
|
703
|
+
downloadCsvTemplate() {
|
|
704
|
+
if (!this.originalDetailsSchema) {
|
|
705
|
+
logger.warn('Cannot download CSV template: schema not available');
|
|
706
|
+
this.$emit('csv-template-error', { error: 'Schema not available' });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
// Use UI schema to determine which columns to include (only fields in uischema, not all schema fields)
|
|
710
|
+
const csvContent = generateCsvTemplate(this.originalDetailsSchema, this.uischema);
|
|
711
|
+
if (!csvContent) {
|
|
712
|
+
logger.warn('Cannot download CSV template: no fields in schema');
|
|
713
|
+
this.$emit('csv-template-error', { error: 'No fields found in schema' });
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
// Generate filename based on resource name or use default
|
|
717
|
+
const baseName = this.resourceName
|
|
718
|
+
? this.resourceName.replace(/^EVENT_/, '').toLowerCase()
|
|
719
|
+
: 'bulk_insert';
|
|
720
|
+
const fileName = `${baseName}_template.csv`;
|
|
721
|
+
downloadCsvFile(csvContent, fileName);
|
|
722
|
+
this.$emit('csv-template-downloaded', { fileName });
|
|
723
|
+
logger.debug('CSV template downloaded:', fileName);
|
|
724
|
+
}
|
|
331
725
|
};
|
|
332
726
|
__decorate([
|
|
333
727
|
attr({ attribute: 'design-system-prefix' })
|
|
@@ -380,6 +774,30 @@ __decorate([
|
|
|
380
774
|
__decorate([
|
|
381
775
|
attr({ attribute: 'hide-submit-button', mode: 'boolean' })
|
|
382
776
|
], Form.prototype, "hideSubmit", void 0);
|
|
777
|
+
__decorate([
|
|
778
|
+
attr({ attribute: 'bulk-insert', mode: 'boolean' })
|
|
779
|
+
], Form.prototype, "bulkInsert", void 0);
|
|
780
|
+
__decorate([
|
|
781
|
+
attr({
|
|
782
|
+
attribute: 'bulk-insert-min-items',
|
|
783
|
+
converter: {
|
|
784
|
+
fromView: (v) => parseInt(v, 10) || 1,
|
|
785
|
+
toView: (v) => String(v),
|
|
786
|
+
},
|
|
787
|
+
})
|
|
788
|
+
], Form.prototype, "bulkInsertMinItems", void 0);
|
|
789
|
+
__decorate([
|
|
790
|
+
attr({
|
|
791
|
+
attribute: 'bulk-insert-max-items',
|
|
792
|
+
converter: {
|
|
793
|
+
fromView: (v) => (v ? parseInt(v, 10) : undefined),
|
|
794
|
+
toView: (v) => (v !== undefined ? String(v) : ''),
|
|
795
|
+
},
|
|
796
|
+
})
|
|
797
|
+
], Form.prototype, "bulkInsertMaxItems", void 0);
|
|
798
|
+
__decorate([
|
|
799
|
+
observable
|
|
800
|
+
], Form.prototype, "rowSubmitStatuses", void 0);
|
|
383
801
|
__decorate([
|
|
384
802
|
volatile
|
|
385
803
|
], Form.prototype, "isSubmitHidden", null);
|
package/dist/esm/form.styles.js
CHANGED
|
@@ -25,6 +25,10 @@ export const foundationFormStyles = css `
|
|
|
25
25
|
box-sizing: border-box;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
.form-grid-compact {
|
|
29
|
+
font-size: 0.9em;
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
.form-controls {
|
|
29
33
|
display: grid;
|
|
30
34
|
grid-gap: calc(${designUnit} * 1px);
|
|
@@ -67,9 +71,45 @@ export const foundationFormStyles = css `
|
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
.submit-button {
|
|
70
|
-
width: 70px;
|
|
71
74
|
margin: 0;
|
|
72
75
|
}
|
|
76
|
+
|
|
77
|
+
.csv-upload-section {
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
gap: calc(${designUnit} * 3px);
|
|
81
|
+
padding: calc(${designUnit} * 1px) 0;
|
|
82
|
+
margin-bottom: calc(${designUnit} * 2px);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.csv-file-input {
|
|
86
|
+
display: none;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Card-like styling for CSV buttons - matches array item aesthetic */
|
|
90
|
+
.csv-template-button,
|
|
91
|
+
.csv-import-button {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: calc(${designUnit} * 1px);
|
|
95
|
+
padding: calc(${designUnit} * 2px) calc(${designUnit} * 3px);
|
|
96
|
+
background-color: var(--neutral-layer-1, #fff);
|
|
97
|
+
border: 1px solid var(--neutral-stroke-rest, rgb(0 0 0 / 12%));
|
|
98
|
+
border-radius: calc(${designUnit} * 2px);
|
|
99
|
+
box-shadow:
|
|
100
|
+
0 1px 2px rgb(0 0 0 / 4%),
|
|
101
|
+
0 4px 8px rgb(0 0 0 / 6%);
|
|
102
|
+
transition:
|
|
103
|
+
box-shadow 0.2s ease,
|
|
104
|
+
border-color 0.15s ease;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.csv-template-button:hover,
|
|
108
|
+
.csv-import-button:hover {
|
|
109
|
+
box-shadow:
|
|
110
|
+
0 2px 4px rgb(0 0 0 / 6%),
|
|
111
|
+
0 6px 12px rgb(0 0 0 / 8%);
|
|
112
|
+
}
|
|
73
113
|
`.withBehaviors(forcedColorsStylesheetBehavior(css `
|
|
74
114
|
:host {
|
|
75
115
|
background: ${SystemColors.Canvas};
|
|
@@ -10,9 +10,39 @@ avoidTreeShaking(JSONForms, ArrayListWrapper, CategorizationWrapper, ControlWrap
|
|
|
10
10
|
/** @internal */
|
|
11
11
|
export const getPrefixedForm = (prefix) => html `
|
|
12
12
|
<template>
|
|
13
|
+
${when((x) => x.bulkInsert, html `
|
|
14
|
+
<div class="csv-upload-section" part="csv-upload">
|
|
15
|
+
<input
|
|
16
|
+
type="file"
|
|
17
|
+
accept=".csv,text/csv"
|
|
18
|
+
${ref('csvFileInput')}
|
|
19
|
+
@change=${(x, c) => x.handleCsvFileSelected(c.event)}
|
|
20
|
+
class="csv-file-input"
|
|
21
|
+
/>
|
|
22
|
+
<${prefix}-button
|
|
23
|
+
appearance="lightweight"
|
|
24
|
+
@click=${(x) => x.downloadCsvTemplate()}
|
|
25
|
+
class="csv-template-button"
|
|
26
|
+
data-test-id="csv-template-button"
|
|
27
|
+
>
|
|
28
|
+
<${prefix}-icon name="file-arrow-down"></${prefix}-icon>
|
|
29
|
+
Download Template
|
|
30
|
+
</${prefix}-button>
|
|
31
|
+
<${prefix}-button
|
|
32
|
+
appearance="lightweight"
|
|
33
|
+
@click=${(x) => { var _a; return (_a = x.csvFileInput) === null || _a === void 0 ? void 0 : _a.click(); }}
|
|
34
|
+
class="csv-import-button"
|
|
35
|
+
data-test-id="csv-import-button"
|
|
36
|
+
>
|
|
37
|
+
<${prefix}-icon name="file-arrow-up"></${prefix}-icon>
|
|
38
|
+
Import CSV
|
|
39
|
+
</${prefix}-button>
|
|
40
|
+
</div>
|
|
41
|
+
`)}
|
|
13
42
|
<json-forms
|
|
14
43
|
@submit-button-clicked=${(x) => x._submit()}
|
|
15
44
|
@submit-part=${(x, c) => x.submitPart(c.event)}
|
|
45
|
+
@submit-single-row=${(x, c) => x.submitSingleRow(c.event.detail.index)}
|
|
16
46
|
@reset-form=${(x) => x.reset(false)}
|
|
17
47
|
?readonly=${(x) => x.readonly}
|
|
18
48
|
?submitted=${(x) => x.submitted}
|
|
@@ -21,6 +51,8 @@ export const getPrefixedForm = (prefix) => html `
|
|
|
21
51
|
:schema=${(x) => x.jsonSchema}
|
|
22
52
|
:data=${(x) => x.data}
|
|
23
53
|
:prefix=${(x) => x.prefix}
|
|
54
|
+
:rowSubmitStatuses=${(x) => x.rowSubmitStatuses}
|
|
55
|
+
:bulkInsert=${(x) => x.bulkInsert}
|
|
24
56
|
@data-change=${(x, c) => x.onChange(c.event)}
|
|
25
57
|
></json-forms>
|
|
26
58
|
${when((x) => x.isSubmitHidden, html `
|
|
@@ -31,7 +63,7 @@ export const getPrefixedForm = (prefix) => html `
|
|
|
31
63
|
class="submit-button"
|
|
32
64
|
appearance="accent"
|
|
33
65
|
>
|
|
34
|
-
Submit
|
|
66
|
+
${(x) => (x.bulkInsert ? 'Submit All' : 'Submit')}
|
|
35
67
|
</${prefix}-button>
|
|
36
68
|
</slot>
|
|
37
69
|
`)}
|