@dataverse-kit/export-engine 1.4.0 → 1.6.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/index.mjs CHANGED
@@ -12790,6 +12790,7 @@ function buildV8Body(args) {
12790
12790
  handlerBlock = "",
12791
12791
  rowCommandsLiteral = "",
12792
12792
  deleteDialogJsx = "",
12793
+ createDialogJsx = "",
12793
12794
  commandBarBlock = "",
12794
12795
  selectionModeAttr = "",
12795
12796
  onSelectionChangedAttr = ""
@@ -12816,7 +12817,7 @@ const ${componentName}: React.FC<{ items: Record<string, unknown>[] }> = ({ item
12816
12817
  editable
12817
12818
  editTrigger="click"
12818
12819
  getKey={(it) => String((it as { __rowIndex?: number }).__rowIndex ?? '')}${selectionModeAttr}${onSelectionChangedAttr}${onValueChangeAttr}${rowCommandsAttr}
12819
- />${editableAggregate}${editablePagination}${saveStatusJsx}${deleteDialogJsx}
12820
+ />${editableAggregate}${editablePagination}${saveStatusJsx}${deleteDialogJsx}${createDialogJsx}
12820
12821
  </div>
12821
12822
  );
12822
12823
  };`;
@@ -12869,6 +12870,7 @@ function buildLiveReadOnlyV8Body(args) {
12869
12870
  handlerBlock = "",
12870
12871
  rowCommandsLiteral = "",
12871
12872
  deleteDialogJsx = "",
12873
+ createDialogJsx = "",
12872
12874
  commandBarBlock = ""
12873
12875
  } = args;
12874
12876
  const rowCommandsAttr = rowCommandsLiteral ? `
@@ -12897,7 +12899,7 @@ const ${helperName}: React.FC = () => {
12897
12899
  ] as ColumnDef[]}
12898
12900
  registry={registry}
12899
12901
  getKey={(it) => String((it as { __rowIndex?: number }).__rowIndex ?? '')}${rowCommandsAttr}
12900
- />${flatAggregateInner}${flatPaginationInner}${deleteDialogJsx}
12902
+ />${flatAggregateInner}${flatPaginationInner}${deleteDialogJsx}${createDialogJsx}
12901
12903
  </div>
12902
12904
  );
12903
12905
  };`;
@@ -12943,6 +12945,7 @@ function buildCardV8LiveBody(args) {
12943
12945
  handlerBlock = "",
12944
12946
  rowCommandsLiteral = "",
12945
12947
  deleteDialogJsx = "",
12948
+ createDialogJsx = "",
12946
12949
  commandBarBlock = "",
12947
12950
  selectionModeAttr = "",
12948
12951
  onSelectionChangedAttr = ""
@@ -12974,7 +12977,7 @@ const ${helperName}: React.FC = () => {
12974
12977
  registry={registry}
12975
12978
  getKey={(it) => String((it as { __rowIndex?: number }).__rowIndex ?? '')}${selectionModeAttr}${onSelectionChangedAttr}
12976
12979
  card={{ ${cardConfigLiteral} }}${rowCommandsAttr}
12977
- />${flatAggregateInner}${flatPaginationInner}${deleteDialogJsx}
12980
+ />${flatAggregateInner}${flatPaginationInner}${deleteDialogJsx}${createDialogJsx}
12978
12981
  </div>
12979
12982
  );
12980
12983
  };`;
@@ -13300,6 +13303,7 @@ function buildNestedSubgridBody(args) {
13300
13303
  commandBarBlock,
13301
13304
  handlerBlock,
13302
13305
  deleteDialogJsx,
13306
+ createDialogJsx,
13303
13307
  hasSelection,
13304
13308
  hasHandler,
13305
13309
  dataverseHooks,
@@ -13355,7 +13359,7 @@ const ${componentName}: React.FC<{ items: Record<string, unknown>[] }> = ({ item
13355
13359
  calloutMaxRows: ${calloutMaxRows},
13356
13360
  hoverDelay: ${hoverDelay},${childEditableLiteral}
13357
13361
  }}
13358
- />${deleteDialogJsx}${childSaveStatusJsx}
13362
+ />${deleteDialogJsx}${createDialogJsx}${childSaveStatusJsx}
13359
13363
  </div>
13360
13364
  );
13361
13365
  };`;
@@ -13398,6 +13402,7 @@ function buildFocusedViewSubgridBody(args) {
13398
13402
  commandBarBlock,
13399
13403
  handlerBlock,
13400
13404
  deleteDialogJsx,
13405
+ createDialogJsx,
13401
13406
  hasSelection,
13402
13407
  hasHandler,
13403
13408
  dataverseHooks,
@@ -13451,7 +13456,7 @@ const ${componentName}: React.FC<{ items: Record<string, unknown>[] }> = ({ item
13451
13456
  childSelectionMode: ${JSON.stringify(childSelectionMode)},
13452
13457
  childSelectionGating: ${JSON.stringify(childSelectionGating)},${childEditableLiteral}
13453
13458
  }}
13454
- />${deleteDialogJsx}${childSaveStatusJsx}
13459
+ />${deleteDialogJsx}${createDialogJsx}${childSaveStatusJsx}
13455
13460
  </div>
13456
13461
  );
13457
13462
  };`;
@@ -14230,14 +14235,213 @@ function effectiveMinSelection(item) {
14230
14235
  if (typeof item.minSelectionCount === "number") return item.minSelectionCount;
14231
14236
  return DEFAULT_MIN_SEL_BY_ACTION[item.actionType ?? "custom"] ?? 0;
14232
14237
  }
