@nubitio/crud 0.5.19 → 0.5.22

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
@@ -2110,7 +2110,7 @@ function buildToolbar(options, t, onAddClick, includeAddAction = true) {
2110
2110
  showRefresh: options.toolbar?.showRefresh ?? true
2111
2111
  };
2112
2112
  }
2113
- function SummaryFooter({ fields, hasCheckbox, hasDetail, hasRowActions, rows, summaryFields, footerRef, colWidths }) {
2113
+ function SummaryFooter({ fields, hasCheckbox, hasDetail, hasRowActions, rows, summaryFields, gridSummary, footerRef, colWidths }) {
2114
2114
  if (!summaryFields?.length) return null;
2115
2115
  const itemsByColumn = new Map(summaryFields.filter((item) => item.column).map((item) => [item.column, item]));
2116
2116
  const unboundItems = summaryFields.filter((item) => !item.column);
@@ -2138,7 +2138,7 @@ function SummaryFooter({ fields, hasCheckbox, hasDetail, hasRowActions, rows, su
2138
2138
  children: item.label
2139
2139
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2140
2140
  className: "nb-datagrid__summary-value",
2141
- children: resolveSummaryText(rows, item)
2141
+ children: item.column && gridSummary && item.column in gridSummary ? formatSummaryValue(gridSummary[item.column], item) : resolveSummaryText(rows, item)
2142
2142
  })]
2143
2143
  })
2144
2144
  }, field.name);
@@ -2159,6 +2159,7 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
2159
2159
  const [rows, setRows] = (0, react.useState)([]);
2160
2160
  const rowsRef = (0, react.useRef)([]);
2161
2161
  const [totalCount, setTotalCount] = (0, react.useState)(0);
2162
+ const [gridSummary, setGridSummary] = (0, react.useState)(null);
2162
2163
  const [selectedKeys, setSelectedKeys] = (0, react.useState)([]);
2163
2164
  const [filters, setFilters] = (0, react.useState)({});
2164
2165
  const [filterInputs, setFilterInputs] = (0, react.useState)({});
@@ -2340,6 +2341,7 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
2340
2341
  rowsRef.current = result.data;
2341
2342
  setRows(result.data);
2342
2343
  setTotalCount(result.totalCount);
2344
+ setGridSummary(result.gridSummary ?? null);
2343
2345
  setIsGridLoading(false);
2344
2346
  onContentReadyRef.current?.();
2345
2347
  return result.data;
@@ -3135,6 +3137,7 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
3135
3137
  hasRowActions,
3136
3138
  rows,
3137
3139
  summaryFields: options.summaryFields,
3140
+ gridSummary,
3138
3141
  footerRef: tfootRef,
3139
3142
  colWidths: resolvedColWidths
3140
3143
  })
@@ -3173,7 +3176,7 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
3173
3176
  children: item.label
3174
3177
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3175
3178
  className: "nb-datagrid__summary-value",
3176
- children: resolveSummaryText(rows, item)
3179
+ children: item.column && gridSummary && item.column in gridSummary ? formatSummaryValue(gridSummary[item.column], item) : resolveSummaryText(rows, item)
3177
3180
  })]
3178
3181
  }, index))
3179
3182
  }),
@@ -7466,14 +7469,16 @@ function ResourceSchemaProvider({ children, resolver }) {
7466
7469
  children
7467
7470
  });
7468
7471
  }
7469
- function resolveWithRuntimeErrors(resolver, supportedOperations = [], formLayout) {
7472
+ function resolveWithRuntimeErrors(resolver, supportedOperations = [], formLayout, workflow, summaryFields) {
7470
7473
  try {
7471
7474
  return {
7472
7475
  fields: resolver(),
7473
7476
  isLoading: false,
7474
7477
  error: void 0,
7475
7478
  supportedOperations,
7476
- formLayout
7479
+ formLayout,
7480
+ workflow,
7481
+ summaryFields
7477
7482
  };
7478
7483
  } catch (runtimeError) {
7479
7484
  return {
@@ -7481,7 +7486,9 @@ function resolveWithRuntimeErrors(resolver, supportedOperations = [], formLayout
7481
7486
  isLoading: false,
7482
7487
  error: runtimeError instanceof Error ? runtimeError : new Error(String(runtimeError)),
7483
7488
  supportedOperations,
7484
- formLayout
7489
+ formLayout,
7490
+ workflow,
7491
+ summaryFields
7485
7492
  };
7486
7493
  }
7487
7494
  }
@@ -7507,7 +7514,7 @@ function useResolvedResourceFields({ apiUrl, manualFields, overrides, fieldContr
7507
7514
  baselineFields: baseline.fields,
7508
7515
  contract: fieldContract,
7509
7516
  legacyOverrides: fieldContract ? void 0 : overrides
7510
- }), baseline.supportedOperations, baseline.formLayout);
7517
+ }), baseline.supportedOperations, baseline.formLayout, baseline.workflow, baseline.summaryFields);
7511
7518
  }, [
7512
7519
  baseline,
7513
7520
  fieldContract,
@@ -7515,6 +7522,28 @@ function useResolvedResourceFields({ apiUrl, manualFields, overrides, fieldContr
7515
7522
  ]);
7516
7523
  }
