@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.cjs CHANGED
@@ -12810,6 +12810,7 @@ function buildV8Body(args) {
12810
12810
  handlerBlock = "",
12811
12811
  rowCommandsLiteral = "",
12812
12812
  deleteDialogJsx = "",
12813
+ createDialogJsx = "",
12813
12814
  commandBarBlock = "",
12814
12815
  selectionModeAttr = "",
12815
12816
  onSelectionChangedAttr = ""
@@ -12836,7 +12837,7 @@ const ${componentName}: React.FC<{ items: Record<string, unknown>[] }> = ({ item
12836
12837
  editable
12837
12838
  editTrigger="click"
12838
12839
  getKey={(it) => String((it as { __rowIndex?: number }).__rowIndex ?? '')}${selectionModeAttr}${onSelectionChangedAttr}${onValueChangeAttr}${rowCommandsAttr}
12839
- />${editableAggregate}${editablePagination}${saveStatusJsx}${deleteDialogJsx}
12840
+ />${editableAggregate}${editablePagination}${saveStatusJsx}${deleteDialogJsx}${createDialogJsx}
12840
12841
  </div>
12841
12842
  );
12842
12843
  };`;
@@ -12889,6 +12890,7 @@ function buildLiveReadOnlyV8Body(args) {
12889
12890
  handlerBlock = "",
12890
12891
  rowCommandsLiteral = "",
12891
12892
  deleteDialogJsx = "",
12893
+ createDialogJsx = "",
12892
12894
  commandBarBlock = ""
12893
12895
  } = args;
12894
12896
  const rowCommandsAttr = rowCommandsLiteral ? `
@@ -12917,7 +12919,7 @@ const ${helperName}: React.FC = () => {
12917
12919
  ] as ColumnDef[]}
12918
12920
  registry={registry}
12919
12921
  getKey={(it) => String((it as { __rowIndex?: number }).__rowIndex ?? '')}${rowCommandsAttr}
12920
- />${flatAggregateInner}${flatPaginationInner}${deleteDialogJsx}
12922
+ />${flatAggregateInner}${flatPaginationInner}${deleteDialogJsx}${createDialogJsx}
12921
12923
  </div>
12922
12924
  );
12923
12925
  };`;
@@ -12963,6 +12965,7 @@ function buildCardV8LiveBody(args) {
12963
12965
  handlerBlock = "",
12964
12966
  rowCommandsLiteral = "",
12965
12967
  deleteDialogJsx = "",
12968
+ createDialogJsx = "",
12966
12969
  commandBarBlock = "",
12967
12970
  selectionModeAttr = "",
12968
12971
  onSelectionChangedAttr = ""
@@ -12994,7 +12997,7 @@ const ${helperName}: React.FC = () => {
12994
12997
  registry={registry}
12995
12998
  getKey={(it) => String((it as { __rowIndex?: number }).__rowIndex ?? '')}${selectionModeAttr}${onSelectionChangedAttr}
12996
12999
  card={{ ${cardConfigLiteral} }}${rowCommandsAttr}
12997
- />${flatAggregateInner}${flatPaginationInner}${deleteDialogJsx}
13000
+ />${flatAggregateInner}${flatPaginationInner}${deleteDialogJsx}${createDialogJsx}
12998
13001
  </div>
12999
13002
  );
13000
13003
  };`;
@@ -13320,6 +13323,7 @@ function buildNestedSubgridBody(args) {
13320
13323
  commandBarBlock,
13321
13324
  handlerBlock,
13322
13325
  deleteDialogJsx,
13326
+ createDialogJsx,
13323
13327
  hasSelection,
13324
13328
  hasHandler,
13325
13329
  dataverseHooks,
@@ -13375,7 +13379,7 @@ const ${componentName}: React.FC<{ items: Record<string, unknown>[] }> = ({ item
13375
13379
  calloutMaxRows: ${calloutMaxRows},
13376
13380
  hoverDelay: ${hoverDelay},${childEditableLiteral}
13377
13381
  }}
13378
- />${deleteDialogJsx}${childSaveStatusJsx}
13382
+ />${deleteDialogJsx}${createDialogJsx}${childSaveStatusJsx}
13379
13383
  </div>
13380
13384
  );
13381
13385
  };`;
@@ -13418,6 +13422,7 @@ function buildFocusedViewSubgridBody(args) {
13418
13422
  commandBarBlock,
13419
13423
  handlerBlock,
13420
13424
  deleteDialogJsx,
13425
+ createDialogJsx,
13421
13426
  hasSelection,
13422
13427
  hasHandler,
13423
13428
  dataverseHooks,
@@ -13471,7 +13476,7 @@ const ${componentName}: React.FC<{ items: Record<string, unknown>[] }> = ({ item
13471
13476
  childSelectionMode: ${JSON.stringify(childSelectionMode)},
13472
13477
  childSelectionGating: ${JSON.stringify(childSelectionGating)},${childEditableLiteral}
13473
13478
  }}
13474
- />${deleteDialogJsx}${childSaveStatusJsx}
13479
+ />${deleteDialogJsx}${createDialogJsx}${childSaveStatusJsx}
13475
13480
  </div>
13476
13481
  );
13477
13482
  };`;
@@ -14250,14 +14255,213 @@ function effectiveMinSelection(item) {
14250
14255
  if (typeof item.minSelectionCount === "number") return item.minSelectionCount;
14251
14256
  return DEFAULT_MIN_SEL_BY_ACTION[item.actionType ?? "custom"] ?? 0;
14252
14257
  }