14238
+ function buildExportColumnsLiteral(cols) {
14239
+ return cols.map(
14240
+ (c) => `{ key: ${JSON.stringify(c.fieldName)}, name: ${JSON.stringify(c.displayName ?? c.fieldName)} }`
14241
+ ).join(", ");
14242
+ }
14243
+ var CREATE_FIELD_DENYLIST = /* @__PURE__ */ new Set([
14244
+ "createdon",
14245
+ "createdby",
14246
+ "createdonbehalfby",
14247
+ "modifiedon",
14248
+ "modifiedby",
14249
+ "modifiedonbehalfby",
14250
+ "owningbusinessunit",
14251
+ "owningteam",
14252
+ "owninguser",
14253
+ "ownerid",
14254
+ "versionnumber",
14255
+ "overriddencreatedon",
14256
+ "importsequencenumber",
14257
+ "timezoneruleversionnumber",
14258
+ "utcconversiontimezonecode",
14259
+ "statecode",
14260
+ "statuscode"
14261
+ ]);
14262
+ function buildCreateFields(cols, primaryIdAttribute) {
14263
+ const primaryLower = (primaryIdAttribute ?? "").toLowerCase();
14264
+ const entries = [];
14265
+ for (const c of cols) {
14266
+ if (!c.fieldName) continue;
14267
+ const logical = (c.attributeLogicalName ?? c.fieldName).toLowerCase();
14268
+ if (logical === primaryLower) continue;
14269
+ if (c.isLocked) continue;
14270
+ if (c.dataType === "lookup" || c.dataType === "image") continue;
14271
+ if (CREATE_FIELD_DENYLIST.has(logical)) continue;
14272
+ const label = c.displayName ?? c.fieldName;
14273
+ let kind = "text";
14274
+ let inputType;
14275
+ let optionsLit = "";
14276
+ switch (c.dataType) {
14277
+ case "numeric":
14278
+ case "currency":
14279
+ kind = "number";
14280
+ inputType = "number";
14281
+ break;
14282
+ case "boolean":
14283
+ kind = "boolean";
14284
+ break;
14285
+ case "date":
14286
+ kind = "date";
14287
+ inputType = "date";
14288
+ break;
14289
+ case "datetime":
14290
+ kind = "date";
14291
+ inputType = "datetime-local";
14292
+ break;
14293
+ case "optionset": {
14294
+ const opts = c.rendererConfig?.options ?? [];
14295
+ if (opts.length === 0) {
14296
+ kind = "number";
14297
+ inputType = "number";
14298
+ } else {
14299
+ kind = "optionset";
14300
+ optionsLit = opts.map(
14301
+ (o) => `{ key: ${JSON.stringify(o.value)}, text: ${JSON.stringify(o.label)} }`
14302
+ ).join(", ");
14303
+ }
14304
+ break;
14305
+ }
14306
+ case "email":
14307
+ kind = "text";
14308
+ inputType = "email";
14309
+ break;
14310
+ case "phone":
14311
+ kind = "text";
14312
+ inputType = "tel";
14313
+ break;
14314
+ case "url":
14315
+ kind = "text";
14316
+ inputType = "url";
14317
+ break;
14318
+ default:
14319
+ kind = "text";
14320
+ break;
14321
+ }
14322
+ const payloadKey = c.attributeLogicalName ?? c.fieldName;
14323
+ const parts = [
14324
+ `key: ${JSON.stringify(payloadKey)}`,
14325
+ `label: ${JSON.stringify(label)}`,
14326
+ `kind: ${JSON.stringify(kind)}`
14327
+ ];
14328
+ if (inputType) parts.push(`inputType: ${JSON.stringify(inputType)}`);
14329
+ if (kind === "optionset") parts.push(`options: [${optionsLit}]`);
14330
+ entries.push(`{ ${parts.join(", ")} }`);
14331
+ }
14332
+ return entries.join(", ");
14333
+ }
14233
14334
  function buildGridCommandHandlerBlock(args) {
14234
14335
  const {
14235
14336
  entityName,
14236
14337
  entitySetName,
14237
14338
  primaryIdAttribute,
14238
14339
  itemsExpr,
14239
- selectionExpr
14340
+ selectionExpr,
14341
+ exportColumnsLiteral,
14342
+ createColumnsLiteral,
14343
+ addExisting
14240
14344
  } = args;
14345
+ const exportCase = exportColumnsLiteral ? `{
14346
+ const exportColumns = [${exportColumnsLiteral}];
14347
+ const rowsToExport = (indices.length ? indices.map((i) => items[i]) : items.slice())
14348
+ .filter((r): r is Record<string, unknown> => Boolean(r))
14349
+ .map((r) => {
14350
+ const formattedRow: Record<string, unknown> = {};
14351
+ for (const col of exportColumns) {
14352
+ formattedRow[col.key] = r[col.key + '@OData.Community.Display.V1.FormattedValue'] ?? r[col.key];
14353
+ }
14354
+ return formattedRow;
14355
+ });
14356
+ exportToFile(rowsToExport, exportColumns, 'csv', generateDefaultFilename(${JSON.stringify(entityName + "-export")}));
14357
+ return;
14358
+ }` : `{
14359
+ console.warn('[grid] Export to Excel is not yet wired in generated forms.');
14360
+ return;
14361
+ }`;
14362
+ const createStateBlock = createColumnsLiteral ? `
14363
+ const gridCreateMutation = useCreateRecord<Record<string, unknown>>('${entitySetName}');
14364
+ const [pendingCreate, setPendingCreate] = React.useState(false);
14365
+ const [createValues, setCreateValues] = React.useState<Record<string, unknown>>({});
14366
+ const [createError, setCreateError] = React.useState<string | null>(null);
14367
+ // Derived from the grid's columns; lookups/images/system fields are excluded by
14368
+ // the emitter. The payload is best-effort and validated server-side (errors show
14369
+ // in \`createError\`). NOTE: against a mock/standalone host (no Dataverse token),
14370
+ // MockApiService.createRecord no-ops and resolves \u2014 the dialog closes as if it
14371
+ // succeeded but nothing is created. Xrm is the primary path in a model-driven host.
14372
+ const createFields = React.useMemo<Array<{ key: string; label: string; kind: 'text' | 'number' | 'boolean' | 'optionset' | 'date'; inputType?: string; options?: Array<{ key: number; text: string }> }>>(
14373
+ () => [${createColumnsLiteral}],
14374
+ [],
14375
+ );
14376
+ const submitCreate = React.useCallback(async () => {
14377
+ setCreateError(null);
14378
+ const payload: Record<string, unknown> = {};
14379
+ for (const f of createFields) {
14380
+ const raw = createValues[f.key];
14381
+ if (raw === undefined || raw === null || raw === '') continue;
14382
+ payload[f.key] = (f.kind === 'number' || f.kind === 'optionset') ? Number(raw) : raw;
14383
+ }
14384
+ if (Object.keys(payload).length === 0) {
14385
+ setCreateError('Enter at least one value before creating.');
14386
+ return;
14387
+ }
14388
+ try {
14389
+ await gridCreateMutation.mutateAsync(payload);
14390
+ setPendingCreate(false);
14391
+ setCreateValues({});
14392
+ } catch (err) {
14393
+ setCreateError((err as Error)?.message ?? 'Create failed.');
14394
+ }
14395
+ }, [createValues, gridCreateMutation, createFields]);` : "";
14396
+ const newCase = createColumnsLiteral ? `{
14397
+ if (xrm?.Navigation?.openForm) {
14398
+ await xrm.Navigation.openForm({ entityName: '${entityName}', useQuickCreateForm: false });
14399
+ await refreshGrid();
14400
+ } else {
14401
+ setCreateError(null);
14402
+ setCreateValues({});
14403
+ setPendingCreate(true);
14404
+ }
14405
+ return;
14406
+ }` : `{
14407
+ if (xrm?.Navigation?.openForm) {
14408
+ await xrm.Navigation.openForm({ entityName: '${entityName}', useQuickCreateForm: false });
14409
+ await refreshGrid();
14410
+ } else {
14411
+ console.warn('[grid] New action requires Xrm.Navigation.openForm; not available in this host.');
14412
+ }
14413
+ return;
14414
+ }`;
14415
+ const addExistingStateBlock = addExisting ? `
14416
+ const gridAddExistingMutation = useUpdateRecord<Record<string, unknown>>('${addExisting.childEntitySet}');` : "";
14417
+ const addExistingCase = addExisting ? `{
14418
+ const parentId = selectedIds[0];
14419
+ if (!parentId) {
14420
+ console.warn('[grid] Select or open a parent record before adding an existing ${addExisting.childEntityLogical}.');
14421
+ return;
14422
+ }
14423
+ const lookup = (xrm as { Utility?: { lookupObjects?: (opts: unknown) => Promise<Array<{ id: string }>> } } | undefined)?.Utility;
14424
+ if (lookup?.lookupObjects) {
14425
+ const picked = await lookup.lookupObjects({ entityTypes: ['${addExisting.childEntityLogical}'], allowMultiSelect: true });
14426
+ if (!picked || picked.length === 0) return;
14427
+ for (const rec of picked) {
14428
+ const childId = String(rec.id).replace(/[{}]/g, '');
14429
+ await gridAddExistingMutation.mutateAsync({ id: childId, data: { '${addExisting.childField}@odata.bind': '/${addExisting.parentEntitySet}(' + parentId + ')' } as Record<string, unknown> });
14430
+ }
14431
+ await refreshGrid();
14432
+ } else {
14433
+ console.warn('[grid] Add Existing requires Xrm.Utility.lookupObjects; not available in this host.');
14434
+ }
14435
+ return;
14436
+ }` : `{
14437
+ if (xrm?.Navigation?.openForm) {
14438
+ await xrm.Navigation.openForm({ entityName: '${entityName}' });
14439
+ await refreshGrid();
14440
+ } else {
14441
+ console.warn('[grid] Add Existing requires Xrm.Navigation; not available in this host.');
14442
+ }
14443
+ return;
14444
+ }`;
14241
14445
  return `
14242
14446
  const queryClient = useQueryClient();
14243
14447
  const gridUpdateMutation = useUpdateRecord<Record<string, unknown>>('${entitySetName}');
@@ -14261,7 +14465,7 @@ function buildGridCommandHandlerBlock(args) {
14261
14465
  } catch (err) {
14262
14466
  setDeleteError((err as Error)?.message ?? 'Delete failed.');
14263
14467
  }
14264
- }, [pendingDelete, gridDeleteMutation]);
14468
+ }, [pendingDelete, gridDeleteMutation]);${createStateBlock}${addExistingStateBlock}
14265
14469
  const handleGridCommand = React.useCallback(async (actionType: string, overrideRowIndex?: number) => {
14266
14470
  const xrm = (typeof window !== 'undefined' ? (window as unknown as { Xrm?: any }).Xrm : undefined);
14267
14471
  const items = ${itemsExpr} as ReadonlyArray<Record<string, unknown>>;
@@ -14286,24 +14490,8 @@ function buildGridCommandHandlerBlock(args) {
14286
14490
  return true;
14287
14491
  };
14288
14492
  switch (actionType) {
14289
- case 'new': {
14290
- if (xrm?.Navigation?.openForm) {
14291
- await xrm.Navigation.openForm({ entityName: '${entityName}', useQuickCreateForm: false });
14292
- await refreshGrid();
14293
- } else {
14294
- console.warn('[grid] New action requires Xrm.Navigation.openForm; not available in this host.');
14295
- }
14296
- return;
14297
- }
14298
- case 'addExisting': {
14299
- if (xrm?.Navigation?.openForm) {
14300
- await xrm.Navigation.openForm({ entityName: '${entityName}' });
14301
- await refreshGrid();
14302
- } else {
14303
- console.warn('[grid] Add Existing requires Xrm.Navigation; not available in this host.');
14304
- }
14305
- return;
14306
- }
14493
+ case 'new': ${newCase}
14494
+ case 'addExisting': ${addExistingCase}
14307
14495
  case 'edit': {
14308
14496
  if (!requireSel('edit')) return;
14309
14497
  if (selectedIds.length > 1 && xrm?.Navigation?.openBulkEditForm) {
@@ -14339,10 +14527,7 @@ function buildGridCommandHandlerBlock(args) {
14339
14527
  await refreshGrid();
14340
14528
  return;
14341
14529
  }
14342
- case 'export': {
14343
- console.warn('[grid] Export to Excel is not yet wired in generated forms.');
14344
- return;
14345
- }
14530
+ case 'export': ${exportCase}
14346
14531
  case 'bulkEdit': {
14347
14532
  if (!requireSel('bulk edit')) return;
14348
14533
  if (xrm?.Navigation?.openBulkEditForm) {
@@ -14356,10 +14541,10 @@ function buildGridCommandHandlerBlock(args) {
14356
14541
  default:
14357
14542
  return;
14358
14543
  }
14359
- }, [${itemsExpr}, ${selectionExpr}, gridUpdateMutation, refreshGrid]);`;
14544
+ }, [${itemsExpr}, ${selectionExpr}, gridUpdateMutation, refreshGrid${addExisting ? ", gridAddExistingMutation" : ""}]);`;
14360
14545
  }
14361
14546
  function buildRowCommandsLiteral(contextMenuItems) {
14362
- return contextMenuItems.filter((ci) => (ci.actionType ?? "custom") !== "custom").map(
14547
+ return contextMenuItems.filter((ci) => (ci.actionType ?? "custom") !== "custom").filter((ci) => ci.actionType !== "new").map(
14363
14548
  (ci) => `{ key: ${JSON.stringify(ci.id)}, text: ${JSON.stringify(ci.text)}` + (ci.iconName ? `, iconName: ${JSON.stringify(ci.iconName)}` : "") + `, onClick: (item: Record<string, unknown>) => { void handleGridCommand(${JSON.stringify(ci.actionType)}, (item as { __rowIndex?: number }).__rowIndex); } }`
14364
14549
  ).join(",\n ");
14365
14550
  }
@@ -14395,6 +14580,68 @@ function buildGridDeleteDialogJsx(entityName) {
14395
14580
  </DialogFooter>
14396
14581
  </Dialog>`;
