@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.
Files changed (60) hide show
  1. package/dist/custom-elements.json +325 -1
  2. package/dist/dts/form.d.ts +100 -1
  3. package/dist/dts/form.d.ts.map +1 -1
  4. package/dist/dts/form.styles.d.ts.map +1 -1
  5. package/dist/dts/form.template.d.ts.map +1 -1
  6. package/dist/dts/jsonforms/json-forms.d.ts +13 -0
  7. package/dist/dts/jsonforms/json-forms.d.ts.map +1 -1
  8. package/dist/dts/jsonforms/renderers/ArrayListWrapperRenderer.d.ts +5 -0
  9. package/dist/dts/jsonforms/renderers/ArrayListWrapperRenderer.d.ts.map +1 -1
  10. package/dist/dts/types.d.ts +88 -1
  11. package/dist/dts/types.d.ts.map +1 -1
  12. package/dist/dts/utils/csv-parser.d.ts +82 -0
  13. package/dist/dts/utils/csv-parser.d.ts.map +1 -0
  14. package/dist/dts/utils/index.d.ts +1 -0
  15. package/dist/dts/utils/index.d.ts.map +1 -1
  16. package/dist/dts/utils/schema-utils.d.ts +46 -0
  17. package/dist/dts/utils/schema-utils.d.ts.map +1 -0
  18. package/dist/dts/utils/validation.d.ts +2 -0
  19. package/dist/dts/utils/validation.d.ts.map +1 -1
  20. package/dist/esm/form.js +421 -5
  21. package/dist/esm/form.styles.js +38 -1
  22. package/dist/esm/form.template.js +33 -1
  23. package/dist/esm/jsonforms/json-forms.js +30 -0
  24. package/dist/esm/jsonforms/renderers/ArrayListWrapperRenderer.js +207 -13
  25. package/dist/esm/utils/csv-parser.js +458 -0
  26. package/dist/esm/utils/index.js +1 -0
  27. package/dist/esm/utils/schema-utils.js +120 -0
  28. package/dist/esm/utils/validation.js +2 -0
  29. package/dist/foundation-forms.api.json +1006 -29
  30. package/dist/foundation-forms.d.ts +281 -1
  31. package/docs/api/foundation-forms.arrayrendereroptions.md +2 -2
  32. package/docs/api/foundation-forms.bulkrowstatus.md +22 -0
  33. package/docs/api/foundation-forms.bulkrowsubmitstatus.md +13 -0
  34. package/docs/api/foundation-forms.bulksubmitfaileditem.md +20 -0
  35. package/docs/api/foundation-forms.bulksubmitresult.md +18 -0
  36. package/docs/api/foundation-forms.bulksubmitsuccessitem.md +17 -0
  37. package/docs/api/foundation-forms.childuischemaresolver.md +15 -0
  38. package/docs/api/foundation-forms.csvmappingresult.mappedrows.md +13 -0
  39. package/docs/api/foundation-forms.csvmappingresult.md +77 -0
  40. package/docs/api/foundation-forms.csvmappingresult.unmappedcolumns.md +13 -0
  41. package/docs/api/foundation-forms.csvparseresult.errors.md +13 -0
  42. package/docs/api/foundation-forms.csvparseresult.headers.md +13 -0
  43. package/docs/api/foundation-forms.csvparseresult.md +96 -0
  44. package/docs/api/foundation-forms.csvparseresult.rows.md +13 -0
  45. package/docs/api/foundation-forms.downloadcsvtemplate.md +74 -0
  46. package/docs/api/foundation-forms.form.bulkinsert.md +13 -0
  47. package/docs/api/foundation-forms.form.bulkinsertmaxitems.md +13 -0
  48. package/docs/api/foundation-forms.form.bulkinsertminitems.md +13 -0
  49. package/docs/api/foundation-forms.form.clearrowsubmitstatuses.md +17 -0
  50. package/docs/api/foundation-forms.form.downloadcsvtemplate.md +17 -0
  51. package/docs/api/foundation-forms.form.handlecsvfileselected.md +54 -0
  52. package/docs/api/foundation-forms.form.md +132 -0
  53. package/docs/api/foundation-forms.form.rowsubmitstatuses.md +13 -0
  54. package/docs/api/foundation-forms.form.submitsinglerow.md +56 -0
  55. package/docs/api/foundation-forms.generatecsvtemplate.md +104 -0
  56. package/docs/api/foundation-forms.mapcsvtoschema.md +72 -0
  57. package/docs/api/foundation-forms.md +147 -0
  58. package/docs/api/foundation-forms.parsecsv.md +56 -0
  59. package/docs/api-report.md.api.md +85 -3
  60. 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 } = data, details = __rest(data, ["APPROVAL_MESSAGE"]);
212
- return { DETAILS: details, APPROVAL_MESSAGE };
482
+ const { APPROVAL_MESSAGE } = details, rest = __rest(details, ["APPROVAL_MESSAGE"]);
483
+ return { DETAILS: rest, APPROVAL_MESSAGE };
213
484
  }
214
- return { DETAILS: data };
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);
@@ -67,9 +67,46 @@ export const foundationFormStyles = css `
67
67
  }
68
68
 
69
69
  .submit-button {
70
- width: 70px;
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',