14258
+ function buildExportColumnsLiteral(cols) {
14259
+ return cols.map(
14260
+ (c) => `{ key: ${JSON.stringify(c.fieldName)}, name: ${JSON.stringify(c.displayName ?? c.fieldName)} }`
14261
+ ).join(", ");
14262
+ }
14263
+ var CREATE_FIELD_DENYLIST = /* @__PURE__ */ new Set([
14264
+ "createdon",
14265
+ "createdby",
14266
+ "createdonbehalfby",
14267
+ "modifiedon",
14268
+ "modifiedby",
14269
+ "modifiedonbehalfby",
14270
+ "owningbusinessunit",
14271
+ "owningteam",
14272
+ "owninguser",
14273
+ "ownerid",
14274
+ "versionnumber",
14275
+ "overriddencreatedon",
14276
+ "importsequencenumber",
14277
+ "timezoneruleversionnumber",
14278
+ "utcconversiontimezonecode",
14279
+ "statecode",
14280
+ "statuscode"
14281
+ ]);
14282
+ function buildCreateFields(cols, primaryIdAttribute) {
14283
+ const primaryLower = (primaryIdAttribute ?? "").toLowerCase();
14284
+ const entries = [];
14285
+ for (const c of cols) {
14286
+ if (!c.fieldName) continue;
14287
+ const logical = (c.attributeLogicalName ?? c.fieldName).toLowerCase();
14288
+ if (logical === primaryLower) continue;
14289
+ if (c.isLocked) continue;
14290
+ if (c.dataType === "lookup" || c.dataType === "image") continue;
14291
+ if (CREATE_FIELD_DENYLIST.has(logical)) continue;
14292
+ const label = c.displayName ?? c.fieldName;
14293
+ let kind = "text";
14294
+ let inputType;
14295
+ let optionsLit = "";
14296
+ switch (c.dataType) {
14297
+ case "numeric":
14298
+ case "currency":
14299
+ kind = "number";
14300
+ inputType = "number";
14301
+ break;
14302
+ case "boolean":
14303
+ kind = "boolean";
14304
+ break;
14305
+ case "date":
14306
+ kind = "date";
14307
+ inputType = "date";
14308
+ break;
14309
+ case "datetime":
14310
+ kind = "date";
14311
+ inputType = "datetime-local";
14312
+ break;
14313
+ case "optionset": {
14314
+ const opts = c.rendererConfig?.options ?? [];
14315
+ if (opts.length === 0) {
14316
+ kind = "number";
14317
+ inputType = "number";
14318
+ } else {
14319
+ kind = "optionset";
14320
+ optionsLit = opts.map(
14321
+ (o) => `{ key: ${JSON.stringify(o.value)}, text: ${JSON.stringify(o.label)} }`
14322
+ ).join(", ");
14323
+ }
14324
+ break;
14325
+ }
14326
+ case "email":
14327
+ kind = "text";
14328
+ inputType = "email";
14329
+ break;
14330
+ case "phone":
14331
+ kind = "text";
14332
+ inputType = "tel";
14333
+ break;
14334
+ case "url":
14335
+ kind = "text";
14336
+ inputType = "url";
14337
+ break;
14338
+ default:
14339
+ kind = "text";
14340
+ break;
14341
+ }
14342
+ const payloadKey = c.attributeLogicalName ?? c.fieldName;
14343
+ const parts = [
14344
+ `key: ${JSON.stringify(payloadKey)}`,
14345
+ `label: ${JSON.stringify(label)}`,
14346
+ `kind: ${JSON.stringify(kind)}`
14347
+ ];
14348
+ if (inputType) parts.push(`inputType: ${JSON.stringify(inputType)}`);
14349
+ if (kind === "optionset") parts.push(`options: [${optionsLit}]`);
14350
+ entries.push(`{ ${parts.join(", ")} }`);
14351
+ }
14352
+ return entries.join(", ");
14353
+ }
14253
14354
  function buildGridCommandHandlerBlock(args) {
14254
14355
  const {
14255
14356
  entityName,
14256
14357
  entitySetName,
14257
14358
  primaryIdAttribute,
14258
14359
  itemsExpr,
14259
- selectionExpr
14360
+ selectionExpr,
14361
+ exportColumnsLiteral,
14362
+ createColumnsLiteral,
14363
+ addExisting
14260
14364
  } = args;
14365
+ const exportCase = exportColumnsLiteral ? `{
14366
+ const exportColumns = [${exportColumnsLiteral}];
14367
+ const rowsToExport = (indices.length ? indices.map((i) => items[i]) : items.slice())
14368
+ .filter((r): r is Record<string, unknown> => Boolean(r))
14369
+ .map((r) => {
14370
+ const formattedRow: Record<string, unknown> = {};
14371
+ for (const col of exportColumns) {
14372
+ formattedRow[col.key] = r[col.key + '@OData.Community.Display.V1.FormattedValue'] ?? r[col.key];
14373
+ }
14374
+ return formattedRow;
14375
+ });
14376
+ exportToFile(rowsToExport, exportColumns, 'csv', generateDefaultFilename(${JSON.stringify(entityName + "-export")}));
14377
+ return;
14378
+ }` : `{
14379
+ console.warn('[grid] Export to Excel is not yet wired in generated forms.');
14380
+ return;
14381
+ }`;
14382
+ const createStateBlock = createColumnsLiteral ? `
14383
+ const gridCreateMutation = useCreateRecord<Record<string, unknown>>('${entitySetName}');
14384
+ const [pendingCreate, setPendingCreate] = React.useState(false);
14385
+ const [createValues, setCreateValues] = React.useState<Record<string, unknown>>({});
14386
+ const [createError, setCreateError] = React.useState<string | null>(null);
14387
+ // Derived from the grid's columns; lookups/images/system fields are excluded by
14388
+ // the emitter. The payload is best-effort and validated server-side (errors show
14389
+ // in \`createError\`). NOTE: against a mock/standalone host (no Dataverse token),
14390
+ // MockApiService.createRecord no-ops and resolves \u2014 the dialog closes as if it
14391
+ // succeeded but nothing is created. Xrm is the primary path in a model-driven host.
14392
+ const createFields = React.useMemo<Array<{ key: string; label: string; kind: 'text' | 'number' | 'boolean' | 'optionset' | 'date'; inputType?: string; options?: Array<{ key: number; text: string }> }>>(
14393
+ () => [${createColumnsLiteral}],
14394
+ [],
14395
+ );
14396
+ const submitCreate = React.useCallback(async () => {
14397
+ setCreateError(null);
14398
+ const payload: Record<string, unknown> = {};
14399
+ for (const f of createFields) {
14400
+ const raw = createValues[f.key];
14401
+ if (raw === undefined || raw === null || raw === '') continue;
14402
+ payload[f.key] = (f.kind === 'number' || f.kind === 'optionset') ? Number(raw) : raw;
14403
+ }
14404
+ if (Object.keys(payload).length === 0) {
14405
+ setCreateError('Enter at least one value before creating.');
14406
+ return;
14407
+ }
14408
+ try {
14409
+ await gridCreateMutation.mutateAsync(payload);
14410
+ setPendingCreate(false);
14411
+ setCreateValues({});
14412
+ } catch (err) {
14413
+ setCreateError((err as Error)?.message ?? 'Create failed.');
14414
+ }
14415
+ }, [createValues, gridCreateMutation, createFields]);` : "";
14416
+ const newCase = createColumnsLiteral ? `{
14417
+ if (xrm?.Navigation?.openForm) {
14418
+ await xrm.Navigation.openForm({ entityName: '${entityName}', useQuickCreateForm: false });
14419
+ await refreshGrid();
14420
+ } else {
14421
+ setCreateError(null);
14422
+ setCreateValues({});
14423
+ setPendingCreate(true);
14424
+ }
14425
+ return;
14426
+ }` : `{
14427
+ if (xrm?.Navigation?.openForm) {
14428
+ await xrm.Navigation.openForm({ entityName: '${entityName}', useQuickCreateForm: false });
14429
+ await refreshGrid();
14430
+ } else {
14431
+ console.warn('[grid] New action requires Xrm.Navigation.openForm; not available in this host.');
14432
+ }
14433
+ return;
14434
+ }`;
14435
+ const addExistingStateBlock = addExisting ? `
14436
+ const gridAddExistingMutation = useUpdateRecord<Record<string, unknown>>('${addExisting.childEntitySet}');` : "";
14437
+ const addExistingCase = addExisting ? `{
14438
+ const parentId = selectedIds[0];
14439
+ if (!parentId) {
14440
+ console.warn('[grid] Select or open a parent record before adding an existing ${addExisting.childEntityLogical}.');
14441
+ return;
14442
+ }
14443
+ const lookup = (xrm as { Utility?: { lookupObjects?: (opts: unknown) => Promise<Array<{ id: string }>> } } | undefined)?.Utility;
14444
+ if (lookup?.lookupObjects) {
14445
+ const picked = await lookup.lookupObjects({ entityTypes: ['${addExisting.childEntityLogical}'], allowMultiSelect: true });
14446
+ if (!picked || picked.length === 0) return;
14447
+ for (const rec of picked) {
14448
+ const childId = String(rec.id).replace(/[{}]/g, '');
14449
+ await gridAddExistingMutation.mutateAsync({ id: childId, data: { '${addExisting.childField}@odata.bind': '/${addExisting.parentEntitySet}(' + parentId + ')' } as Record<string, unknown> });
14450
+ }
14451
+ await refreshGrid();
14452
+ } else {
14453
+ console.warn('[grid] Add Existing requires Xrm.Utility.lookupObjects; not available in this host.');
14454
+ }
14455
+ return;
14456
+ }` : `{
14457
+ if (xrm?.Navigation?.openForm) {
14458
+ await xrm.Navigation.openForm({ entityName: '${entityName}' });
14459
+ await refreshGrid();
14460
+ } else {
14461
+ console.warn('[grid] Add Existing requires Xrm.Navigation; not available in this host.');
14462
+ }
14463
+ return;
14464
+ }`;
14261
14465
  return `
14262
14466
  const queryClient = useQueryClient();
14263
14467
  const gridUpdateMutation = useUpdateRecord<Record<string, unknown>>('${entitySetName}');
@@ -14281,7 +14485,7 @@ function buildGridCommandHandlerBlock(args) {
14281
14485
  } catch (err) {
14282
14486
  setDeleteError((err as Error)?.message ?? 'Delete failed.');
14283
14487
  }
14284
- }, [pendingDelete, gridDeleteMutation]);
14488
+ }, [pendingDelete, gridDeleteMutation]);${createStateBlock}${addExistingStateBlock}
14285
14489
  const handleGridCommand = React.useCallback(async (actionType: string, overrideRowIndex?: number) => {
14286
14490
  const xrm = (typeof window !== 'undefined' ? (window as unknown as { Xrm?: any }).Xrm : undefined);
14287
14491
  const items = ${itemsExpr} as ReadonlyArray<Record<string, unknown>>;
@@ -14306,24 +14510,8 @@ function buildGridCommandHandlerBlock(args) {
14306
14510
  return true;
14307
14511
  };
14308
14512
  switch (actionType) {
14309
- case 'new': {
14310
- if (xrm?.Navigation?.openForm) {
14311
- await xrm.Navigation.openForm({ entityName: '${entityName}', useQuickCreateForm: false });
14312
- await refreshGrid();
14313
- } else {
14314
- console.warn('[grid] New action requires Xrm.Navigation.openForm; not available in this host.');
14315
- }
14316
- return;
14317
- }
14318
- case 'addExisting': {
14319
- if (xrm?.Navigation?.openForm) {
14320
- await xrm.Navigation.openForm({ entityName: '${entityName}' });
14321
- await refreshGrid();
14322
- } else {
14323
- console.warn('[grid] Add Existing requires Xrm.Navigation; not available in this host.');
14324
- }
14325
- return;
14326
- }
14513
+ case 'new': ${newCase}
14514
+ case 'addExisting': ${addExistingCase}
14327
14515
  case 'edit': {
14328
14516
  if (!requireSel('edit')) return;
14329
14517
  if (selectedIds.length > 1 && xrm?.Navigation?.openBulkEditForm) {
@@ -14359,10 +14547,7 @@ function buildGridCommandHandlerBlock(args) {
14359
14547
  await refreshGrid();
14360
14548
  return;
14361
14549
  }
14362
- case 'export': {
14363
- console.warn('[grid] Export to Excel is not yet wired in generated forms.');
14364
- return;
14365
- }
14550
+ case 'export': ${exportCase}
14366
14551
  case 'bulkEdit': {
14367
14552
  if (!requireSel('bulk edit')) return;
14368
14553
  if (xrm?.Navigation?.openBulkEditForm) {
@@ -14376,10 +14561,10 @@ function buildGridCommandHandlerBlock(args) {
14376
14561
  default:
14377
14562
  return;
14378
14563
  }
14379
- }, [${itemsExpr}, ${selectionExpr}, gridUpdateMutation, refreshGrid]);`;
14564
+ }, [${itemsExpr}, ${selectionExpr}, gridUpdateMutation, refreshGrid${addExisting ? ", gridAddExistingMutation" : ""}]);`;
14380
14565
  }
14381
14566
  function buildRowCommandsLiteral(contextMenuItems) {
14382
- return contextMenuItems.filter((ci) => (ci.actionType ?? "custom") !== "custom").map(
14567
+ return contextMenuItems.filter((ci) => (ci.actionType ?? "custom") !== "custom").filter((ci) => ci.actionType !== "new").map(
14383
14568
  (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); } }`