14397
14582
  }
14583
+ function buildGridCreateDialogJsx(entityName) {
14584
+ return `
14585
+ <Dialog
14586
+ hidden={!pendingCreate}
14587
+ onDismiss={() => { if (!gridCreateMutation.isPending) setPendingCreate(false); }}
14588
+ dialogContentProps={{
14589
+ type: DialogType.normal,
14590
+ title: ${JSON.stringify("Create " + entityName)},
14591
+ }}
14592
+ modalProps={{ isBlocking: true }}
14593
+ >
14594
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
14595
+ {createFields.map((f) => {
14596
+ if (f.kind === 'boolean') {
14597
+ return (
14598
+ <Checkbox
14599
+ key={f.key}
14600
+ label={f.label}
14601
+ checked={createValues[f.key] === true}
14602
+ onChange={(_ev, checked) => setCreateValues((prev) => ({ ...prev, [f.key]: !!checked }))}
14603
+ />
14604
+ );
14605
+ }
14606
+ if (f.kind === 'optionset') {
14607
+ return (
14608
+ <Dropdown
14609
+ key={f.key}
14610
+ label={f.label}
14611
+ options={f.options ?? []}
14612
+ selectedKey={(createValues[f.key] as number | undefined) ?? null}
14613
+ onChange={(_ev, opt) => setCreateValues((prev) => ({ ...prev, [f.key]: opt?.key }))}
14614
+ />
14615
+ );
14616
+ }
14617
+ return (
14618
+ <TextField
14619
+ key={f.key}
14620
+ label={f.label}
14621
+ type={f.inputType ?? 'text'}
14622
+ value={createValues[f.key] == null ? '' : String(createValues[f.key])}
14623
+ onChange={(_ev, val) => setCreateValues((prev) => ({ ...prev, [f.key]: val }))}
14624
+ />
14625
+ );
14626
+ })}
14627
+ </div>
14628
+ {createError && (
14629
+ <div style={{ color: '#a4262c', fontSize: 12, marginTop: 8 }}>{createError}</div>
14630
+ )}
14631
+ <DialogFooter>
14632
+ <PrimaryButton
14633
+ onClick={submitCreate}
14634
+ text={gridCreateMutation.isPending ? 'Creating\u2026' : 'Create'}
14635
+ disabled={gridCreateMutation.isPending}
14636
+ />
14637
+ <DefaultButton
14638
+ onClick={() => setPendingCreate(false)}
14639
+ text="Cancel"
14640
+ disabled={gridCreateMutation.isPending}
14641
+ />
14642
+ </DialogFooter>
14643
+ </Dialog>`;
14644
+ }
14398
14645
  function buildV9RenderCell(col) {
14399
14646
  const safeFieldName = JSON.stringify(col.fieldName);
14400
14647
  switch (col.rendererType) {
@@ -14757,12 +15004,18 @@ function generateLinkedSubgrid(gridDef, entityName, imports, library = "fluent-v
14757
15004
  const showRefresh = gridDef.toolbar?.showRefresh ?? false;
14758
15005
  const showColumnChooser = gridDef.toolbar?.showColumnChooser ?? false;
14759
15006
  const hasToolbarIcons = showFilters || showViewToggle || showExport || showRefresh || showColumnChooser;
15007
+ const childGridDef = gridDef.nestedGridId ? _currentGridCustomizers.find((g) => g.id === gridDef.nestedGridId) : void 0;
15008
+ const _aeRel = gridDef.nestedRelationship;
15009
+ const addExistingFeasible = Boolean(
15010
+ childGridDef && _includeDataAccessLayer && _aeRel?.relationshipType === "OneToMany" && _aeRel?.parentField && _aeRel?.childField && gridDef.dataSource?.entitySetName && gridDef.dataSource?.fetchXml && childGridDef.dataSource?.entitySetName && childGridDef.dataSource?.entityName && childGridDef.dataSource?.fetchXml
15011
+ );
14760
15012
  const barItems = (gridDef.commandBarItems ?? []).filter(
14761
15013
  (ci) => ci.visibility === "commandBar" || ci.visibility === "both" || !ci.visibility
14762
- );
15014
+ ).filter((ci) => ci.actionType !== "addExisting" || addExistingFeasible);
14763
15015
  const contextMenuItems = (gridDef.commandBarItems ?? []).filter(
14764
15016
  (ci) => ci.visibility === "contextMenu" || ci.visibility === "both" || !ci.visibility
14765
- );
15017
+ ).filter((ci) => ci.actionType !== "addExisting" || addExistingFeasible);
15018
+ const enableAddExisting = addExistingFeasible && (barItems.some((ci) => ci.actionType === "addExisting") || contextMenuItems.some((ci) => ci.actionType === "addExisting"));
14766
15019
  const _handlerAvailable = (
14767
15020
  // The dispatcher calls useUpdateRecord/useDeleteRecord/useQueryClient — which only
14768
15021
  // exist (module + provider + dep) when the data-access layer is included. Without it,
@@ -14772,6 +15025,8 @@ function generateLinkedSubgrid(gridDef, entityName, imports, library = "fluent-v
14772
15025
  // need the dispatcher in scope even when no wired commandBarItems exist.
14773
15026
  !!gridDef.toolbar?.showRefresh)
14774
15027
  );
15028
+ const enableExport = _handlerAvailable && (showExport || barItems.some((ci) => ci.actionType === "export") || contextMenuItems.some((ci) => ci.actionType === "export"));
15029
+ const enableCreate = _handlerAvailable && barItems.some((ci) => ci.actionType === "new");
14775
15030
  if (gridDef.showCommandBar && (barItems.length > 0 || showSearch || hasToolbarIcons)) {
14776
15031
  if (showSearch) imports.add("SearchBox");
14777
15032
  imports.add("DefaultButton");
@@ -14823,10 +15078,12 @@ function generateLinkedSubgrid(gridDef, entityName, imports, library = "fluent-v
14823
15078
  toolbarIcons.push(
14824
15079
  `<IconButton iconProps={{ iconName: 'ColumnOptions' }} title="Column chooser" ariaLabel="Column chooser" />`
14825
15080
  );
14826
- if (showExport)
15081
+ if (showExport) {
15082
+ const exportOnClick = _handlerAvailable ? ` onClick={() => { void handleGridCommand('export'); }}` : "";
14827
15083
  toolbarIcons.push(
14828
- `<IconButton iconProps={{ iconName: 'Download' }} title="Export" ariaLabel="Export" />`
15084
+ `<IconButton iconProps={{ iconName: 'Download' }} title="Export" ariaLabel="Export"${exportOnClick} />`
14829
15085
  );
15086
+ }
14830
15087
  if (showRefresh) {
14831
15088
  const refreshOnClick = _handlerAvailable ? ` onClick={() => { void handleGridCommand('refresh'); }}` : "";
14832
15089
  toolbarIcons.push(
@@ -14847,9 +15104,11 @@ function generateLinkedSubgrid(gridDef, entityName, imports, library = "fluent-v
14847
15104
  </div>
14848
15105
  </div>`;
14849
15106
  }
14850
- const childGridDef = gridDef.nestedGridId ? _currentGridCustomizers.find((g) => g.id === gridDef.nestedGridId) : void 0;
14851
15107
  const isGridKitNested = !!childGridDef;
14852
15108
  const visibleCols = [...gridDef.columns].filter((c) => c.isVisible).sort((a, b) => a.order - b.order);
15109
+ const exportColumnsLiteral = enableExport ? buildExportColumnsLiteral(visibleCols) : "";
15110
+ const primaryIdAttr = resolvePrimaryIdAttribute(gridDef);
15111
+ const createColumnsLiteral = enableCreate ? buildCreateFields(visibleCols, primaryIdAttr) : "";
14853
15112
  visibleCols.filter((c) => c.rendererType !== "text");
14854
15113
  const colEntries = isGridKitNested ? "" : generateColumnEntries(visibleCols, library);
14855
15114
  const selectionMode = gridDef.selectionMode === "multiple" ? "SelectionMode.multiple" : gridDef.selectionMode === "single" ? "SelectionMode.single" : "SelectionMode.none";
@@ -14915,19 +15174,38 @@ ${childEntries.join(",\n")},
14915
15174
  entitySetName: gridDef.dataSource.entitySetName,
14916
15175
  primaryIdAttribute: resolvePrimaryIdAttribute(gridDef),
14917
15176
  itemsExpr: "keyedItems",
14918
- selectionExpr: "selectedIndices"
15177
+ selectionExpr: "selectedIndices",
15178
+ exportColumnsLiteral,
15179
+ createColumnsLiteral,
15180
+ // Associate an existing child to the clicked/active parent (the ONLY call site
15181
+ // that wires it). Shared by the nested AND focused-view emitters below — both
15182
+ // reuse this ngHandlerBlock. Gated on enableAddExisting (feasible AND an
15183
+ // addExisting item present); the && chain narrows the optional entity-set strings
15184
+ // to `string` (enableAddExisting ⟹ addExistingFeasible guarantees they're present).
15185
+ addExisting: enableAddExisting && childEntitySet && childEntityLogical && parentEntitySet && nestedRel?.childField ? {
15186
+ childEntitySet,
15187
+ childEntityLogical,
15188
+ childField: nestedRel.childField,
15189
+ parentEntitySet
15190
+ } : null
14919
15191
  }) : "";
14920
15192
  const ngHasDelete = (gridDef.commandBarItems ?? []).some(
14921
15193
  (ci) => ci.actionType === "delete"
14922
15194
  );
14923
15195
  const ngDeleteDialogJsx = ngHasDelete && ngHandlerBlock !== "" ? buildGridDeleteDialogJsx(gridDef.dataSource?.entityName ?? entityName) : "";
14924
- if (ngDeleteDialogJsx) {
15196
+ const ngCreateDialogJsx = ngHandlerBlock !== "" && createColumnsLiteral ? buildGridCreateDialogJsx(gridDef.dataSource?.entityName ?? entityName) : "";
15197
+ if (ngDeleteDialogJsx || ngCreateDialogJsx) {
14925
15198
  imports.add("Dialog");
14926
15199
  imports.add("DialogType");
14927
15200
  imports.add("DialogFooter");
14928
15201
  imports.add("PrimaryButton");
14929
15202
  imports.add("DefaultButton");
14930
15203
  }
15204
+ if (ngCreateDialogJsx) {
15205
+ imports.add("TextField");
15206
+ imports.add("Dropdown");
15207
+ imports.add("Checkbox");
15208
+ }
14931
15209
  const ngRowCommandsLiteral = _handlerAvailable && (ngHasCommandBar || ngHasContextMenu && useLiveNested) ? buildRowCommandsLiteral(contextMenuItems) : "";
14932
15210
  const ngToolbarWired = (gridDef.commandBarItems ?? []).some(
14933
15211
  (ci) => (ci.actionType ?? "custom") !== "custom"
@@ -14941,9 +15219,18 @@ ${childEntries.join(",\n")},
14941
15219
  const ngDataverseHooks = [
14942
15220
  ...useLiveNested ? ["useDataverseQuery"] : [],
14943
15221
  ...useLiveNested && ngNeedsUpdate || ngHasHandler ? ["useUpdateRecord"] : [],
14944
- ...useLiveNested && ngNeedsDelete || ngHasHandler ? ["useDeleteRecord"] : []
15222
+ ...useLiveNested && ngNeedsDelete || ngHasHandler ? ["useDeleteRecord"] : [],
15223
+ // Create mutation only when the create dialog is wired (the `new` case's
15224
+ // non-Xrm fallback) — gated like the export util import to stay byte-stable
15225
+ // for non-create handler grids.
15226
+ ...ngHasHandler && createColumnsLiteral ? ["useCreateRecord"] : []
15227
+ ];
15228
+ const ngCommandBarImports = [
15229
+ ...useLiveNested && ngNeedsDelete || ngHasHandler ? [`import { useQueryClient } from '@tanstack/react-query';`] : [],
15230
+ // Export case (shared dispatcher → both nested + focused-view) calls
15231
+ // exportToFile/generateDefaultFilename when export is wired.
15232
+ ...ngHasHandler && enableExport ? [`import { exportToFile, generateDefaultFilename } from '../lib/grid-kit/utils';`] : []
14945
15233
  ];
14946
- const ngCommandBarImports = useLiveNested && ngNeedsDelete || ngHasHandler ? [`import { useQueryClient } from '@tanstack/react-query';`] : [];
14947
15234
  const ngParentSelectionMode = ngHasCommandBar ? gridDef.selectionMode === "single" ? "single" : "multiple" : gridDef.selectionMode ?? "none";
14948
15235
  const ngChildSaveBack = Boolean(
14949
15236
  useLiveNested && live && (childGridDef.isEditable ?? false)
@@ -14992,6 +15279,7 @@ ${childEntries.join(",\n")},
14992
15279
  commandBarBlock: gridCommandBarBlock,
14993
15280
  handlerBlock: ngHandlerBlock,
14994
15281
  deleteDialogJsx: ngDeleteDialogJsx,
15282
+ createDialogJsx: ngCreateDialogJsx,
14995
15283
  hasSelection: ngHasCommandBar,
14996
15284
  hasHandler: ngHandlerBlock !== "",
14997
15285
  dataverseHooks: ngDataverseHooks,
@@ -15047,6 +15335,7 @@ ${childEntries.join(",\n")},
15047
15335
  commandBarBlock: gridCommandBarBlock,
15048
15336
  handlerBlock: ngHandlerBlock,
15049
15337
  deleteDialogJsx: ngDeleteDialogJsx,
15338
+ createDialogJsx: ngCreateDialogJsx,
15050
15339
  hasSelection: ngHasCommandBar,
15051
15340
  dataverseHooks: ngDataverseHooks,
15052
15341
  commandBarImports: ngCommandBarImports,
@@ -15127,11 +15416,16 @@ ${childEntries.join(",\n")},
15127
15416
  entitySetName: gridDef.dataSource.entitySetName,
15128
15417
  primaryIdAttribute: resolvePrimaryIdAttribute(gridDef),
15129
15418
  itemsExpr: "rows",
15130
- selectionExpr: "selectedIndices"
15419
+ selectionExpr: "selectedIndices",
15420
+ exportColumnsLiteral,
15421
+ createColumnsLiteral,
15422
+ addExisting: null
15423
+ // flat (no parent relationship) → addExisting filtered out above
15131
15424
  }) : "";
15132
15425
  const cardSelectedIndicesState = cardEmitHandler ? "\n const [selectedIndices, setSelectedIndices] = React.useState<Set<number>>(new Set());" : "";
15133
15426
  const cardRowCommandsLiteral = cardEmitRowCommands ? buildRowCommandsLiteral(contextMenuItems) : "";
15134
15427
  const cardDeleteDialogJsx = cardEmitRowCommands && flatHasDelete || cardEmitToolbar && flatBarHasDelete ? buildGridDeleteDialogJsx(gridDef.dataSource?.entityName ?? entityName) : "";
15428
+ const cardCreateDialogJsx = cardEmitHandler && createColumnsLiteral ? buildGridCreateDialogJsx(gridDef.dataSource?.entityName ?? entityName) : "";
15135
15429
  const cardCommandBarBlock = cardEmitToolbar ? gridCommandBarBlock : "";
15136
15430
  const cardSelectionModeAttr = cardEmitToolbar ? flatSelectionModeAttr : "";
15137
15431
  const cardOnSelectionChangedAttr = cardEmitToolbar ? flatOnSelectionChangedAttr : "";
@@ -15150,20 +15444,26 @@ ${childEntries.join(",\n")},
15150
15444
  handlerBlock: cardHandlerBlock,
15151
15445
  rowCommandsLiteral: cardRowCommandsLiteral,
15152
15446
  deleteDialogJsx: cardDeleteDialogJsx,
15447
+ createDialogJsx: cardCreateDialogJsx,
15153
15448
  commandBarBlock: cardCommandBarBlock,
15154
15449
  selectionModeAttr: cardSelectionModeAttr,
15155
15450
  onSelectionChangedAttr: cardOnSelectionChangedAttr
15156
15451
  })
15157
15452
  ];
15158
15453
  _usesGridKit = true;
15159
- const cardDataverseImport = cardEmitHandler ? `import { useDataverseQuery, useUpdateRecord, useDeleteRecord } from '../lib/dataverse';` : `import { useDataverseQuery } from '../lib/dataverse';`;
15160
- if (cardDeleteDialogJsx) {
15454
+ const cardDataverseImport = cardEmitHandler ? `import { useDataverseQuery, useUpdateRecord, useDeleteRecord${createColumnsLiteral ? ", useCreateRecord" : ""} } from '../lib/dataverse';` : `import { useDataverseQuery } from '../lib/dataverse';`;
15455
+ if (cardDeleteDialogJsx || cardCreateDialogJsx) {
15161
15456
  imports.add("Dialog");
15162
15457
  imports.add("DialogType");
15163
15458
  imports.add("DialogFooter");
15164
15459
  imports.add("PrimaryButton");
15165
15460
  imports.add("DefaultButton");
15166
15461
  }
15462
+ if (cardCreateDialogJsx) {
15463
+ imports.add("TextField");
15464
+ imports.add("Dropdown");
15465
+ imports.add("Checkbox");
15466
+ }
15167
15467
  return {
15168
15468
  imports,
15169
15469
  v9Imports,
@@ -15171,7 +15471,10 @@ ${childEntries.join(",\n")},
15171
15471
  extraImports: cardIsLive ? cardEmitHandler ? [
15172
15472
  cardDataverseImport,
15173
15473
  `import { useQueryClient } from '@tanstack/react-query';`,
15174
- `import { CardGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`
15474
+ `import { CardGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`,
15475
+ // Export case calls exportToFile/generateDefaultFilename (cardEmitHandler
15476
+ // here; the export case is only real when enableExport).
15477
+ ...enableExport ? [`import { exportToFile, generateDefaultFilename } from '../lib/grid-kit/utils';`] : []
15175
15478
  ] : [
15176
15479
  `import { useDataverseQuery } from '../lib/dataverse';`,
15177
15480
  `import { CardGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`
@@ -15209,11 +15512,16 @@ ${childEntries.join(",\n")},
15209
15512
  entitySetName: gridDef.dataSource.entitySetName,
15210
15513
  primaryIdAttribute: resolvePrimaryIdAttribute(gridDef),
15211
15514
  itemsExpr: "rows",
15212
- selectionExpr: "selectedIndices"
15515
+ selectionExpr: "selectedIndices",
15516
+ exportColumnsLiteral,
15517
+ createColumnsLiteral,
15518
+ addExisting: null
15519
+ // flat (no parent relationship) → addExisting filtered out above
15213
15520
  }) : "";
15214
15521
  const roSelectedIndicesState = roEmitHandler ? "\n const [selectedIndices, setSelectedIndices] = React.useState<Set<number>>(new Set());" : "";
15215
15522
  const roRowCommandsLiteral = roEmitRowCommands ? buildRowCommandsLiteral(contextMenuItems) : "";
15216
15523
  const roDeleteDialogJsx = roEmitRowCommands && flatHasDelete ? buildGridDeleteDialogJsx(gridDef.dataSource?.entityName ?? entityName) : "";
15524
+ const roCreateDialogJsx = roEmitHandler && createColumnsLiteral ? buildGridCreateDialogJsx(gridDef.dataSource?.entityName ?? entityName) : "";
15217
15525
  const roCommandBarBlock = roEmitToolbar ? gridCommandBarBlock : "";
15218
15526
  const helperComponents = [
15219
15527
  buildLiveReadOnlySubgridBody({
@@ -15232,18 +15540,24 @@ ${childEntries.join(",\n")},
15232
15540
  handlerBlock: roHandlerBlock,
15233
15541
  rowCommandsLiteral: roRowCommandsLiteral,
15234
15542
  deleteDialogJsx: roDeleteDialogJsx,
15543
+ createDialogJsx: roCreateDialogJsx,
15235
15544
  commandBarBlock: roCommandBarBlock
15236
15545
  })
15237
15546
  ];
15238
15547
  if (!isV9) _usesGridKit = true;
15239
- const roDataverseImport = roEmitHandler ? `import { useDataverseQuery, useUpdateRecord, useDeleteRecord } from '../lib/dataverse';` : `import { useDataverseQuery } from '../lib/dataverse';`;
15240
- if (roDeleteDialogJsx) {
15548
+ const roDataverseImport = roEmitHandler ? `import { useDataverseQuery, useUpdateRecord, useDeleteRecord${createColumnsLiteral ? ", useCreateRecord" : ""} } from '../lib/dataverse';` : `import { useDataverseQuery } from '../lib/dataverse';`;
15549
+ if (roDeleteDialogJsx || roCreateDialogJsx) {
15241
15550
  imports.add("Dialog");
15242
15551
  imports.add("DialogType");
15243
15552
  imports.add("DialogFooter");
15244
15553
  imports.add("PrimaryButton");
15245
15554
  imports.add("DefaultButton");
15246
15555
  }
15556
+ if (roCreateDialogJsx) {
15557
+ imports.add("TextField");
15558
+ imports.add("Dropdown");
15559
+ imports.add("Checkbox");
15560
+ }
15247
15561
  return {
15248
15562
  imports,
15249
15563
  v9Imports,
@@ -15251,7 +15565,8 @@ ${childEntries.join(",\n")},
15251
15565
  extraImports: isV9 ? [`import { useDataverseQuery } from '../lib/dataverse';`] : roEmitHandler ? [
15252
15566
  roDataverseImport,
15253
15567
  `import { useQueryClient } from '@tanstack/react-query';`,
15254
- `import { ReadOnlyGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`
15568
+ `import { ReadOnlyGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`,
15569
+ ...enableExport ? [`import { exportToFile, generateDefaultFilename } from '../lib/grid-kit/utils';`] : []
15255
15570
  ] : [
15256
15571
  `import { useDataverseQuery } from '../lib/dataverse';`,
15257
15572
  `import { ReadOnlyGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`
@@ -15311,11 +15626,16 @@ const ${liveWrapperName}: React.FC = () => {
15311
15626
  entitySetName: gridDef.dataSource.entitySetName,
15312
15627
  primaryIdAttribute: resolvePrimaryIdAttribute(gridDef),
15313
15628
  itemsExpr: "keyedItems",
15314
- selectionExpr: "selectedIndices"
15629
+ selectionExpr: "selectedIndices",
15630
+ exportColumnsLiteral,
15631
+ createColumnsLiteral,
15632
+ addExisting: null
15633
+ // flat (no parent relationship) → addExisting filtered out above
15315
15634
  }) : "";
15316
15635
  const edSelectedIndicesState = edEmitHandler ? "\n const [selectedIndices, setSelectedIndices] = React.useState<Set<number>>(new Set());" : "";
15317
15636
  const edRowCommandsLiteral = edEmitRowCommands ? buildRowCommandsLiteral(contextMenuItems) : "";
15318
15637
  const edDeleteDialogJsx = edEmitRowCommands && flatHasDelete || edEmitToolbar && flatBarHasDelete ? buildGridDeleteDialogJsx(gridDef.dataSource?.entityName ?? entityName) : "";
15638
+ const edCreateDialogJsx = edEmitHandler && createColumnsLiteral ? buildGridCreateDialogJsx(gridDef.dataSource?.entityName ?? entityName) : "";
15319
15639
  const edCommandBarBlock = edEmitToolbar ? gridCommandBarBlock : "";
15320
15640
  const edSelectionModeAttr = edEmitToolbar ? flatSelectionModeAttr : "";
15321
15641
  const edOnSelectionChangedAttr = edEmitToolbar ? flatOnSelectionChangedAttr : "";
@@ -15332,6 +15652,7 @@ const ${liveWrapperName}: React.FC = () => {
15332
15652
  handlerBlock: edHandlerBlock,
15333
15653
  rowCommandsLiteral: edRowCommandsLiteral,
15334
15654
  deleteDialogJsx: edDeleteDialogJsx,
15655
+ createDialogJsx: edCreateDialogJsx,
15335
15656
  commandBarBlock: edCommandBarBlock,
15336
15657
  selectionModeAttr: edSelectionModeAttr,
15337
15658
  onSelectionChangedAttr: edOnSelectionChangedAttr
@@ -15343,18 +15664,24 @@ const ${liveWrapperName}: React.FC = () => {
15343
15664
  const jsx3 = editableUsesLiveData && liveWrapperName ? `{/* Subgrid: ${safeEntityName} \u2014 ${safeGridName} (editable, live) */}${todoComment}
15344
15665
  <${liveWrapperName} />` : `{/* Subgrid: ${safeEntityName} \u2014 ${safeGridName} (editable) */}${todoComment}
15345
15666
  <${componentName} items={${itemsLiteral}} />`;
15346
- const edDataverseImport = edEmitHandler ? `import { useDataverseQuery, useUpdateRecord, useDeleteRecord } from '../lib/dataverse';` : `import { useDataverseQuery, useUpdateRecord } from '../lib/dataverse';`;
15347
- if (edDeleteDialogJsx) {
15667
+ const edDataverseImport = edEmitHandler ? `import { useDataverseQuery, useUpdateRecord, useDeleteRecord${createColumnsLiteral ? ", useCreateRecord" : ""} } from '../lib/dataverse';` : `import { useDataverseQuery, useUpdateRecord } from '../lib/dataverse';`;
15668
+ if (edDeleteDialogJsx || edCreateDialogJsx) {
15348
15669
  imports.add("Dialog");
15349
15670
  imports.add("DialogType");
15350
15671
  imports.add("DialogFooter");
15351
15672
  imports.add("PrimaryButton");
15352
15673
  imports.add("DefaultButton");
15353
15674
  }
15675
+ if (edCreateDialogJsx) {
15676
+ imports.add("TextField");
15677
+ imports.add("Dropdown");
15678
+ imports.add("Checkbox");
15679
+ }
15354
15680
  const extraImports2 = editableUsesLiveData ? edEmitHandler ? [
15355
15681
  edDataverseImport,
15356
15682
  `import { useQueryClient } from '@tanstack/react-query';`,
15357
- `import { DataGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`
15683
+ `import { DataGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`,
15684
+ ...enableExport ? [`import { exportToFile, generateDefaultFilename } from '../lib/grid-kit/utils';`] : []
15358
15685
  ] : [
15359
15686
  `import { useDataverseQuery, useUpdateRecord } from '../lib/dataverse';`,
15360
15687
  `import { DataGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`
@@ -18163,6 +18490,19 @@ ${code}
18163
18490
  };
18164
18491
  `;
18165
18492
  }
18493
+ function dedupeImportStatements(content) {
18494
+ const isImportLine = (l) => /^import\b[^\n]*\bfrom\b[^\n]*;[ \t]*$/.test(l) || /^import\s+['"][^'"]+['"];[ \t]*$/.test(l);
18495
+ const seen = /* @__PURE__ */ new Set();
18496
+ const out = [];
18497
+ for (const line of content.split("\n")) {
18498
+ if (isImportLine(line)) {
18499
+ if (seen.has(line)) continue;
18500
+ seen.add(line);
18501
+ }
18502
+ out.push(line);
18503
+ }
18504
+ return out.join("\n");
18505
+ }
18166
18506
  function generateFormCode(form, options) {
18167
18507
  _currentGridCustomizers = options?.gridCustomizers ?? [];
18168
18508
  _includeSampleData = options?.includeSampleData ?? false;
@@ -18193,6 +18533,7 @@ function generateFormCode(form, options) {
18193
18533
  default:
18194
18534
  content = generateMainForm(form, hasRules, library);
18195
18535
  }
18536
+ content = dedupeImportStatements(content);
18196
18537
  const files = [
18197
18538
  { path: `src/components/${fileName}`, content }
18198
18539
  ];