7517
7524
  //#endregion
7525
+ //#region packages/crud/workflow/buildWorkflowRowActions.ts
7526
+ function buildWorkflowRowActions(row, workflow, apiUrl, roles, onDone) {
7527
+ if (!workflow) return [];
7528
+ const current = String(row[workflow.field] ?? "");
7529
+ return workflow.transitions.filter((transition) => transition.from.includes(current)).filter((transition) => !transition.roles?.length || transition.roles.some((role) => roles.includes(role))).map((transition) => ({
7530
+ text: transition.label ?? transition.name,
7531
+ onClick: async () => {
7532
+ const base = apiUrl.replace(/\/$/, "");
7533
+ const id = row.id;
7534
+ const response = await fetch(`${base}/${id}/transition/${transition.name}`, {
7535
+ method: "POST",
7536
+ credentials: "include"
7537
+ });
7538
+ if (!response.ok) {
7539
+ const detail = await response.text().catch(() => "");
7540
+ throw new Error(detail || `Transition "${transition.name}" failed (${response.status})`);
7541
+ }
7542
+ onDone?.();
7543
+ }
7544
+ }));
7545
+ }
7546
+ //#endregion
7518
7547
  //#region packages/crud/crud/SmartCrudPage.tsx
7519
7548
  function CrudSkeleton() {
7520
7549
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -7587,7 +7616,7 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
7587
7616
  const effectiveGridRef = gridRef ?? internalGridRef;
7588
7617
  const resolvedBaseResource = (0, react.useMemo)(() => resolveCrudResource(resource), [resource]);
7589
7618
  const hasManualFields = !resource.fieldContract && Array.isArray(resource.fields) && resource.fields.length > 0;
7590
- const { fields, isLoading, error, supportedOperations, formLayout: inferredFormLayout } = useResolvedResourceFields({
7619
+ const { fields, isLoading, error, supportedOperations, formLayout: inferredFormLayout, workflow, summaryFields: inferredSummaryFields } = useResolvedResourceFields({
7591
7620
  apiUrl: resolvedBaseResource.apiUrl,
7592
7621
  manualFields: hasManualFields ? buildFields(resource.fields) : void 0,
7593
7622
  overrides: hasManualFields ? void 0 : fieldOverrides,
@@ -7611,30 +7640,42 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
7611
7640
  const routingState = useRouting(resource.routing);
7612
7641
  const { activeOperation, formData, handleFormDataChange, startCreate, startEdit, resetOperation } = useSmartCrudOperation(void 0, routingState);
7613
7642
  const roles = useSmartCrudRoles();
7614
- const { gridFields, processedFields, computedValues } = useSmartCrudFields(fields, activeOperation, formData, (0, react.useMemo)(() => roles ?? [], [roles]));
7643
+ const stableRoles = (0, react.useMemo)(() => roles ?? [], [roles]);
7644
+ const { gridFields, processedFields, computedValues } = useSmartCrudFields(fields, activeOperation, formData, stableRoles);
7615
7645
  const formFields = (0, react.useMemo)(() => applyFormDetailFormFieldOverrides(processedFields, resolvedBaseResource.formDetail), [processedFields, resolvedBaseResource.formDetail]);
7616
7646
  (0, _nubitio_core.useMercureSubscription)(resource.apiUrl, () => {
7617
7647
  effectiveGridRef.current?.refresh();
7618
7648
  }, resolvedBaseResource.mercure !== false);
7619
7649
  const normalizedApiUrl = resolvedBaseResource.apiUrl.startsWith("/") ? resolvedBaseResource.apiUrl : `/${resolvedBaseResource.apiUrl}`;
7620
- const resolvedResource = (0, react.useMemo)(() => ({
7621
- ...resolvedBaseResource,
7622
- ...!hasManualFields ? { fields: gridFields } : {},
7623
- apiUrl: normalizedApiUrl,
7624
- fields: hasManualFields ? buildFields(resource.fields) : gridFields,
7625
- formFields,
7626
- formLayout: resolvedBaseResource.formLayout ?? inferredFormLayout,
7627
- _supportedOperations: supportedOperations
7628
- }), [
7650
+ const resolvedResource = (0, react.useMemo)(() => {
7651
+ const rowActions = resolvedBaseResource.rowActions ?? (workflow ? (row) => buildWorkflowRowActions(row, workflow, normalizedApiUrl, stableRoles, () => {
7652
+ effectiveGridRef.current?.refresh();
7653
+ }) : void 0);
7654
+ return {
7655
+ ...resolvedBaseResource,
7656
+ ...!hasManualFields ? { fields: gridFields } : {},
7657
+ apiUrl: normalizedApiUrl,
7658
+ fields: hasManualFields ? buildFields(resource.fields) : gridFields,
7659
+ formFields,
7660
+ formLayout: resolvedBaseResource.formLayout ?? inferredFormLayout,
7661
+ summaryFields: resolvedBaseResource.summaryFields ?? inferredSummaryFields,
7662
+ _supportedOperations: supportedOperations,
7663
+ rowActions
7664
+ };
7665
+ }, [
7666
+ effectiveGridRef,
7629
7667
  fields,
7630
7668
  gridFields,
7631
7669
  hasManualFields,
7632
7670
  inferredFormLayout,
7671
+ inferredSummaryFields,
7633
7672
  normalizedApiUrl,
7634
7673
  formFields,
7635
7674
  resolvedBaseResource,
7636
7675
  resource.fields,
7637
- supportedOperations
7676
+ stableRoles,
7677
+ supportedOperations,
7678
+ workflow
7638
7679
  ]);
7639
7680
  if (!hasManualFields && isLoading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CrudSkeleton, {});
7640
7681
  if (!hasManualFields && error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CrudError, {
@@ -8460,24 +8501,6 @@ function ToolbarSelect({ id, label, icon = "ph-funnel", value, options, onChange
8460
8501
  });
8461
8502
  }
8462
8503
  //#endregion
8463
- //#region packages/crud/workflow/buildWorkflowRowActions.ts
8464
- function buildWorkflowRowActions(row, workflow, apiUrl, roles, onDone) {
8465
- if (!workflow) return [];
8466
- const current = String(row[workflow.field] ?? "");
8467
- return workflow.transitions.filter((transition) => transition.from.includes(current)).filter((transition) => !transition.roles?.length || transition.roles.some((role) => roles.includes(role))).map((transition) => ({
8468
- text: transition.label ?? transition.name,
8469
- onClick: async () => {
8470
- const base = apiUrl.replace(/\/$/, "");
8471
- const id = row.id;
8472
- await fetch(`${base}/${id}/transition/${transition.name}`, {
8473
- method: "POST",
8474
- credentials: "include"
8475
- });
8476
- onDone?.();
8477
- }
8478
- }));
8479
- }
8480
- //#endregion
8481
8504
  //#region packages/crud/adapter/RestAdapter.ts
8482
8505
  /**
8483
8506
  * Backend adapter for plain OpenAPI / REST backends.
@@ -8611,15 +8634,26 @@ function createRestResourceStore(dialect = {}) {
8611
8634
  if (result === null) return {
8612
8635
  data: [],
8613
8636
  totalCount: 0,
8614
- summary: null
8637
+ summary: null,
8638
+ gridSummary: null
8615
8639
  };
8616
8640
  const body = result.data;
8617
8641
  const data = Array.isArray(body) ? body : body.items ?? body.data ?? [];
8618
8642
  const headerTotal = Number(result.headers.get("x-total-count"));
8643
+ const totalCount = Array.isArray(body) ? Number.isFinite(headerTotal) && headerTotal > 0 ? headerTotal : body.length : body.total ?? body.totalCount ?? data.length;
8644
+ const gridSummaryHeader = result.headers.get("x-grid-summary");
8645
+ let gridSummary = null;
8646
+ if (gridSummaryHeader) try {
8647
+ const parsed = JSON.parse(gridSummaryHeader);
8648
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) gridSummary = parsed;
8649
+ } catch {
8650
+ gridSummary = null;
8651
+ }
8619
8652
  return {
8620
8653
  data,
8621
- totalCount: Array.isArray(body) ? Number.isFinite(headerTotal) && headerTotal > 0 ? headerTotal : body.length : body.total ?? body.totalCount ?? data.length,
8622
- summary: null
8654
+ totalCount,
8655
+ summary: null,
8656
+ gridSummary
8623
8657
  };
8624
8658
  },
8625
8659
  async byKey(key) {
package/dist/index.d.cts CHANGED
@@ -1549,6 +1549,8 @@ interface DataGridViewOptions {
1549
1549
  /** Per-row gate for the Delete action. Absent = allowed. */
1550
1550
  canDeleteRow?: (row: DataRecord$1) => boolean;
1551
1551
  summaryFields?: DataGridSummaryItem[];
1552
+ /** Server-side aggregates for the current filtered collection. */
1553
+ gridSummary?: Record<string, unknown> | null;
1552
1554
  filter?: FilterRule[];
1553
1555
  sort?: Array<{
1554
1556
  selector: string;
@@ -1919,6 +1921,7 @@ interface ResourceSchemaResolution {
1919
1921
  */
1920
1922
  formLayout?: FormLayout;
1921
1923
  workflow?: WorkflowSchema;
1924
+ summaryFields?: SummaryItem[];
1922
1925
  }
1923
1926
  interface ResourceSchemaResolver {
1924
1927
  useResourceSchema(request: ResourceSchemaRequest): ResourceSchemaResolution;
package/dist/index.d.mts CHANGED
@@ -1549,6 +1549,8 @@ interface DataGridViewOptions {
1549
1549
  /** Per-row gate for the Delete action. Absent = allowed. */
1550
1550
  canDeleteRow?: (row: DataRecord$1) => boolean;
1551
1551
  summaryFields?: DataGridSummaryItem[];
1552
+ /** Server-side aggregates for the current filtered collection. */
1553
+ gridSummary?: Record<string, unknown> | null;
1552
1554
  filter?: FilterRule[];
1553
1555
  sort?: Array<{
1554
1556
  selector: string;
@@ -1919,6 +1921,7 @@ interface ResourceSchemaResolution {
1919
1921
  */
1920
1922
  formLayout?: FormLayout;
1921
1923
  workflow?: WorkflowSchema;
1924
+ summaryFields?: SummaryItem[];
1922
1925
  }
1923
1926
  interface ResourceSchemaResolver {
1924
1927
  useResourceSchema(request: ResourceSchemaRequest): ResourceSchemaResolution;
package/dist/index.mjs CHANGED
@@ -2086,7 +2086,7 @@ function buildToolbar(options, t, onAddClick, includeAddAction = true) {
2086
2086
  showRefresh: options.toolbar?.showRefresh ?? true
2087
2087
  };
2088
2088
  }
2089
- function SummaryFooter({ fields, hasCheckbox, hasDetail, hasRowActions, rows, summaryFields, footerRef, colWidths }) {
2089
+ function SummaryFooter({ fields, hasCheckbox, hasDetail, hasRowActions, rows, summaryFields, gridSummary, footerRef, colWidths }) {
2090
2090
  if (!summaryFields?.length) return null;
2091
2091
  const itemsByColumn = new Map(summaryFields.filter((item) => item.column).map((item) => [item.column, item]));
2092
2092
  const unboundItems = summaryFields.filter((item) => !item.column);
@@ -2114,7 +2114,7 @@ function SummaryFooter({ fields, hasCheckbox, hasDetail, hasRowActions, rows, su
2114
2114
  children: item.label
2115
2115
  }), /* @__PURE__ */ jsx("span", {
2116
2116
  className: "nb-datagrid__summary-value",
2117
- children: resolveSummaryText(rows, item)
2117
+ children: item.column && gridSummary && item.column in gridSummary ? formatSummaryValue(gridSummary[item.column], item) : resolveSummaryText(rows, item)
2118
2118
  })]
2119
2119
  })
2120
2120
  }, field.name);
@@ -2135,6 +2135,7 @@ const NativeDataGridView = forwardRef((options, ref) => {
2135
2135
  const [rows, setRows] = useState([]);
2136
2136
  const rowsRef = useRef([]);
2137
2137
  const [totalCount, setTotalCount] = useState(0);
2138
+ const [gridSummary, setGridSummary] = useState(null);
2138
2139
  const [selectedKeys, setSelectedKeys] = useState([]);
2139
2140
  const [filters, setFilters] = useState({});
2140
2141
  const [filterInputs, setFilterInputs] = useState({});
@@ -2316,6 +2317,7 @@ const NativeDataGridView = forwardRef((options, ref) => {
2316
2317
  rowsRef.current = result.data;
2317
2318
  setRows(result.data);
2318
2319
  setTotalCount(result.totalCount);
2320
+ setGridSummary(result.gridSummary ?? null);
2319
2321
  setIsGridLoading(false);
2320
2322
  onContentReadyRef.current?.();
2321
2323
  return result.data;
@@ -3111,6 +3113,7 @@ const NativeDataGridView = forwardRef((options, ref) => {
3111
3113
  hasRowActions,
3112
3114
  rows,
3113
3115
  summaryFields: options.summaryFields,
3116
+ gridSummary,
3114
3117
  footerRef: tfootRef,
3115
3118
  colWidths: resolvedColWidths
3116
3119
  })
@@ -3149,7 +3152,7 @@ const NativeDataGridView = forwardRef((options, ref) => {
3149
3152
  children: item.label
3150
3153
  }), /* @__PURE__ */ jsx("span", {
3151
3154
  className: "nb-datagrid__summary-value",
3152
- children: resolveSummaryText(rows, item)
3155
+ children: item.column && gridSummary && item.column in gridSummary ? formatSummaryValue(gridSummary[item.column], item) : resolveSummaryText(rows, item)
3153
3156
  })]
3154
3157
  }, index))
3155
3158
  }),
@@ -7442,14 +7445,16 @@ function ResourceSchemaProvider({ children, resolver }) {
7442
7445
  children
7443
7446
  });
7444
7447
  }
7445
- function resolveWithRuntimeErrors(resolver, supportedOperations = [], formLayout) {
7448
+ function resolveWithRuntimeErrors(resolver, supportedOperations = [], formLayout, workflow, summaryFields) {
7446
7449
  try {
7447
7450
  return {
7448
7451
  fields: resolver(),
7449
7452
  isLoading: false,
7450
7453
  error: void 0,
7451
7454
  supportedOperations,
7452
- formLayout
7455
+ formLayout,
7456
+ workflow,
7457
+ summaryFields
7453
7458
  };
7454
7459
  } catch (runtimeError) {
7455
7460
  return {
@@ -7457,7 +7462,9 @@ function resolveWithRuntimeErrors(resolver, supportedOperations = [], formLayout
7457
7462
  isLoading: false,
7458
7463
  error: runtimeError instanceof Error ? runtimeError : new Error(String(runtimeError)),
7459
7464
  supportedOperations,
7460
- formLayout
7465
+ formLayout,
7466
+ workflow,
7467
+ summaryFields
7461
7468
  };
7462
7469
  }
7463
7470
  }
@@ -7483,7 +7490,7 @@ function useResolvedResourceFields({ apiUrl, manualFields, overrides, fieldContr
7483
7490
  baselineFields: baseline.fields,
7484
7491
  contract: fieldContract,
7485
7492
  legacyOverrides: fieldContract ? void 0 : overrides
7486
- }), baseline.supportedOperations, baseline.formLayout);
7493
+ }), baseline.supportedOperations, baseline.formLayout, baseline.workflow, baseline.summaryFields);
7487
7494
  }, [
7488
7495
  baseline,
7489
7496
  fieldContract,
@@ -7491,6 +7498,28 @@ function useResolvedResourceFields({ apiUrl, manualFields, overrides, fieldContr
7491
7498
  ]);
7492
7499
  }
7493
7500
  //#endregion
7501
+ //#region packages/crud/workflow/buildWorkflowRowActions.ts
7502
+ function buildWorkflowRowActions(row, workflow, apiUrl, roles, onDone) {
7503
+ if (!workflow) return [];
7504
+ const current = String(row[workflow.field] ?? "");
7505
+ return workflow.transitions.filter((transition) => transition.from.includes(current)).filter((transition) => !transition.roles?.length || transition.roles.some((role) => roles.includes(role))).map((transition) => ({
7506
+ text: transition.label ?? transition.name,
7507
+ onClick: async () => {
7508
+ const base = apiUrl.replace(/\/$/, "");
7509
+ const id = row.id;
7510
+ const response = await fetch(`${base}/${id}/transition/${transition.name}`, {
7511
+ method: "POST",
7512
+ credentials: "include"
7513
+ });
7514
+ if (!response.ok) {
7515
+ const detail = await response.text().catch(() => "");
7516
+ throw new Error(detail || `Transition "${transition.name}" failed (${response.status})`);
7517
+ }
7518
+ onDone?.();
7519
+ }
7520
+ }));
7521
+ }
7522
+ //#endregion
7494
7523
  //#region packages/crud/crud/SmartCrudPage.tsx
7495
7524
  function CrudSkeleton() {
7496
7525
  return /* @__PURE__ */ jsxs("div", {
@@ -7563,7 +7592,7 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
7563
7592
  const effectiveGridRef = gridRef ?? internalGridRef;
7564
7593
  const resolvedBaseResource = useMemo(() => resolveCrudResource(resource), [resource]);
7565
7594
  const hasManualFields = !resource.fieldContract && Array.isArray(resource.fields) && resource.fields.length > 0;
7566
- const { fields, isLoading, error, supportedOperations, formLayout: inferredFormLayout } = useResolvedResourceFields({
7595
+ const { fields, isLoading, error, supportedOperations, formLayout: inferredFormLayout, workflow, summaryFields: inferredSummaryFields } = useResolvedResourceFields({
7567
7596
  apiUrl: resolvedBaseResource.apiUrl,
7568
7597
  manualFields: hasManualFields ? buildFields(resource.fields) : void 0,
7569
7598
  overrides: hasManualFields ? void 0 : fieldOverrides,
@@ -7587,30 +7616,42 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
7587
7616
  const routingState = useRouting(resource.routing);
7588
7617
  const { activeOperation, formData, handleFormDataChange, startCreate, startEdit, resetOperation } = useSmartCrudOperation(void 0, routingState);
7589
7618
  const roles = useSmartCrudRoles();
7590
- const { gridFields, processedFields, computedValues } = useSmartCrudFields(fields, activeOperation, formData, useMemo(() => roles ?? [], [roles]));
7619
+ const stableRoles = useMemo(() => roles ?? [], [roles]);
7620
+ const { gridFields, processedFields, computedValues } = useSmartCrudFields(fields, activeOperation, formData, stableRoles);
7591
7621
  const formFields = useMemo(() => applyFormDetailFormFieldOverrides(processedFields, resolvedBaseResource.formDetail), [processedFields, resolvedBaseResource.formDetail]);
7592
7622
  useMercureSubscription(resource.apiUrl, () => {
7593
7623
  effectiveGridRef.current?.refresh();
7594
7624
  }, resolvedBaseResource.mercure !== false);
7595
7625
  const normalizedApiUrl = resolvedBaseResource.apiUrl.startsWith("/") ? resolvedBaseResource.apiUrl : `/${resolvedBaseResource.apiUrl}`;
7596
- const resolvedResource = useMemo(() => ({
7597
- ...resolvedBaseResource,
7598
- ...!hasManualFields ? { fields: gridFields } : {},
7599
- apiUrl: normalizedApiUrl,
7600
- fields: hasManualFields ? buildFields(resource.fields) : gridFields,
7601
- formFields,
7602
- formLayout: resolvedBaseResource.formLayout ?? inferredFormLayout,
7603
- _supportedOperations: supportedOperations
7604
- }), [
7626
+ const resolvedResource = useMemo(() => {
7627
+ const rowActions = resolvedBaseResource.rowActions ?? (workflow ? (row) => buildWorkflowRowActions(row, workflow, normalizedApiUrl, stableRoles, () => {
7628
+ effectiveGridRef.current?.refresh();
7629
+ }) : void 0);
7630
+ return {
7631
+ ...resolvedBaseResource,
7632
+ ...!hasManualFields ? { fields: gridFields } : {},
7633
+ apiUrl: normalizedApiUrl,
7634
+ fields: hasManualFields ? buildFields(resource.fields) : gridFields,
7635
+ formFields,
7636
+ formLayout: resolvedBaseResource.formLayout ?? inferredFormLayout,
7637
+ summaryFields: resolvedBaseResource.summaryFields ?? inferredSummaryFields,
7638
+ _supportedOperations: supportedOperations,
7639
+ rowActions
7640
+ };
7641
+ }, [
7642
+ effectiveGridRef,
7605
7643
  fields,
7606
7644
  gridFields,
7607
7645
  hasManualFields,
7608
7646
  inferredFormLayout,
7647
+ inferredSummaryFields,
7609
7648
  normalizedApiUrl,
7610
7649
  formFields,
7611
7650
  resolvedBaseResource,
7612
7651
  resource.fields,
7613
- supportedOperations
7652
+ stableRoles,
7653
+ supportedOperations,
7654
+ workflow
7614
7655
  ]);
7615
7656
  if (!hasManualFields && isLoading) return /* @__PURE__ */ jsx(CrudSkeleton, {});
7616
7657
  if (!hasManualFields && error) return /* @__PURE__ */ jsx(CrudError, {
@@ -8436,24 +8477,6 @@ function ToolbarSelect({ id, label, icon = "ph-funnel", value, options, onChange
8436
8477
  });
8437
8478
  }
8438
8479
  //#endregion
8439
- //#region packages/crud/workflow/buildWorkflowRowActions.ts
8440
- function buildWorkflowRowActions(row, workflow, apiUrl, roles, onDone) {
8441
- if (!workflow) return [];
8442
- const current = String(row[workflow.field] ?? "");
8443
- return workflow.transitions.filter((transition) => transition.from.includes(current)).filter((transition) => !transition.roles?.length || transition.roles.some((role) => roles.includes(role))).map((transition) => ({
8444
- text: transition.label ?? transition.name,
8445
- onClick: async () => {
8446
- const base = apiUrl.replace(/\/$/, "");
8447
- const id = row.id;
8448
- await fetch(`${base}/${id}/transition/${transition.name}`, {
8449
- method: "POST",
8450
- credentials: "include"
8451
- });
8452
- onDone?.();
8453
- }
8454
- }));
8455
- }
8456
- //#endregion
8457
8480
  //#region packages/crud/adapter/RestAdapter.ts
8458
8481
  /**
8459
8482
  * Backend adapter for plain OpenAPI / REST backends.
@@ -8587,15 +8610,26 @@ function createRestResourceStore(dialect = {}) {
8587
8610
  if (result === null) return {
8588
8611
  data: [],
8589
8612
  totalCount: 0,
8590
- summary: null
8613
+ summary: null,
8614
+ gridSummary: null
8591
8615
  };
8592
8616
  const body = result.data;
8593
8617
  const data = Array.isArray(body) ? body : body.items ?? body.data ?? [];
8594
8618
  const headerTotal = Number(result.headers.get("x-total-count"));
8619
+ const totalCount = Array.isArray(body) ? Number.isFinite(headerTotal) && headerTotal > 0 ? headerTotal : body.length : body.total ?? body.totalCount ?? data.length;
8620
+ const gridSummaryHeader = result.headers.get("x-grid-summary");
8621
+ let gridSummary = null;
8622
+ if (gridSummaryHeader) try {
8623
+ const parsed = JSON.parse(gridSummaryHeader);
8624
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) gridSummary = parsed;
8625
+ } catch {
8626
+ gridSummary = null;
8627
+ }
8595
8628
  return {
8596
8629
  data,
8597
- totalCount: Array.isArray(body) ? Number.isFinite(headerTotal) && headerTotal > 0 ? headerTotal : body.length : body.total ?? body.totalCount ?? data.length,
8598
- summary: null
8630
+ totalCount,
8631
+ summary: null,
8632
+ gridSummary
8599
8633
  };
8600
8634
  },
8601
8635
  async byKey(key) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubitio/crud",
3
- "version": "0.5.19",
3
+ "version": "0.5.22",
4
4
  "type": "module",
5
5
  "description": "Declarative CRUD engine with field DSL, forms, datagrids, RBAC, conditional logic and pluggable adapters (Hydra/REST).",
6
6
  "license": "MIT",
@@ -56,7 +56,7 @@
56
56
  "react-dom": "^19.0.0",
57
57
  "react-i18next": "^14.0.0",
58
58
  "react-router-dom": "^6.0.0",
59
- "@nubitio/core": "^0.5.19",
60
- "@nubitio/ui": "^0.5.19"
59
+ "@nubitio/core": "^0.5.22",
60
+ "@nubitio/ui": "^0.5.22"
61
61
  }
62
62
  }