14384
14569
  ).join(",\n ");
14385
14570
  }
@@ -14415,6 +14600,68 @@ function buildGridDeleteDialogJsx(entityName) {
14415
14600
  </DialogFooter>
14416
14601
  </Dialog>`;
14417
14602
  }
14603
+ function buildGridCreateDialogJsx(entityName) {
14604
+ return `
14605
+ <Dialog
14606
+ hidden={!pendingCreate}
14607
+ onDismiss={() => { if (!gridCreateMutation.isPending) setPendingCreate(false); }}
14608
+ dialogContentProps={{
14609
+ type: DialogType.normal,
14610
+ title: ${JSON.stringify("Create " + entityName)},
14611
+ }}
14612
+ modalProps={{ isBlocking: true }}
14613
+ >
14614
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
14615
+ {createFields.map((f) => {
14616
+ if (f.kind === 'boolean') {
14617
+ return (
14618
+ <Checkbox
14619
+ key={f.key}
14620
+ label={f.label}
14621
+ checked={createValues[f.key] === true}
14622
+ onChange={(_ev, checked) => setCreateValues((prev) => ({ ...prev, [f.key]: !!checked }))}
14623
+ />
14624
+ );
14625
+ }
14626
+ if (f.kind === 'optionset') {
14627
+ return (
14628
+ <Dropdown
14629
+ key={f.key}
14630
+ label={f.label}
14631
+ options={f.options ?? []}
14632
+ selectedKey={(createValues[f.key] as number | undefined) ?? null}
14633
+ onChange={(_ev, opt) => setCreateValues((prev) => ({ ...prev, [f.key]: opt?.key }))}
14634
+ />
14635
+ );
14636
+ }
14637
+ return (
14638
+ <TextField
14639
+ key={f.key}
14640
+ label={f.label}
14641
+ type={f.inputType ?? 'text'}
14642
+ value={createValues[f.key] == null ? '' : String(createValues[f.key])}
14643
+ onChange={(_ev, val) => setCreateValues((prev) => ({ ...prev, [f.key]: val }))}
14644
+ />
14645
+ );
14646
+ })}
14647
+ </div>
14648
+ {createError && (
14649
+ <div style={{ color: '#a4262c', fontSize: 12, marginTop: 8 }}>{createError}</div>
14650
+ )}
14651
+ <DialogFooter>
14652
+ <PrimaryButton
14653
+ onClick={submitCreate}
14654
+ text={gridCreateMutation.isPending ? 'Creating\u2026' : 'Create'}
14655
+ disabled={gridCreateMutation.isPending}
14656
+ />
14657
+ <DefaultButton
14658
+ onClick={() => setPendingCreate(false)}
14659
+ text="Cancel"
14660
+ disabled={gridCreateMutation.isPending}
14661
+ />
14662
+ </DialogFooter>
14663
+ </Dialog>`;
14664
+ }
14418
14665
  function buildV9RenderCell(col) {
14419
14666
  const safeFieldName = JSON.stringify(col.fieldName);
14420
14667
  switch (col.rendererType) {
@@ -14777,12 +15024,18 @@ function generateLinkedSubgrid(gridDef, entityName, imports, library = "fluent-v
14777
15024
  const showRefresh = gridDef.toolbar?.showRefresh ?? false;
14778
15025
  const showColumnChooser = gridDef.toolbar?.showColumnChooser ?? false;
14779
15026
  const hasToolbarIcons = showFilters || showViewToggle || showExport || showRefresh || showColumnChooser;
15027
+ const childGridDef = gridDef.nestedGridId ? _currentGridCustomizers.find((g) => g.id === gridDef.nestedGridId) : void 0;
15028
+ const _aeRel = gridDef.nestedRelationship;
15029
+ const addExistingFeasible = Boolean(
15030
+ childGridDef && _includeDataAccessLayer && _aeRel?.relationshipType === "OneToMany" && _aeRel?.parentField && _aeRel?.childField && gridDef.dataSource?.entitySetName && gridDef.dataSource?.fetchXml && childGridDef.dataSource?.entitySetName && childGridDef.dataSource?.entityName && childGridDef.dataSource?.fetchXml
15031
+ );
14780
15032
  const barItems = (gridDef.commandBarItems ?? []).filter(
14781
15033
  (ci) => ci.visibility === "commandBar" || ci.visibility === "both" || !ci.visibility
14782
- );
15034
+ ).filter((ci) => ci.actionType !== "addExisting" || addExistingFeasible);
14783
15035
  const contextMenuItems = (gridDef.commandBarItems ?? []).filter(
14784
15036
  (ci) => ci.visibility === "contextMenu" || ci.visibility === "both" || !ci.visibility
14785
- );
15037
+ ).filter((ci) => ci.actionType !== "addExisting" || addExistingFeasible);
15038
+ const enableAddExisting = addExistingFeasible && (barItems.some((ci) => ci.actionType === "addExisting") || contextMenuItems.some((ci) => ci.actionType === "addExisting"));
14786
15039
  const _handlerAvailable = (
14787
15040
  // The dispatcher calls useUpdateRecord/useDeleteRecord/useQueryClient — which only
14788
15041
  // exist (module + provider + dep) when the data-access layer is included. Without it,
@@ -14792,6 +15045,8 @@ function generateLinkedSubgrid(gridDef, entityName, imports, library = "fluent-v
14792
15045
  // need the dispatcher in scope even when no wired commandBarItems exist.
14793
15046
  !!gridDef.toolbar?.showRefresh)
14794
15047
  );
15048
+ const enableExport = _handlerAvailable && (showExport || barItems.some((ci) => ci.actionType === "export") || contextMenuItems.some((ci) => ci.actionType === "export"));
15049
+ const enableCreate = _handlerAvailable && barItems.some((ci) => ci.actionType === "new");
14795
15050
  if (gridDef.showCommandBar && (barItems.length > 0 || showSearch || hasToolbarIcons)) {
14796
15051
  if (showSearch) imports.add("SearchBox");
14797
15052
  imports.add("DefaultButton");
@@ -14843,10 +15098,12 @@ function generateLinkedSubgrid(gridDef, entityName, imports, library = "fluent-v
14843
15098
  toolbarIcons.push(
14844
15099
  `<IconButton iconProps={{ iconName: 'ColumnOptions' }} title="Column chooser" ariaLabel="Column chooser" />`
14845
15100
  );
14846
- if (showExport)
15101
+ if (showExport) {
15102
+ const exportOnClick = _handlerAvailable ? ` onClick={() => { void handleGridCommand('export'); }}` : "";
14847
15103
  toolbarIcons.push(
14848
- `<IconButton iconProps={{ iconName: 'Download' }} title="Export" ariaLabel="Export" />`
15104
+ `<IconButton iconProps={{ iconName: 'Download' }} title="Export" ariaLabel="Export"${exportOnClick} />`
14849
15105
  );
15106
+ }
14850
15107
  if (showRefresh) {
14851
15108
  const refreshOnClick = _handlerAvailable ? ` onClick={() => { void handleGridCommand('refresh'); }}` : "";
14852
15109
  toolbarIcons.push(
@@ -14867,9 +15124,11 @@ function generateLinkedSubgrid(gridDef, entityName, imports, library = "fluent-v
14867
15124
  </div>
14868
15125
  </div>`;
14869
15126
  }
