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