@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.
Files changed (77) hide show
  1. package/dist/custom-elements.json +389 -4
  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/jsonforms/renderers/BooleanControlRenderer.d.ts.map +1 -1
  11. package/dist/dts/jsonforms/renderers/ConnectedMultiselectControlRenderer.d.ts.map +1 -1
  12. package/dist/dts/jsonforms/renderers/ControlWrapperRenderer.d.ts.map +1 -1
  13. package/dist/dts/jsonforms/renderers/EnumControlRenderer.d.ts.map +1 -1
  14. package/dist/dts/jsonforms/renderers/LayoutFormGridRenderer.d.ts +3 -0
  15. package/dist/dts/jsonforms/renderers/LayoutFormGridRenderer.d.ts.map +1 -0
  16. package/dist/dts/jsonforms/renderers/RenderersRanks.d.ts +1 -0
  17. package/dist/dts/jsonforms/renderers/RenderersRanks.d.ts.map +1 -1
  18. package/dist/dts/jsonforms/testers/isOneOfOptionMultiselect.d.ts.map +1 -1
  19. package/dist/dts/types.d.ts +89 -2
  20. package/dist/dts/types.d.ts.map +1 -1
  21. package/dist/dts/utils/csv-parser.d.ts +85 -0
  22. package/dist/dts/utils/csv-parser.d.ts.map +1 -0
  23. package/dist/dts/utils/index.d.ts +1 -0
  24. package/dist/dts/utils/index.d.ts.map +1 -1
  25. package/dist/dts/utils/schema-utils.d.ts +46 -0
  26. package/dist/dts/utils/schema-utils.d.ts.map +1 -0
  27. package/dist/dts/utils/validation.d.ts +2 -0
  28. package/dist/dts/utils/validation.d.ts.map +1 -1
  29. package/dist/esm/form.js +423 -5
  30. package/dist/esm/form.styles.js +41 -1
  31. package/dist/esm/form.template.js +33 -1
  32. package/dist/esm/jsonforms/json-forms.js +30 -0
  33. package/dist/esm/jsonforms/renderers/ArrayListWrapperRenderer.js +223 -22
  34. package/dist/esm/jsonforms/renderers/BooleanControlRenderer.js +1 -2
  35. package/dist/esm/jsonforms/renderers/ConnectedMultiselectControlRenderer.js +13 -2
  36. package/dist/esm/jsonforms/renderers/ControlWrapperRenderer.js +25 -4
  37. package/dist/esm/jsonforms/renderers/EnumControlRenderer.js +14 -5
  38. package/dist/esm/jsonforms/renderers/LayoutFormGridRenderer.js +39 -0
  39. package/dist/esm/jsonforms/renderers/RenderersRanks.js +1 -0
  40. package/dist/esm/jsonforms/testers/isOneOfOptionMultiselect.js +1 -1
  41. package/dist/esm/utils/csv-parser.js +486 -0
  42. package/dist/esm/utils/index.js +1 -0
  43. package/dist/esm/utils/schema-utils.js +120 -0
  44. package/dist/esm/utils/validation.js +2 -0
  45. package/dist/foundation-forms.api.json +1028 -34
  46. package/dist/foundation-forms.d.ts +285 -2
  47. package/docs/api/foundation-forms.arrayrendereroptions.md +2 -2
  48. package/docs/api/foundation-forms.bulkrowstatus.md +22 -0
  49. package/docs/api/foundation-forms.bulkrowsubmitstatus.md +13 -0
  50. package/docs/api/foundation-forms.bulksubmitfaileditem.md +20 -0
  51. package/docs/api/foundation-forms.bulksubmitresult.md +18 -0
  52. package/docs/api/foundation-forms.bulksubmitsuccessitem.md +17 -0
  53. package/docs/api/foundation-forms.childuischemaresolver.md +15 -0
  54. package/docs/api/foundation-forms.csvmappingresult.mappedrows.md +13 -0
  55. package/docs/api/foundation-forms.csvmappingresult.md +77 -0
  56. package/docs/api/foundation-forms.csvmappingresult.unmappedcolumns.md +13 -0
  57. package/docs/api/foundation-forms.csvparseresult.errors.md +13 -0
  58. package/docs/api/foundation-forms.csvparseresult.headers.md +13 -0
  59. package/docs/api/foundation-forms.csvparseresult.md +96 -0
  60. package/docs/api/foundation-forms.csvparseresult.rows.md +13 -0
  61. package/docs/api/foundation-forms.downloadcsvtemplate.md +74 -0
  62. package/docs/api/foundation-forms.form.bulkinsert.md +13 -0
  63. package/docs/api/foundation-forms.form.bulkinsertmaxitems.md +13 -0
  64. package/docs/api/foundation-forms.form.bulkinsertminitems.md +13 -0
  65. package/docs/api/foundation-forms.form.clearrowsubmitstatuses.md +17 -0
  66. package/docs/api/foundation-forms.form.downloadcsvtemplate.md +17 -0
  67. package/docs/api/foundation-forms.form.handlecsvfileselected.md +54 -0
  68. package/docs/api/foundation-forms.form.md +132 -0
  69. package/docs/api/foundation-forms.form.rowsubmitstatuses.md +13 -0
  70. package/docs/api/foundation-forms.form.submitsinglerow.md +56 -0
  71. package/docs/api/foundation-forms.generatecsvtemplate.md +104 -0
  72. package/docs/api/foundation-forms.mapcsvtoschema.md +88 -0
  73. package/docs/api/foundation-forms.md +147 -0
  74. package/docs/api/foundation-forms.parsecsv.md +56 -0
  75. package/docs/api/foundation-forms.uischemaelementtype.md +1 -1
  76. package/docs/api-report.md.api.md +87 -4
  77. 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 } = data, details = __rest(data, ["APPROVAL_MESSAGE"]);
212
- return { DETAILS: details, APPROVAL_MESSAGE };
484
+ const { APPROVAL_MESSAGE } = details, rest = __rest(details, ["APPROVAL_MESSAGE"]);
485
+ return { DETAILS: rest, APPROVAL_MESSAGE };
213
486
  }
214
- return { DETAILS: data };
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);
@@ -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
  `)}