14870
- const childGridDef = gridDef.nestedGridId ? _currentGridCustomizers.find((g) => g.id === gridDef.nestedGridId) : void 0;
14871
15127
  const isGridKitNested = !!childGridDef;
14872
15128
  const visibleCols = [...gridDef.columns].filter((c) => c.isVisible).sort((a, b) => a.order - b.order);
15129
+ const exportColumnsLiteral = enableExport ? buildExportColumnsLiteral(visibleCols) : "";
15130
+ const primaryIdAttr = resolvePrimaryIdAttribute(gridDef);
15131
+ const createColumnsLiteral = enableCreate ? buildCreateFields(visibleCols, primaryIdAttr) : "";
14873
15132
  visibleCols.filter((c) => c.rendererType !== "text");
14874
15133
  const colEntries = isGridKitNested ? "" : generateColumnEntries(visibleCols, library);
14875
15134
  const selectionMode = gridDef.selectionMode === "multiple" ? "SelectionMode.multiple" : gridDef.selectionMode === "single" ? "SelectionMode.single" : "SelectionMode.none";
@@ -14935,19 +15194,38 @@ ${childEntries.join(",\n")},
14935
15194
  entitySetName: gridDef.dataSource.entitySetName,
14936
15195
  primaryIdAttribute: resolvePrimaryIdAttribute(gridDef),
14937
15196
  itemsExpr: "keyedItems",
14938
- selectionExpr: "selectedIndices"
15197
+ selectionExpr: "selectedIndices",
15198
+ exportColumnsLiteral,
15199
+ createColumnsLiteral,
15200
+ // Associate an existing child to the clicked/active parent (the ONLY call site
15201
+ // that wires it). Shared by the nested AND focused-view emitters below — both
15202
+ // reuse this ngHandlerBlock. Gated on enableAddExisting (feasible AND an
15203
+ // addExisting item present); the && chain narrows the optional entity-set strings
15204
+ // to `string` (enableAddExisting ⟹ addExistingFeasible guarantees they're present).
15205
+ addExisting: enableAddExisting && childEntitySet && childEntityLogical && parentEntitySet && nestedRel?.childField ? {
15206
+ childEntitySet,
15207
+ childEntityLogical,
15208
+ childField: nestedRel.childField,
15209
+ parentEntitySet
15210
+ } : null
14939
15211
  }) : "";
14940
15212
  const ngHasDelete = (gridDef.commandBarItems ?? []).some(
14941
15213
  (ci) => ci.actionType === "delete"
14942
15214
  );
14943
15215
  const ngDeleteDialogJsx = ngHasDelete && ngHandlerBlock !== "" ? buildGridDeleteDialogJsx(gridDef.dataSource?.entityName ?? entityName) : "";
14944
- if (ngDeleteDialogJsx) {
15216
+ const ngCreateDialogJsx = ngHandlerBlock !== "" && createColumnsLiteral ? buildGridCreateDialogJsx(gridDef.dataSource?.entityName ?? entityName) : "";
15217
+ if (ngDeleteDialogJsx || ngCreateDialogJsx) {
14945
15218
  imports.add("Dialog");
14946
15219
  imports.add("DialogType");
14947
15220
  imports.add("DialogFooter");
14948
15221
  imports.add("PrimaryButton");
14949
15222
  imports.add("DefaultButton");
14950
15223
  }
15224
+ if (ngCreateDialogJsx) {
15225
+ imports.add("TextField");
15226
+ imports.add("Dropdown");
15227
+ imports.add("Checkbox");
15228
+ }
14951
15229
  const ngRowCommandsLiteral = _handlerAvailable && (ngHasCommandBar || ngHasContextMenu && useLiveNested) ? buildRowCommandsLiteral(contextMenuItems) : "";
14952
15230
  const ngToolbarWired = (gridDef.commandBarItems ?? []).some(
14953
15231
  (ci) => (ci.actionType ?? "custom") !== "custom"
@@ -14961,9 +15239,18 @@ ${childEntries.join(",\n")},
14961
15239
  const ngDataverseHooks = [
14962
15240
  ...useLiveNested ? ["useDataverseQuery"] : [],
14963
15241
  ...useLiveNested && ngNeedsUpdate || ngHasHandler ? ["useUpdateRecord"] : [],
14964
- ...useLiveNested && ngNeedsDelete || ngHasHandler ? ["useDeleteRecord"] : []
15242
+ ...useLiveNested && ngNeedsDelete || ngHasHandler ? ["useDeleteRecord"] : [],
15243
+ // Create mutation only when the create dialog is wired (the `new` case's
15244
+ // non-Xrm fallback) — gated like the export util import to stay byte-stable
15245
+ // for non-create handler grids.
15246
+ ...ngHasHandler && createColumnsLiteral ? ["useCreateRecord"] : []
15247
+ ];
15248
+ const ngCommandBarImports = [
15249
+ ...useLiveNested && ngNeedsDelete || ngHasHandler ? [`import { useQueryClient } from '@tanstack/react-query';`] : [],
15250
+ // Export case (shared dispatcher → both nested + focused-view) calls
15251
+ // exportToFile/generateDefaultFilename when export is wired.
15252
+ ...ngHasHandler && enableExport ? [`import { exportToFile, generateDefaultFilename } from '../lib/grid-kit/utils';`] : []
14965
15253
  ];
14966
- const ngCommandBarImports = useLiveNested && ngNeedsDelete || ngHasHandler ? [`import { useQueryClient } from '@tanstack/react-query';`] : [];
14967
15254
  const ngParentSelectionMode = ngHasCommandBar ? gridDef.selectionMode === "single" ? "single" : "multiple" : gridDef.selectionMode ?? "none";
14968
15255
  const ngChildSaveBack = Boolean(
14969
15256
  useLiveNested && live && (childGridDef.isEditable ?? false)
@@ -15012,6 +15299,7 @@ ${childEntries.join(",\n")},
15012
15299
  commandBarBlock: gridCommandBarBlock,
15013
15300
  handlerBlock: ngHandlerBlock,
15014
15301
  deleteDialogJsx: ngDeleteDialogJsx,
15302
+ createDialogJsx: ngCreateDialogJsx,
15015
15303
  hasSelection: ngHasCommandBar,
15016
15304
  hasHandler: ngHandlerBlock !== "",
15017
15305
  dataverseHooks: ngDataverseHooks,
@@ -15067,6 +15355,7 @@ ${childEntries.join(",\n")},
15067
15355
  commandBarBlock: gridCommandBarBlock,
15068
15356
  handlerBlock: ngHandlerBlock,
15069
15357
  deleteDialogJsx: ngDeleteDialogJsx,
15358
+ createDialogJsx: ngCreateDialogJsx,
15070
15359
  hasSelection: ngHasCommandBar,
15071
15360
  dataverseHooks: ngDataverseHooks,
15072
15361
  commandBarImports: ngCommandBarImports,
@@ -15147,11 +15436,16 @@ ${childEntries.join(",\n")},
15147
15436
  entitySetName: gridDef.dataSource.entitySetName,
15148
15437
  primaryIdAttribute: resolvePrimaryIdAttribute(gridDef),
15149
15438
  itemsExpr: "rows",
15150
- selectionExpr: "selectedIndices"
15439
+ selectionExpr: "selectedIndices",
15440
+ exportColumnsLiteral,
15441
+ createColumnsLiteral,
15442
+ addExisting: null
15443
+ // flat (no parent relationship) → addExisting filtered out above
15151
15444
  }) : "";
15152
15445
  const cardSelectedIndicesState = cardEmitHandler ? "\n const [selectedIndices, setSelectedIndices] = React.useState<Set<number>>(new Set());" : "";
15153
15446
  const cardRowCommandsLiteral = cardEmitRowCommands ? buildRowCommandsLiteral(contextMenuItems) : "";
15154
15447
  const cardDeleteDialogJsx = cardEmitRowCommands && flatHasDelete || cardEmitToolbar && flatBarHasDelete ? buildGridDeleteDialogJsx(gridDef.dataSource?.entityName ?? entityName) : "";
15448
+ const cardCreateDialogJsx = cardEmitHandler && createColumnsLiteral ? buildGridCreateDialogJsx(gridDef.dataSource?.entityName ?? entityName) : "";
15155
15449
  const cardCommandBarBlock = cardEmitToolbar ? gridCommandBarBlock : "";
15156
15450
  const cardSelectionModeAttr = cardEmitToolbar ? flatSelectionModeAttr : "";
15157
15451
  const cardOnSelectionChangedAttr = cardEmitToolbar ? flatOnSelectionChangedAttr : "";
@@ -15170,20 +15464,26 @@ ${childEntries.join(",\n")},
15170
15464
  handlerBlock: cardHandlerBlock,
15171
15465
  rowCommandsLiteral: cardRowCommandsLiteral,
15172
15466
  deleteDialogJsx: cardDeleteDialogJsx,
15467
+ createDialogJsx: cardCreateDialogJsx,
15173
15468
  commandBarBlock: cardCommandBarBlock,
15174
15469
  selectionModeAttr: cardSelectionModeAttr,
15175
15470
  onSelectionChangedAttr: cardOnSelectionChangedAttr
15176
15471
  })
15177
15472
  ];
15178
15473
  _usesGridKit = true;
15179
- const cardDataverseImport = cardEmitHandler ? `import { useDataverseQuery, useUpdateRecord, useDeleteRecord } from '../lib/dataverse';` : `import { useDataverseQuery } from '../lib/dataverse';`;
15180
- if (cardDeleteDialogJsx) {
15474
+ const cardDataverseImport = cardEmitHandler ? `import { useDataverseQuery, useUpdateRecord, useDeleteRecord${createColumnsLiteral ? ", useCreateRecord" : ""} } from '../lib/dataverse';` : `import { useDataverseQuery } from '../lib/dataverse';`;
15475
+ if (cardDeleteDialogJsx || cardCreateDialogJsx) {
15181
15476
  imports.add("Dialog");
15182
15477
  imports.add("DialogType");
15183
15478
  imports.add("DialogFooter");
15184
15479
  imports.add("PrimaryButton");
15185
15480
  imports.add("DefaultButton");
15186
15481
  }
15482
+ if (cardCreateDialogJsx) {
15483
+ imports.add("TextField");
15484
+ imports.add("Dropdown");
15485
+ imports.add("Checkbox");
15486
+ }
15187
15487
  return {
15188
15488
  imports,
15189
15489
  v9Imports,
@@ -15191,7 +15491,10 @@ ${childEntries.join(",\n")},
15191
15491
  extraImports: cardIsLive ? cardEmitHandler ? [
15192
15492
  cardDataverseImport,
15193
15493
  `import { useQueryClient } from '@tanstack/react-query';`,
15194
- `import { CardGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`
15494
+ `import { CardGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`,
15495
+ // Export case calls exportToFile/generateDefaultFilename (cardEmitHandler
15496
+ // here; the export case is only real when enableExport).
15497
+ ...enableExport ? [`import { exportToFile, generateDefaultFilename } from '../lib/grid-kit/utils';`] : []
15195
15498
  ] : [
15196
15499
  `import { useDataverseQuery } from '../lib/dataverse';`,
15197
15500
  `import { CardGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`
@@ -15229,11 +15532,16 @@ ${childEntries.join(",\n")},
15229
15532
  entitySetName: gridDef.dataSource.entitySetName,
15230
15533
  primaryIdAttribute: resolvePrimaryIdAttribute(gridDef),
15231
15534
  itemsExpr: "rows",
15232
- selectionExpr: "selectedIndices"
15535
+ selectionExpr: "selectedIndices",
15536
+ exportColumnsLiteral,
15537
+ createColumnsLiteral,
15538
+ addExisting: null
15539
+ // flat (no parent relationship) → addExisting filtered out above
15233
15540
  }) : "";
15234
15541
  const roSelectedIndicesState = roEmitHandler ? "\n const [selectedIndices, setSelectedIndices] = React.useState<Set<number>>(new Set());" : "";
15235
15542
  const roRowCommandsLiteral = roEmitRowCommands ? buildRowCommandsLiteral(contextMenuItems) : "";
15236
15543
  const roDeleteDialogJsx = roEmitRowCommands && flatHasDelete ? buildGridDeleteDialogJsx(gridDef.dataSource?.entityName ?? entityName) : "";
15544
+ const roCreateDialogJsx = roEmitHandler && createColumnsLiteral ? buildGridCreateDialogJsx(gridDef.dataSource?.entityName ?? entityName) : "";
15237
15545
  const roCommandBarBlock = roEmitToolbar ? gridCommandBarBlock : "";
15238
15546
  const helperComponents = [
15239
15547
  buildLiveReadOnlySubgridBody({
@@ -15252,18 +15560,24 @@ ${childEntries.join(",\n")},
15252
15560
  handlerBlock: roHandlerBlock,
15253
15561
  rowCommandsLiteral: roRowCommandsLiteral,
15254
15562
  deleteDialogJsx: roDeleteDialogJsx,
15563
+ createDialogJsx: roCreateDialogJsx,
15255
15564
  commandBarBlock: roCommandBarBlock
15256
15565
  })
15257
15566
  ];
15258
15567
  if (!isV9) _usesGridKit = true;
15259
- const roDataverseImport = roEmitHandler ? `import { useDataverseQuery, useUpdateRecord, useDeleteRecord } from '../lib/dataverse';` : `import { useDataverseQuery } from '../lib/dataverse';`;
15260
- if (roDeleteDialogJsx) {
15568
+ const roDataverseImport = roEmitHandler ? `import { useDataverseQuery, useUpdateRecord, useDeleteRecord${createColumnsLiteral ? ", useCreateRecord" : ""} } from '../lib/dataverse';` : `import { useDataverseQuery } from '../lib/dataverse';`;
15569
+ if (roDeleteDialogJsx || roCreateDialogJsx) {
15261
15570
  imports.add("Dialog");
15262
15571
  imports.add("DialogType");
15263
15572
  imports.add("DialogFooter");
15264
15573
  imports.add("PrimaryButton");
15265
15574
  imports.add("DefaultButton");
15266
15575
  }
15576
+ if (roCreateDialogJsx) {
15577
+ imports.add("TextField");
15578
+ imports.add("Dropdown");
15579
+ imports.add("Checkbox");
15580
+ }
15267
15581
  return {
15268
15582
  imports,
15269
15583
  v9Imports,
@@ -15271,7 +15585,8 @@ ${childEntries.join(",\n")},
15271
15585
  extraImports: isV9 ? [`import { useDataverseQuery } from '../lib/dataverse';`] : roEmitHandler ? [
15272
15586
  roDataverseImport,
15273
15587
  `import { useQueryClient } from '@tanstack/react-query';`,
15274
- `import { ReadOnlyGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`
15588
+ `import { ReadOnlyGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`,
15589
+ ...enableExport ? [`import { exportToFile, generateDefaultFilename } from '../lib/grid-kit/utils';`] : []
15275
15590
  ] : [
15276
15591
  `import { useDataverseQuery } from '../lib/dataverse';`,
15277
15592
  `import { ReadOnlyGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`
@@ -15331,11 +15646,16 @@ const ${liveWrapperName}: React.FC = () => {
15331
15646
  entitySetName: gridDef.dataSource.entitySetName,
15332
15647
  primaryIdAttribute: resolvePrimaryIdAttribute(gridDef),
15333
15648
  itemsExpr: "keyedItems",
15334
- selectionExpr: "selectedIndices"
15649
+ selectionExpr: "selectedIndices",
15650
+ exportColumnsLiteral,
15651
+ createColumnsLiteral,
15652
+ addExisting: null
15653
+ // flat (no parent relationship) → addExisting filtered out above
15335
15654
  }) : "";
15336
15655
  const edSelectedIndicesState = edEmitHandler ? "\n const [selectedIndices, setSelectedIndices] = React.useState<Set<number>>(new Set());" : "";
15337
15656
  const edRowCommandsLiteral = edEmitRowCommands ? buildRowCommandsLiteral(contextMenuItems) : "";
15338
15657
  const edDeleteDialogJsx = edEmitRowCommands && flatHasDelete || edEmitToolbar && flatBarHasDelete ? buildGridDeleteDialogJsx(gridDef.dataSource?.entityName ?? entityName) : "";
15658
+ const edCreateDialogJsx = edEmitHandler && createColumnsLiteral ? buildGridCreateDialogJsx(gridDef.dataSource?.entityName ?? entityName) : "";
15339
15659
  const edCommandBarBlock = edEmitToolbar ? gridCommandBarBlock : "";
15340
15660
  const edSelectionModeAttr = edEmitToolbar ? flatSelectionModeAttr : "";
15341
15661
  const edOnSelectionChangedAttr = edEmitToolbar ? flatOnSelectionChangedAttr : "";
@@ -15352,6 +15672,7 @@ const ${liveWrapperName}: React.FC = () => {
15352
15672
  handlerBlock: edHandlerBlock,
15353
15673
  rowCommandsLiteral: edRowCommandsLiteral,
15354
15674
  deleteDialogJsx: edDeleteDialogJsx,
15675
+ createDialogJsx: edCreateDialogJsx,
15355
15676
  commandBarBlock: edCommandBarBlock,
15356
15677
  selectionModeAttr: edSelectionModeAttr,
15357
15678
  onSelectionChangedAttr: edOnSelectionChangedAttr
@@ -15363,18 +15684,24 @@ const ${liveWrapperName}: React.FC = () => {
15363
15684
  const jsx3 = editableUsesLiveData && liveWrapperName ? `{/* Subgrid: ${safeEntityName} \u2014 ${safeGridName} (editable, live) */}${todoComment}
15364
15685
  <${liveWrapperName} />` : `{/* Subgrid: ${safeEntityName} \u2014 ${safeGridName} (editable) */}${todoComment}
15365
15686
  <${componentName} items={${itemsLiteral}} />`;
15366
- const edDataverseImport = edEmitHandler ? `import { useDataverseQuery, useUpdateRecord, useDeleteRecord } from '../lib/dataverse';` : `import { useDataverseQuery, useUpdateRecord } from '../lib/dataverse';`;
15367
- if (edDeleteDialogJsx) {
15687
+ const edDataverseImport = edEmitHandler ? `import { useDataverseQuery, useUpdateRecord, useDeleteRecord${createColumnsLiteral ? ", useCreateRecord" : ""} } from '../lib/dataverse';` : `import { useDataverseQuery, useUpdateRecord } from '../lib/dataverse';`;
15688
+ if (edDeleteDialogJsx || edCreateDialogJsx) {
15368
15689
  imports.add("Dialog");
15369
15690
  imports.add("DialogType");
15370
15691
  imports.add("DialogFooter");
15371
15692
  imports.add("PrimaryButton");
15372
15693
  imports.add("DefaultButton");
15373
15694
  }
15695
+ if (edCreateDialogJsx) {
15696
+ imports.add("TextField");
15697
+ imports.add("Dropdown");
15698
+ imports.add("Checkbox");
15699
+ }
15374
15700
  const extraImports2 = editableUsesLiveData ? edEmitHandler ? [
15375
15701
  edDataverseImport,
15376
15702
  `import { useQueryClient } from '@tanstack/react-query';`,
15377
- `import { DataGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`
15703
+ `import { DataGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`,
15704
+ ...enableExport ? [`import { exportToFile, generateDefaultFilename } from '../lib/grid-kit/utils';`] : []
15378
15705
  ] : [
15379
15706
  `import { useDataverseQuery, useUpdateRecord } from '../lib/dataverse';`,
15380
15707
  `import { DataGrid, createCellRegistry, type ColumnDef } from '../lib/grid-kit';`
@@ -18183,6 +18510,19 @@ ${code}
18183
18510
  };
18184
18511
  `;
18185
18512
  }
18513
+ function dedupeImportStatements(content) {
18514
+ const isImportLine = (l) => /^import\b[^\n]*\bfrom\b[^\n]*;[ \t]*$/.test(l) || /^import\s+['"][^'"]+['"];[ \t]*$/.test(l);
18515
+ const seen = /* @__PURE__ */ new Set();
18516
+ const out = [];
18517
+ for (const line of content.split("\n")) {
18518
+ if (isImportLine(line)) {
18519
+ if (seen.has(line)) continue;
18520
+ seen.add(line);
18521
+ }
18522
+ out.push(line);
18523
+ }
18524
+ return out.join("\n");
18525
+ }
18186
18526
  function generateFormCode(form, options) {
18187
18527
  _currentGridCustomizers = options?.gridCustomizers ?? [];
18188
18528
  _includeSampleData = options?.includeSampleData ?? false;
@@ -18213,6 +18553,7 @@ function generateFormCode(form, options) {
18213
18553
  default:
18214
18554
  content = generateMainForm(form, hasRules, library);
18215
18555
  }
18556
+ content = dedupeImportStatements(content);
18216
18557
  const files = [
18217
18558
  { path: `src/components/${fileName}`, content }
18218
18559
  ];