@nubitio/crud 0.5.15 → 0.5.19

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
@@ -28,7 +28,6 @@ let react_dom = require("react-dom");
28
28
  let _nubitio_ui = require("@nubitio/ui");
29
29
  let react_jsx_runtime = require("react/jsx-runtime");
30
30
  let _nubitio_core = require("@nubitio/core");
31
- let react_dropzone = require("react-dropzone");
32
31
  let _tanstack_react_query = require("@tanstack/react-query");
33
32
  //#region packages/crud/crud/defineResource.ts
34
33
  const stringResourceCache = /* @__PURE__ */ new Map();
@@ -63,6 +62,17 @@ function defineResource(apiUrl, overrides) {
63
62
  return resource;
64
63
  }
65
64
  //#endregion
65
+ //#region packages/crud/crud/embeddedLinesUrl.ts
66
+ /**
67
+ * Builds a formDetail reload URL for {@code #[EmbeddedLines]} line entities.
68
+ *
69
+ * @example embeddedLinesUrl('/api/sales_document_lines', 'document')
70
+ * → '/api/sales_document_lines?document={id}'
71
+ */
72
+ function embeddedLinesUrl(route, parentQueryParam) {
73
+ return `${route}${route.includes("?") ? "&" : "?"}${parentQueryParam}={id}`;
74
+ }
75
+ //#endregion
66
76
  //#region packages/crud/datagrid/DataGridEvents.ts
67
77
  const DATA_GRID_EVENTS = {
68
78
  SELECTION_CHANGED: "datagrid:selection.changed",
@@ -1243,9 +1253,6 @@ const enumTypeModule = {
1243
1253
  };
1244
1254
  //#endregion
1245
1255
  //#region packages/crud/form/FileUploadField.tsx
1246
- function cx$1(...values) {
1247
- return values.filter(Boolean).join(" ");
1248
- }
1249
1256
  function resolveMediaPath(media) {
1250
1257
  if (!media) return null;
1251
1258
  const path = media["path"];
@@ -1271,27 +1278,6 @@ function resolveMediaIri(uploadUrl, media) {
1271
1278
  function isImageMimeType(mimeType) {
1272
1279
  return !!mimeType && mimeType.startsWith("image/");
1273
1280
  }
1274
- function buildDropzoneAccept(accept) {
1275
- if (!accept || accept === "*/*" || accept === "*") return void 0;
1276
- if (accept === "image/*") return {
1277
- "image/png": [".png"],
1278
- "image/jpeg": [".jpg", ".jpeg"],
1279
- "image/webp": [".webp"],
1280
- "image/gif": [".gif"]
1281
- };
1282
- if (accept.includes(",")) return accept.split(",").reduce((acc, token) => {
1283
- const trimmed = token.trim();
1284
- if (!trimmed) return acc;
1285
- if (trimmed.startsWith(".")) {
1286
- acc["application/octet-stream"] = [...acc["application/octet-stream"] ?? [], trimmed];
1287
- return acc;
1288
- }
1289
- acc[trimmed] = [];
1290
- return acc;
1291
- }, {});
1292
- if (accept.startsWith(".")) return { "application/octet-stream": [accept] };
1293
- return { [accept]: [] };
1294
- }
1295
1281
  async function uploadMediaFile(file, uploadUrl, httpClient) {
1296
1282
  const body = new FormData();
1297
1283
  body.append("file", file);
@@ -1371,17 +1357,6 @@ function FileUploadField({ field, disabled = false, readOnly = false, invalid =
1371
1357
  t,
1372
1358
  uploadUrl
1373
1359
  ]);
1374
- const { getRootProps, getInputProps, isDragActive, open } = (0, react_dropzone.useDropzone)({
1375
- accept: buildDropzoneAccept(field.accept),
1376
- disabled: disabled || readOnly || status === "uploading",
1377
- multiple: false,
1378
- noClick: !!(previewUrl || fileName),
1379
- noKeyboard: !!(previewUrl || fileName),
1380
- onDrop: (acceptedFiles) => {
1381
- const file = acceptedFiles[0];
1382
- if (file) uploadFile(file);
1383
- }
1384
- });
1385
1360
  const handleClear = () => {
1386
1361
  revokeLocalPreview();
1387
1362
  setPreviewUrl(null);
@@ -1391,104 +1366,35 @@ function FileUploadField({ field, disabled = false, readOnly = false, invalid =
1391
1366
  setErrorMessage(null);
1392
1367
  onCleared(field.name);
1393
1368
  };
1394
- const isInteractive = !disabled && !readOnly;
1395
- const hasContent = !!(previewUrl || fileName);
1396
- const placeholderIcon = imageMode ? "ph-image" : "ph-file-arrow-up";
1397
- const placeholderTitle = isDragActive ? t("form.fileUploadDrop") : imageMode ? t("form.imageUploadPrompt") : t("form.fileUploadPrompt");
1398
- const placeholderHint = imageMode ? t("form.imageUploadHint") : t("form.fileUploadHint");
1399
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1400
- className: cx$1("nb-form__file-upload", imageMode && "nb-form__file-upload--image", invalid && "nb-form__file-upload--invalid"),
1401
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1402
- ...getRootProps({ className: cx$1("nb-form__file-upload-zone", isDragActive && "nb-form__file-upload-zone--active", hasContent && "nb-form__file-upload-zone--filled", status === "uploading" && "nb-form__file-upload-zone--uploading", !isInteractive && "nb-form__file-upload-zone--disabled") }),
1403
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { ...getInputProps({
1404
- id: `nb-form-${field.name}`,
1405
- "aria-label": field.label
1406
- }) }), hasContent ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1407
- previewUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1408
- className: "nb-form__file-upload-preview",
1409
- src: previewUrl,
1410
- alt: field.label
1411
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1412
- className: "nb-form__file-upload-file",
1413
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1414
- className: "nb-form__file-upload-file-icon",
1415
- "aria-hidden": "true",
1416
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { className: "ph ph-file" })
1417
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1418
- className: "nb-form__file-upload-file-meta",
1419
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1420
- className: "nb-form__file-upload-file-name",
1421
- children: fileName
1422
- }), fileUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1423
- className: "nb-form__file-upload-file-link",
1424
- href: fileUrl,
1425
- target: "_blank",
1426
- rel: "noreferrer",
1427
- onClick: (event) => event.stopPropagation(),
1428
- children: t("form.fileUploadOpen")
1429
- })]
1430
- })]
1431
- }),
1432
- status === "uploading" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1433
- className: "nb-form__file-upload-overlay",
1434
- "aria-live": "polite",
1435
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1436
- className: "nb-form__file-upload-spinner",
1437
- "aria-hidden": "true"
1438
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("form.fileUploading") })]
1439
- }),
1440
- isInteractive && status !== "uploading" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1441
- className: "nb-form__file-upload-actions",
1442
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1443
- type: "button",
1444
- className: "nb-form__file-upload-action",
1445
- onClick: (event) => {
1446
- event.stopPropagation();
1447
- open();
1448
- },
1449
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1450
- className: "ph ph-arrows-clockwise",
1451
- "aria-hidden": "true"
1452
- }), t("form.fileUploadReplace")]
1453
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1454
- type: "button",
1455
- className: "nb-form__file-upload-action nb-form__file-upload-action--danger",
1456
- onClick: (event) => {
1457
- event.stopPropagation();
1458
- handleClear();
1459
- },
1460
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1461
- className: "ph ph-trash",
1462
- "aria-hidden": "true"
1463
- }), t("form.fileUploadRemove")]
1464
- })]
1465
- })
1466
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1467
- className: "nb-form__file-upload-placeholder",
1468
- children: [
1469
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1470
- className: "nb-form__file-upload-icon",
1471
- "aria-hidden": "true",
1472
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { className: `ph ${placeholderIcon}` })
1473
- }),
1474
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1475
- className: "nb-form__file-upload-title",
1476
- children: placeholderTitle
1477
- }),
1478
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1479
- className: "nb-form__file-upload-hint",
1480
- children: placeholderHint
1481
- })
1482
- ]
1483
- })]
1484
- }), errorMessage && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1485
- className: "nb-form__file-upload-error",
1486
- role: "alert",
1487
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1488
- className: "ph ph-warning-circle",
1489
- "aria-hidden": "true"
1490
- }), errorMessage]
1491
- })]
1369
+ const value = {
1370
+ fileName,
1371
+ fileUrl,
1372
+ previewUrl
1373
+ };
1374
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.FileDropzone, {
1375
+ accept: field.accept,
1376
+ disabled,
1377
+ readOnly,
1378
+ invalid,
1379
+ image: imageMode,
1380
+ value,
1381
+ uploading: status === "uploading",
1382
+ error: errorMessage,
1383
+ inputId: `nb-form-${field.name}`,
1384
+ inputLabel: field.label,
1385
+ labels: {
1386
+ dropPrompt: t("form.fileUploadDrop"),
1387
+ prompt: t("form.fileUploadPrompt"),
1388
+ imagePrompt: t("form.imageUploadPrompt"),
1389
+ hint: t("form.fileUploadHint"),
1390
+ imageHint: t("form.imageUploadHint"),
1391
+ uploading: t("form.fileUploading"),
1392
+ replace: t("form.fileUploadReplace"),
1393
+ remove: t("form.fileUploadRemove"),
1394
+ open: t("form.fileUploadOpen")
1395
+ },
1396
+ onFileSelect: (file) => void uploadFile(file),
1397
+ onClear: handleClear
1492
1398
  });
1493
1399
  }
1494
1400
  function isImageFileField(field) {
@@ -3693,6 +3599,17 @@ function normalizeEntityField(row, field, adapter, prependDataByField) {
3693
3599
  }
3694
3600
  }
3695
3601
  //#endregion
3602
+ //#region packages/crud/form/loadDetailRows.ts
3603
+ /**
3604
+ * Loads embedded line rows for formDetail edit mode. Accepts both plain JSON
3605
+ * arrays (nubit embedded-lines endpoint) and Hydra collections.
3606
+ */
3607
+ async function loadDetailRows(httpClient, detailUrl, adapter) {
3608
+ const response = await httpClient.get(detailUrl);
3609
+ const { items } = (adapter ?? HydraAdapter).parseListResponse(response.data);
3610
+ return items;
3611
+ }
3612
+ //#endregion
3696
3613
  //#region packages/crud/form/safeRandomId.ts
3697
3614
  /**
3698
3615
  * Generate a unique-enough id string for internal React keys.
@@ -4753,7 +4670,7 @@ const NativeFormView = (0, react.forwardRef)((options, ref) => {
4753
4670
  const detailUrl = typeof detailId === "string" || typeof detailId === "number" ? options.detailUrl?.replace("{id}", String(detailId)) : void 0;
4754
4671
  if (!detailUrl) return;
4755
4672
  emit(FORM_EVENTS.LOADING, true);
4756
- httpClient.get(detailUrl).then((response) => setNextDetailRows(response.data)).finally(() => emit(FORM_EVENTS.LOADING, false));
4673
+ loadDetailRows(httpClient, detailUrl, options.adapter).then((rows) => setNextDetailRows(rows)).finally(() => emit(FORM_EVENTS.LOADING, false));
4757
4674
  }, [
4758
4675
  captureExistingMedia,
4759
4676
  emit,
@@ -8543,6 +8460,24 @@ function ToolbarSelect({ id, label, icon = "ph-funnel", value, options, onChange
8543
8460
  });
8544
8461
  }
8545
8462
  //#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
8546
8481
  //#region packages/crud/adapter/RestAdapter.ts
8547
8482
  /**
8548
8483
  * Backend adapter for plain OpenAPI / REST backends.
@@ -8722,6 +8657,7 @@ exports.SmartCrudRolesProvider = SmartCrudRolesProvider;
8722
8657
  exports.ToolbarSelect = ToolbarSelect;
8723
8658
  exports.buildFieldColSpanContext = buildFieldColSpanContext;
8724
8659
  exports.buildFields = buildFields;
8660
+ exports.buildWorkflowRowActions = buildWorkflowRowActions;
8725
8661
  exports.checkboxField = checkboxField;
8726
8662
  exports.computeSummaryValue = computeSummaryValue;
8727
8663
  Object.defineProperty(exports, "createCrudEvents", {
@@ -8738,6 +8674,7 @@ exports.datetimeField = datetimeField;
8738
8674
  exports.defineFieldContract = defineFieldContract;
8739
8675
  exports.defineFields = defineFields;
8740
8676
  exports.defineResource = defineResource;
8677
+ exports.embeddedLinesUrl = embeddedLinesUrl;
8741
8678
  exports.entityField = entityField;
8742
8679
  exports.enumField = enumField;
8743
8680
  exports.fileField = fileField;
package/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import React, { ReactElement, ReactNode, RefObject } from "react";
2
2
  import { CoreHttpClient, DataGridEventNames, DataRecord, DataRecord as DataRecord$1, DialogEventNames, FormEventNames, GridData, GridData as GridData$1, createCrudEvents } from "@nubitio/core";
3
3
  import { AppDropdownOption } from "@nubitio/ui";
4
+ import { WorkflowSchema } from "@nubitio/hydra";
4
5
 
5
6
  //#region packages/crud/field/FieldType.d.ts
6
7
  declare enum FieldType {
@@ -1013,6 +1014,15 @@ declare function defineResource<T extends DataRecord$1>(apiUrl: string, override
1013
1014
  id?: string;
1014
1015
  }): ResourceConfig<T>;
1015
1016
  //#endregion
1017
+ //#region packages/crud/crud/embeddedLinesUrl.d.ts
1018
+ /**
1019
+ * Builds a formDetail reload URL for {@code #[EmbeddedLines]} line entities.
1020
+ *
1021
+ * @example embeddedLinesUrl('/api/sales_document_lines', 'document')
1022
+ * → '/api/sales_document_lines?document={id}'
1023
+ */
1024
+ declare function embeddedLinesUrl(route: string, parentQueryParam: string): string;
1025
+ //#endregion
1016
1026
  //#region packages/crud/crud/CrudPage.d.ts
1017
1027
  interface CrudPageProps<T extends DataRecord$1 = DataRecord$1> {
1018
1028
  resource: ResourceConfig<T>;
@@ -1867,6 +1877,9 @@ interface ColumnPresetState {
1867
1877
  }
1868
1878
  declare function useColumnPreset(resource: ResourceConfig): ColumnPresetState;
1869
1879
  //#endregion
1880
+ //#region packages/crud/workflow/buildWorkflowRowActions.d.ts
1881
+ declare function buildWorkflowRowActions<T extends DataRecord$1 = DataRecord$1>(row: T, workflow: WorkflowSchema | undefined, apiUrl: string, roles: string[], onDone?: () => void): ResourceToolbarAction[];
1882
+ //#endregion
1870
1883
  //#region packages/crud/adapter/HydraAdapter.d.ts
1871
1884
  /**
1872
1885
  * Default backend adapter for API Platform / JSON-LD + Hydra backends.
@@ -1905,6 +1918,7 @@ interface ResourceSchemaResolution {
1905
1918
  * resource publishes one. Explicit `ResourceConfig.formLayout` wins.
1906
1919
  */
1907
1920
  formLayout?: FormLayout;
1921
+ workflow?: WorkflowSchema;
1908
1922
  }
1909
1923
  interface ResourceSchemaResolver {
1910
1924
  useResourceSchema(request: ResourceSchemaRequest): ResourceSchemaResolution;
@@ -1999,4 +2013,4 @@ interface RestQueryDialect {
1999
2013
  */
2000
2014
  declare function createRestResourceStore(dialect?: RestQueryDialect): ResourceStoreFactory;
2001
2015
  //#endregion
2002
- export { type AuditEntry, type AuditFieldLabelResolver, type AuditTrailConfig, AuditTrailPanel, type AuditTrailPanelProps, type BackendAdapter, type BulkAction, type ColSpan, type ColumnPreset, ColumnPresetSelector, type ColumnPresetState, CrudDialogShell, CrudDrawerShell, type CrudDrawerSize, type CrudDrawerViewEvents, type CrudDrawerViewOptions, CrudFormShell, type CrudFormShellProps, CrudPage, CrudPageShell, type CrudPageViewEvents, type CrudPageViewOptions, type CrudViewMode, type CrudViewModeConfig, DATA_GRID_EVENTS, DEFAULT_DRAWER_SIZE, DEFAULT_DRAWER_WIDTH, DRAWER_WIDTHS, type DataGridSelectionChangedEvent, type DataGridSummaryItem, NativeDataGridView as DataGridView, type DataGridViewOptions, type DataRecord, type DetailSummaryOptions, CrudDialogView as DialogView, type DrawerSize, CrudDrawerView as DrawerView, type EnumOption, FORM_EVENTS, type Field, FieldBuilder, type FieldColSpanContext, type FieldDef, type FieldInput, type FieldOverride, FieldType, type FilterRule, type FormHandle, type FormLayout, type FormLayoutHint, type FormOnChangeFn, type FormPresentationContext, type FormPresentationMode, type FormSection, type FormTab, NativeFormView as FormView, type FormViewOptions, type FormatterFn, type GridCellContext, type GridData, type GridHandle, type GridOnChangeFn, HydraAdapter, type ItemFormatterFn, type LoadOption, type OnChangeFn, CrudPageView as PageView, type ResolvedViewMode, type ResourceConfig, type ResourceEmptyState, type ResourceFilterDescriptor, type ResourceFilterRule, type ResourceFormDetail, type ResourceGridDetail, type ResourceLoadOption, type ResourceLoadOptions, type ResourcePermissions, type ResourceRouting, type ResourceRowActions, ResourceSchemaProvider, type ResourceSchemaProviderProps, type ResourceSchemaResolution, type ResourceSchemaResolver, type ResourceSortDescriptor, type ResourceStore, type ResourceStoreFactory, type ResourceStoreOptions, ResourceStoreProvider, type ResourceStoreProviderProps, type ResourceToolbar, type ResourceToolbarAction, type ResourceToolbarActionVariant, type ResourceToolbarContext, type ResourceToolbarItems, RestAdapter, type RestQueryDialect, type SmartCrudFieldContract, type SmartCrudFieldOperation, type SmartCrudFieldPatch, type SmartCrudHydraFieldContract, type SmartCrudHydraFieldDirective, type SmartCrudManualField, type SmartCrudManualFieldContract, type SmartCrudOperation, SmartCrudPage, SmartCrudRolesProvider, type SummaryCalculateContext, type SummaryFormat, type SummaryItem, type SummaryTextContext, type SummaryType, ToolbarSelect, type ToolbarSelectOption, type ToolbarSelectProps, type ValidationRule, buildFieldColSpanContext, buildFields, checkboxField, computeSummaryValue, createCrudEvents, createRestResourceStore, crudRoute, currencyField, dateField, datetimeField, defineFieldContract, defineFields, defineResource, entityField, enumField, fileField, formatSummaryValue, identityField, imageField, isLongTextField, isShortField, noneField, numberField, parseDrawerWidthPx, passwordField, resolveDrawerLayoutBucket, resolveDrawerSize, resolveDrawerWidth, resolveFieldColSpan, resolveFieldsColSpans, resolveSummaryText, resolveViewMode, selectField, switchField, textField, textareaField, useColumnPreset, useResourceStoreFactory, useSmartCrudRoles, validateFieldContract };
2016
+ export { type AuditEntry, type AuditFieldLabelResolver, type AuditTrailConfig, AuditTrailPanel, type AuditTrailPanelProps, type BackendAdapter, type BulkAction, type ColSpan, type ColumnPreset, ColumnPresetSelector, type ColumnPresetState, CrudDialogShell, CrudDrawerShell, type CrudDrawerSize, type CrudDrawerViewEvents, type CrudDrawerViewOptions, CrudFormShell, type CrudFormShellProps, CrudPage, CrudPageShell, type CrudPageViewEvents, type CrudPageViewOptions, type CrudViewMode, type CrudViewModeConfig, DATA_GRID_EVENTS, DEFAULT_DRAWER_SIZE, DEFAULT_DRAWER_WIDTH, DRAWER_WIDTHS, type DataGridSelectionChangedEvent, type DataGridSummaryItem, NativeDataGridView as DataGridView, type DataGridViewOptions, type DataRecord, type DetailSummaryOptions, CrudDialogView as DialogView, type DrawerSize, CrudDrawerView as DrawerView, type EnumOption, FORM_EVENTS, type Field, FieldBuilder, type FieldColSpanContext, type FieldDef, type FieldInput, type FieldOverride, FieldType, type FilterRule, type FormHandle, type FormLayout, type FormLayoutHint, type FormOnChangeFn, type FormPresentationContext, type FormPresentationMode, type FormSection, type FormTab, NativeFormView as FormView, type FormViewOptions, type FormatterFn, type GridCellContext, type GridData, type GridHandle, type GridOnChangeFn, HydraAdapter, type ItemFormatterFn, type LoadOption, type OnChangeFn, CrudPageView as PageView, type ResolvedViewMode, type ResourceConfig, type ResourceEmptyState, type ResourceFilterDescriptor, type ResourceFilterRule, type ResourceFormDetail, type ResourceGridDetail, type ResourceLoadOption, type ResourceLoadOptions, type ResourcePermissions, type ResourceRouting, type ResourceRowActions, ResourceSchemaProvider, type ResourceSchemaProviderProps, type ResourceSchemaResolution, type ResourceSchemaResolver, type ResourceSortDescriptor, type ResourceStore, type ResourceStoreFactory, type ResourceStoreOptions, ResourceStoreProvider, type ResourceStoreProviderProps, type ResourceToolbar, type ResourceToolbarAction, type ResourceToolbarActionVariant, type ResourceToolbarContext, type ResourceToolbarItems, RestAdapter, type RestQueryDialect, type SmartCrudFieldContract, type SmartCrudFieldOperation, type SmartCrudFieldPatch, type SmartCrudHydraFieldContract, type SmartCrudHydraFieldDirective, type SmartCrudManualField, type SmartCrudManualFieldContract, type SmartCrudOperation, SmartCrudPage, SmartCrudRolesProvider, type SummaryCalculateContext, type SummaryFormat, type SummaryItem, type SummaryTextContext, type SummaryType, ToolbarSelect, type ToolbarSelectOption, type ToolbarSelectProps, type ValidationRule, buildFieldColSpanContext, buildFields, buildWorkflowRowActions, checkboxField, computeSummaryValue, createCrudEvents, createRestResourceStore, crudRoute, currencyField, dateField, datetimeField, defineFieldContract, defineFields, defineResource, embeddedLinesUrl, entityField, enumField, fileField, formatSummaryValue, identityField, imageField, isLongTextField, isShortField, noneField, numberField, parseDrawerWidthPx, passwordField, resolveDrawerLayoutBucket, resolveDrawerSize, resolveDrawerWidth, resolveFieldColSpan, resolveFieldsColSpans, resolveSummaryText, resolveViewMode, selectField, switchField, textField, textareaField, useColumnPreset, useResourceStoreFactory, useSmartCrudRoles, validateFieldContract };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import React, { ReactElement, ReactNode, RefObject } from "react";
2
2
  import { AppDropdownOption } from "@nubitio/ui";
3
3
  import { CoreHttpClient, DataGridEventNames, DataRecord, DataRecord as DataRecord$1, DialogEventNames, FormEventNames, GridData, GridData as GridData$1, createCrudEvents } from "@nubitio/core";
4
+ import { WorkflowSchema } from "@nubitio/hydra";
4
5
 
5
6
  //#region packages/crud/field/FieldType.d.ts
6
7
  declare enum FieldType {
@@ -1013,6 +1014,15 @@ declare function defineResource<T extends DataRecord$1>(apiUrl: string, override
1013
1014
  id?: string;
1014
1015
  }): ResourceConfig<T>;
1015
1016
  //#endregion
1017
+ //#region packages/crud/crud/embeddedLinesUrl.d.ts
1018
+ /**
1019
+ * Builds a formDetail reload URL for {@code #[EmbeddedLines]} line entities.
1020
+ *
1021
+ * @example embeddedLinesUrl('/api/sales_document_lines', 'document')
1022
+ * → '/api/sales_document_lines?document={id}'
1023
+ */
1024
+ declare function embeddedLinesUrl(route: string, parentQueryParam: string): string;
1025
+ //#endregion
1016
1026
  //#region packages/crud/crud/CrudPage.d.ts
1017
1027
  interface CrudPageProps<T extends DataRecord$1 = DataRecord$1> {
1018
1028
  resource: ResourceConfig<T>;
@@ -1867,6 +1877,9 @@ interface ColumnPresetState {
1867
1877
  }
1868
1878
  declare function useColumnPreset(resource: ResourceConfig): ColumnPresetState;
1869
1879
  //#endregion
1880
+ //#region packages/crud/workflow/buildWorkflowRowActions.d.ts
1881
+ declare function buildWorkflowRowActions<T extends DataRecord$1 = DataRecord$1>(row: T, workflow: WorkflowSchema | undefined, apiUrl: string, roles: string[], onDone?: () => void): ResourceToolbarAction[];
1882
+ //#endregion
1870
1883
  //#region packages/crud/adapter/HydraAdapter.d.ts
1871
1884
  /**
1872
1885
  * Default backend adapter for API Platform / JSON-LD + Hydra backends.
@@ -1905,6 +1918,7 @@ interface ResourceSchemaResolution {
1905
1918
  * resource publishes one. Explicit `ResourceConfig.formLayout` wins.
1906
1919
  */
1907
1920
  formLayout?: FormLayout;
1921
+ workflow?: WorkflowSchema;
1908
1922
  }
1909
1923
  interface ResourceSchemaResolver {
1910
1924
  useResourceSchema(request: ResourceSchemaRequest): ResourceSchemaResolution;
@@ -1999,4 +2013,4 @@ interface RestQueryDialect {
1999
2013
  */
2000
2014
  declare function createRestResourceStore(dialect?: RestQueryDialect): ResourceStoreFactory;
2001
2015
  //#endregion
2002
- export { type AuditEntry, type AuditFieldLabelResolver, type AuditTrailConfig, AuditTrailPanel, type AuditTrailPanelProps, type BackendAdapter, type BulkAction, type ColSpan, type ColumnPreset, ColumnPresetSelector, type ColumnPresetState, CrudDialogShell, CrudDrawerShell, type CrudDrawerSize, type CrudDrawerViewEvents, type CrudDrawerViewOptions, CrudFormShell, type CrudFormShellProps, CrudPage, CrudPageShell, type CrudPageViewEvents, type CrudPageViewOptions, type CrudViewMode, type CrudViewModeConfig, DATA_GRID_EVENTS, DEFAULT_DRAWER_SIZE, DEFAULT_DRAWER_WIDTH, DRAWER_WIDTHS, type DataGridSelectionChangedEvent, type DataGridSummaryItem, NativeDataGridView as DataGridView, type DataGridViewOptions, type DataRecord, type DetailSummaryOptions, CrudDialogView as DialogView, type DrawerSize, CrudDrawerView as DrawerView, type EnumOption, FORM_EVENTS, type Field, FieldBuilder, type FieldColSpanContext, type FieldDef, type FieldInput, type FieldOverride, FieldType, type FilterRule, type FormHandle, type FormLayout, type FormLayoutHint, type FormOnChangeFn, type FormPresentationContext, type FormPresentationMode, type FormSection, type FormTab, NativeFormView as FormView, type FormViewOptions, type FormatterFn, type GridCellContext, type GridData, type GridHandle, type GridOnChangeFn, HydraAdapter, type ItemFormatterFn, type LoadOption, type OnChangeFn, CrudPageView as PageView, type ResolvedViewMode, type ResourceConfig, type ResourceEmptyState, type ResourceFilterDescriptor, type ResourceFilterRule, type ResourceFormDetail, type ResourceGridDetail, type ResourceLoadOption, type ResourceLoadOptions, type ResourcePermissions, type ResourceRouting, type ResourceRowActions, ResourceSchemaProvider, type ResourceSchemaProviderProps, type ResourceSchemaResolution, type ResourceSchemaResolver, type ResourceSortDescriptor, type ResourceStore, type ResourceStoreFactory, type ResourceStoreOptions, ResourceStoreProvider, type ResourceStoreProviderProps, type ResourceToolbar, type ResourceToolbarAction, type ResourceToolbarActionVariant, type ResourceToolbarContext, type ResourceToolbarItems, RestAdapter, type RestQueryDialect, type SmartCrudFieldContract, type SmartCrudFieldOperation, type SmartCrudFieldPatch, type SmartCrudHydraFieldContract, type SmartCrudHydraFieldDirective, type SmartCrudManualField, type SmartCrudManualFieldContract, type SmartCrudOperation, SmartCrudPage, SmartCrudRolesProvider, type SummaryCalculateContext, type SummaryFormat, type SummaryItem, type SummaryTextContext, type SummaryType, ToolbarSelect, type ToolbarSelectOption, type ToolbarSelectProps, type ValidationRule, buildFieldColSpanContext, buildFields, checkboxField, computeSummaryValue, createCrudEvents, createRestResourceStore, crudRoute, currencyField, dateField, datetimeField, defineFieldContract, defineFields, defineResource, entityField, enumField, fileField, formatSummaryValue, identityField, imageField, isLongTextField, isShortField, noneField, numberField, parseDrawerWidthPx, passwordField, resolveDrawerLayoutBucket, resolveDrawerSize, resolveDrawerWidth, resolveFieldColSpan, resolveFieldsColSpans, resolveSummaryText, resolveViewMode, selectField, switchField, textField, textareaField, useColumnPreset, useResourceStoreFactory, useSmartCrudRoles, validateFieldContract };
2016
+ export { type AuditEntry, type AuditFieldLabelResolver, type AuditTrailConfig, AuditTrailPanel, type AuditTrailPanelProps, type BackendAdapter, type BulkAction, type ColSpan, type ColumnPreset, ColumnPresetSelector, type ColumnPresetState, CrudDialogShell, CrudDrawerShell, type CrudDrawerSize, type CrudDrawerViewEvents, type CrudDrawerViewOptions, CrudFormShell, type CrudFormShellProps, CrudPage, CrudPageShell, type CrudPageViewEvents, type CrudPageViewOptions, type CrudViewMode, type CrudViewModeConfig, DATA_GRID_EVENTS, DEFAULT_DRAWER_SIZE, DEFAULT_DRAWER_WIDTH, DRAWER_WIDTHS, type DataGridSelectionChangedEvent, type DataGridSummaryItem, NativeDataGridView as DataGridView, type DataGridViewOptions, type DataRecord, type DetailSummaryOptions, CrudDialogView as DialogView, type DrawerSize, CrudDrawerView as DrawerView, type EnumOption, FORM_EVENTS, type Field, FieldBuilder, type FieldColSpanContext, type FieldDef, type FieldInput, type FieldOverride, FieldType, type FilterRule, type FormHandle, type FormLayout, type FormLayoutHint, type FormOnChangeFn, type FormPresentationContext, type FormPresentationMode, type FormSection, type FormTab, NativeFormView as FormView, type FormViewOptions, type FormatterFn, type GridCellContext, type GridData, type GridHandle, type GridOnChangeFn, HydraAdapter, type ItemFormatterFn, type LoadOption, type OnChangeFn, CrudPageView as PageView, type ResolvedViewMode, type ResourceConfig, type ResourceEmptyState, type ResourceFilterDescriptor, type ResourceFilterRule, type ResourceFormDetail, type ResourceGridDetail, type ResourceLoadOption, type ResourceLoadOptions, type ResourcePermissions, type ResourceRouting, type ResourceRowActions, ResourceSchemaProvider, type ResourceSchemaProviderProps, type ResourceSchemaResolution, type ResourceSchemaResolver, type ResourceSortDescriptor, type ResourceStore, type ResourceStoreFactory, type ResourceStoreOptions, ResourceStoreProvider, type ResourceStoreProviderProps, type ResourceToolbar, type ResourceToolbarAction, type ResourceToolbarActionVariant, type ResourceToolbarContext, type ResourceToolbarItems, RestAdapter, type RestQueryDialect, type SmartCrudFieldContract, type SmartCrudFieldOperation, type SmartCrudFieldPatch, type SmartCrudHydraFieldContract, type SmartCrudHydraFieldDirective, type SmartCrudManualField, type SmartCrudManualFieldContract, type SmartCrudOperation, SmartCrudPage, SmartCrudRolesProvider, type SummaryCalculateContext, type SummaryFormat, type SummaryItem, type SummaryTextContext, type SummaryType, ToolbarSelect, type ToolbarSelectOption, type ToolbarSelectProps, type ValidationRule, buildFieldColSpanContext, buildFields, buildWorkflowRowActions, checkboxField, computeSummaryValue, createCrudEvents, createRestResourceStore, crudRoute, currencyField, dateField, datetimeField, defineFieldContract, defineFields, defineResource, embeddedLinesUrl, entityField, enumField, fileField, formatSummaryValue, identityField, imageField, isLongTextField, isShortField, noneField, numberField, parseDrawerWidthPx, passwordField, resolveDrawerLayoutBucket, resolveDrawerSize, resolveDrawerWidth, resolveFieldColSpan, resolveFieldsColSpans, resolveSummaryText, resolveViewMode, selectField, switchField, textField, textareaField, useColumnPreset, useResourceStoreFactory, useSmartCrudRoles, validateFieldContract };
package/dist/index.mjs CHANGED
@@ -1,10 +1,9 @@
1
1
  import React, { createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState } from "react";
2
2
  import { Route, useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";
3
3
  import { createPortal } from "react-dom";
4
- import { AppDialog, AppDropdown, Badge, Button, ConfirmDialog, DatePicker, DateRangePicker, Drawer, EmptyState, IconButton, Skeleton, Timeline, TimelineItem } from "@nubitio/ui";
4
+ import { AppDialog, AppDropdown, Badge, Button, ConfirmDialog, DatePicker, DateRangePicker, Drawer, EmptyState, FileDropzone, IconButton, Skeleton, Timeline, TimelineItem } from "@nubitio/ui";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { createCrudEvents, createScopedEventBus, getCoreCurrency, getCoreLocale, getCoreTimezone, useCoreHttpClient, useCoreRuntime, useCoreTranslation, useEvents, useMercureSubscription } from "@nubitio/core";
7
- import { useDropzone } from "react-dropzone";
8
7
  import { useQueryClient } from "@tanstack/react-query";
9
8
  //#region packages/crud/crud/defineResource.ts
10
9
  const stringResourceCache = /* @__PURE__ */ new Map();
@@ -39,6 +38,17 @@ function defineResource(apiUrl, overrides) {
39
38
  return resource;
40
39
  }
41
40
  //#endregion
41
+ //#region packages/crud/crud/embeddedLinesUrl.ts
42
+ /**
43
+ * Builds a formDetail reload URL for {@code #[EmbeddedLines]} line entities.
44
+ *
45
+ * @example embeddedLinesUrl('/api/sales_document_lines', 'document')
46
+ * → '/api/sales_document_lines?document={id}'
47
+ */
48
+ function embeddedLinesUrl(route, parentQueryParam) {
49
+ return `${route}${route.includes("?") ? "&" : "?"}${parentQueryParam}={id}`;
50
+ }
51
+ //#endregion
42
52
  //#region packages/crud/datagrid/DataGridEvents.ts
43
53
  const DATA_GRID_EVENTS = {
44
54
  SELECTION_CHANGED: "datagrid:selection.changed",
@@ -1219,9 +1229,6 @@ const enumTypeModule = {
1219
1229
  };
1220
1230
  //#endregion
1221
1231
  //#region packages/crud/form/FileUploadField.tsx
1222
- function cx$1(...values) {
1223
- return values.filter(Boolean).join(" ");
1224
- }
1225
1232
  function resolveMediaPath(media) {
1226
1233
  if (!media) return null;
1227
1234
  const path = media["path"];
@@ -1247,27 +1254,6 @@ function resolveMediaIri(uploadUrl, media) {
1247
1254
  function isImageMimeType(mimeType) {
1248
1255
  return !!mimeType && mimeType.startsWith("image/");
1249
1256
  }
1250
- function buildDropzoneAccept(accept) {
1251
- if (!accept || accept === "*/*" || accept === "*") return void 0;
1252
- if (accept === "image/*") return {
1253
- "image/png": [".png"],
1254
- "image/jpeg": [".jpg", ".jpeg"],
1255
- "image/webp": [".webp"],
1256
- "image/gif": [".gif"]
1257
- };
1258
- if (accept.includes(",")) return accept.split(",").reduce((acc, token) => {
1259
- const trimmed = token.trim();
1260
- if (!trimmed) return acc;
1261
- if (trimmed.startsWith(".")) {
1262
- acc["application/octet-stream"] = [...acc["application/octet-stream"] ?? [], trimmed];
1263
- return acc;
1264
- }
1265
- acc[trimmed] = [];
1266
- return acc;
1267
- }, {});
1268
- if (accept.startsWith(".")) return { "application/octet-stream": [accept] };
1269
- return { [accept]: [] };
1270
- }
1271
1257
  async function uploadMediaFile(file, uploadUrl, httpClient) {
1272
1258
  const body = new FormData();
1273
1259
  body.append("file", file);
@@ -1347,17 +1333,6 @@ function FileUploadField({ field, disabled = false, readOnly = false, invalid =
1347
1333
  t,
1348
1334
  uploadUrl
1349
1335
  ]);
1350
- const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
1351
- accept: buildDropzoneAccept(field.accept),
1352
- disabled: disabled || readOnly || status === "uploading",
1353
- multiple: false,
1354
- noClick: !!(previewUrl || fileName),
1355
- noKeyboard: !!(previewUrl || fileName),
1356
- onDrop: (acceptedFiles) => {
1357
- const file = acceptedFiles[0];
1358
- if (file) uploadFile(file);
1359
- }
1360
- });
1361
1336
  const handleClear = () => {
1362
1337
  revokeLocalPreview();
1363
1338
  setPreviewUrl(null);
@@ -1367,104 +1342,35 @@ function FileUploadField({ field, disabled = false, readOnly = false, invalid =
1367
1342
  setErrorMessage(null);
1368
1343
  onCleared(field.name);
1369
1344
  };
1370
- const isInteractive = !disabled && !readOnly;
1371
- const hasContent = !!(previewUrl || fileName);
1372
- const placeholderIcon = imageMode ? "ph-image" : "ph-file-arrow-up";
1373
- const placeholderTitle = isDragActive ? t("form.fileUploadDrop") : imageMode ? t("form.imageUploadPrompt") : t("form.fileUploadPrompt");
1374
- const placeholderHint = imageMode ? t("form.imageUploadHint") : t("form.fileUploadHint");
1375
- return /* @__PURE__ */ jsxs("div", {
1376
- className: cx$1("nb-form__file-upload", imageMode && "nb-form__file-upload--image", invalid && "nb-form__file-upload--invalid"),
1377
- children: [/* @__PURE__ */ jsxs("div", {
1378
- ...getRootProps({ className: cx$1("nb-form__file-upload-zone", isDragActive && "nb-form__file-upload-zone--active", hasContent && "nb-form__file-upload-zone--filled", status === "uploading" && "nb-form__file-upload-zone--uploading", !isInteractive && "nb-form__file-upload-zone--disabled") }),
1379
- children: [/* @__PURE__ */ jsx("input", { ...getInputProps({
1380
- id: `nb-form-${field.name}`,
1381
- "aria-label": field.label
1382
- }) }), hasContent ? /* @__PURE__ */ jsxs(Fragment, { children: [
1383
- previewUrl ? /* @__PURE__ */ jsx("img", {
1384
- className: "nb-form__file-upload-preview",
1385
- src: previewUrl,
1386
- alt: field.label
1387
- }) : /* @__PURE__ */ jsxs("div", {
1388
- className: "nb-form__file-upload-file",
1389
- children: [/* @__PURE__ */ jsx("span", {
1390
- className: "nb-form__file-upload-file-icon",
1391
- "aria-hidden": "true",
1392
- children: /* @__PURE__ */ jsx("i", { className: "ph ph-file" })
1393
- }), /* @__PURE__ */ jsxs("div", {
1394
- className: "nb-form__file-upload-file-meta",
1395
- children: [/* @__PURE__ */ jsx("span", {
1396
- className: "nb-form__file-upload-file-name",
1397
- children: fileName
1398
- }), fileUrl && /* @__PURE__ */ jsx("a", {
1399
- className: "nb-form__file-upload-file-link",
1400
- href: fileUrl,
1401
- target: "_blank",
1402
- rel: "noreferrer",
1403
- onClick: (event) => event.stopPropagation(),
1404
- children: t("form.fileUploadOpen")
1405
- })]
1406
- })]
1407
- }),
1408
- status === "uploading" && /* @__PURE__ */ jsxs("div", {
1409
- className: "nb-form__file-upload-overlay",
1410
- "aria-live": "polite",
1411
- children: [/* @__PURE__ */ jsx("span", {
1412
- className: "nb-form__file-upload-spinner",
1413
- "aria-hidden": "true"
1414
- }), /* @__PURE__ */ jsx("span", { children: t("form.fileUploading") })]
1415
- }),
1416
- isInteractive && status !== "uploading" && /* @__PURE__ */ jsxs("div", {
1417
- className: "nb-form__file-upload-actions",
1418
- children: [/* @__PURE__ */ jsxs("button", {
1419
- type: "button",
1420
- className: "nb-form__file-upload-action",
1421
- onClick: (event) => {
1422
- event.stopPropagation();
1423
- open();
1424
- },
1425
- children: [/* @__PURE__ */ jsx("i", {
1426
- className: "ph ph-arrows-clockwise",
1427
- "aria-hidden": "true"
1428
- }), t("form.fileUploadReplace")]
1429
- }), /* @__PURE__ */ jsxs("button", {
1430
- type: "button",
1431
- className: "nb-form__file-upload-action nb-form__file-upload-action--danger",
1432
- onClick: (event) => {
1433
- event.stopPropagation();
1434
- handleClear();
1435
- },
1436
- children: [/* @__PURE__ */ jsx("i", {
1437
- className: "ph ph-trash",
1438
- "aria-hidden": "true"
1439
- }), t("form.fileUploadRemove")]
1440
- })]
1441
- })
1442
- ] }) : /* @__PURE__ */ jsxs("div", {
1443
- className: "nb-form__file-upload-placeholder",
1444
- children: [
1445
- /* @__PURE__ */ jsx("span", {
1446
- className: "nb-form__file-upload-icon",
1447
- "aria-hidden": "true",
1448
- children: /* @__PURE__ */ jsx("i", { className: `ph ${placeholderIcon}` })
1449
- }),
1450
- /* @__PURE__ */ jsx("span", {
1451
- className: "nb-form__file-upload-title",
1452
- children: placeholderTitle
1453
- }),
1454
- /* @__PURE__ */ jsx("span", {
1455
- className: "nb-form__file-upload-hint",
1456
- children: placeholderHint
1457
- })
1458
- ]
1459
- })]
1460
- }), errorMessage && /* @__PURE__ */ jsxs("span", {
1461
- className: "nb-form__file-upload-error",
1462
- role: "alert",
1463
- children: [/* @__PURE__ */ jsx("i", {
1464
- className: "ph ph-warning-circle",
1465
- "aria-hidden": "true"
1466
- }), errorMessage]
1467
- })]
1345
+ const value = {
1346
+ fileName,
1347
+ fileUrl,
1348
+ previewUrl
1349
+ };
1350
+ return /* @__PURE__ */ jsx(FileDropzone, {
1351
+ accept: field.accept,
1352
+ disabled,
1353
+ readOnly,
1354
+ invalid,
1355
+ image: imageMode,
1356
+ value,
1357
+ uploading: status === "uploading",
1358
+ error: errorMessage,
1359
+ inputId: `nb-form-${field.name}`,
1360
+ inputLabel: field.label,
1361
+ labels: {
1362
+ dropPrompt: t("form.fileUploadDrop"),
1363
+ prompt: t("form.fileUploadPrompt"),
1364
+ imagePrompt: t("form.imageUploadPrompt"),
1365
+ hint: t("form.fileUploadHint"),
1366
+ imageHint: t("form.imageUploadHint"),
1367
+ uploading: t("form.fileUploading"),
1368
+ replace: t("form.fileUploadReplace"),
1369
+ remove: t("form.fileUploadRemove"),
1370
+ open: t("form.fileUploadOpen")
1371
+ },
1372
+ onFileSelect: (file) => void uploadFile(file),
1373
+ onClear: handleClear
1468
1374
  });
1469
1375
  }
1470
1376
  function isImageFileField(field) {
@@ -3669,6 +3575,17 @@ function normalizeEntityField(row, field, adapter, prependDataByField) {
3669
3575
  }
3670
3576
  }
3671
3577
  //#endregion
3578
+ //#region packages/crud/form/loadDetailRows.ts
3579
+ /**
3580
+ * Loads embedded line rows for formDetail edit mode. Accepts both plain JSON
3581
+ * arrays (nubit embedded-lines endpoint) and Hydra collections.
3582
+ */
3583
+ async function loadDetailRows(httpClient, detailUrl, adapter) {
3584
+ const response = await httpClient.get(detailUrl);
3585
+ const { items } = (adapter ?? HydraAdapter).parseListResponse(response.data);
3586
+ return items;
3587
+ }
3588
+ //#endregion
3672
3589
  //#region packages/crud/form/safeRandomId.ts
3673
3590
  /**
3674
3591
  * Generate a unique-enough id string for internal React keys.
@@ -4729,7 +4646,7 @@ const NativeFormView = forwardRef((options, ref) => {
4729
4646
  const detailUrl = typeof detailId === "string" || typeof detailId === "number" ? options.detailUrl?.replace("{id}", String(detailId)) : void 0;
4730
4647
  if (!detailUrl) return;
4731
4648
  emit(FORM_EVENTS.LOADING, true);
4732
- httpClient.get(detailUrl).then((response) => setNextDetailRows(response.data)).finally(() => emit(FORM_EVENTS.LOADING, false));
4649
+ loadDetailRows(httpClient, detailUrl, options.adapter).then((rows) => setNextDetailRows(rows)).finally(() => emit(FORM_EVENTS.LOADING, false));
4733
4650
  }, [
4734
4651
  captureExistingMedia,
4735
4652
  emit,
@@ -8519,6 +8436,24 @@ function ToolbarSelect({ id, label, icon = "ph-funnel", value, options, onChange
8519
8436
  });
8520
8437
  }
8521
8438
  //#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
8522
8457
  //#region packages/crud/adapter/RestAdapter.ts
8523
8458
  /**
8524
8459
  * Backend adapter for plain OpenAPI / REST backends.
@@ -8670,4 +8605,4 @@ function createRestResourceStore(dialect = {}) {
8670
8605
  };
8671
8606
  }
8672
8607
  //#endregion
8673
- export { AuditTrailPanel, ColumnPresetSelector, CrudDialogShell, CrudDrawerShell, CrudFormShell, CrudPage, CrudPageShell, DATA_GRID_EVENTS, DEFAULT_DRAWER_SIZE, DEFAULT_DRAWER_WIDTH, DRAWER_WIDTHS, NativeDataGridView as DataGridView, CrudDialogView as DialogView, CrudDrawerView as DrawerView, FORM_EVENTS, FieldBuilder, FieldType, NativeFormView as FormView, HydraAdapter, CrudPageView as PageView, ResourceSchemaProvider, ResourceStoreProvider, RestAdapter, SmartCrudPage, SmartCrudRolesProvider, ToolbarSelect, buildFieldColSpanContext, buildFields, checkboxField, computeSummaryValue, createCrudEvents, createRestResourceStore, crudRoute, currencyField, dateField, datetimeField, defineFieldContract, defineFields, defineResource, entityField, enumField, fileField, formatSummaryValue, identityField, imageField, isLongTextField, isShortField, noneField, numberField, parseDrawerWidthPx, passwordField, resolveDrawerLayoutBucket, resolveDrawerSize, resolveDrawerWidth, resolveFieldColSpan, resolveFieldsColSpans, resolveSummaryText, resolveViewMode, selectField, switchField, textField, textareaField, useColumnPreset, useResourceStoreFactory, useSmartCrudRoles, validateFieldContract };
8608
+ export { AuditTrailPanel, ColumnPresetSelector, CrudDialogShell, CrudDrawerShell, CrudFormShell, CrudPage, CrudPageShell, DATA_GRID_EVENTS, DEFAULT_DRAWER_SIZE, DEFAULT_DRAWER_WIDTH, DRAWER_WIDTHS, NativeDataGridView as DataGridView, CrudDialogView as DialogView, CrudDrawerView as DrawerView, FORM_EVENTS, FieldBuilder, FieldType, NativeFormView as FormView, HydraAdapter, CrudPageView as PageView, ResourceSchemaProvider, ResourceStoreProvider, RestAdapter, SmartCrudPage, SmartCrudRolesProvider, ToolbarSelect, buildFieldColSpanContext, buildFields, buildWorkflowRowActions, checkboxField, computeSummaryValue, createCrudEvents, createRestResourceStore, crudRoute, currencyField, dateField, datetimeField, defineFieldContract, defineFields, defineResource, embeddedLinesUrl, entityField, enumField, fileField, formatSummaryValue, identityField, imageField, isLongTextField, isShortField, noneField, numberField, parseDrawerWidthPx, passwordField, resolveDrawerLayoutBucket, resolveDrawerSize, resolveDrawerWidth, resolveFieldColSpan, resolveFieldsColSpans, resolveSummaryText, resolveViewMode, selectField, switchField, textField, textareaField, useColumnPreset, useResourceStoreFactory, useSmartCrudRoles, validateFieldContract };
package/dist/style.css CHANGED
@@ -617,7 +617,7 @@
617
617
  font-size: var(--font-size-xs);
618
618
  }
619
619
 
620
- .nb-datagrid__filter-operator {
620
+ .nb-datagrid__filter-wrap > .nb-datagrid__filter-operator {
621
621
  left: 5px;
622
622
  position: absolute;
623
623
  top: 50%;
@@ -625,7 +625,7 @@
625
625
  width: 30px;
626
626
  z-index: 2;
627
627
  }
628
- .nb-datagrid__filter-operator.nb-dropdown .nb-dropdown__trigger {
628
+ .nb-datagrid__filter-wrap > .nb-datagrid__filter-operator.nb-dropdown .nb-dropdown__trigger {
629
629
  appearance: none;
630
630
  background: color-mix(in srgb, var(--accent-color) 10%, transparent);
631
631
  border: 1px solid color-mix(in srgb, var(--accent-color) 20%, transparent);
@@ -641,7 +641,7 @@
641
641
  text-align: center;
642
642
  width: 30px;
643
643
  }
644
- .nb-datagrid__filter-operator.nb-dropdown .nb-dropdown__caret {
644
+ .nb-datagrid__filter-wrap > .nb-datagrid__filter-operator.nb-dropdown .nb-dropdown__caret {
645
645
  display: none;
646
646
  }
647
647
 
@@ -1705,471 +1705,6 @@ html[data-density=compact] .nb-datagrid .nb-badge {
1705
1705
  box-shadow: none;
1706
1706
  }
1707
1707
  }
1708
- .nb-form__file-upload {
1709
- display: flex;
1710
- flex-direction: column;
1711
- gap: var(--space-1);
1712
- width: 100%;
1713
- }
1714
-
1715
- .nb-form__file-upload-zone {
1716
- align-items: center;
1717
- background: var(--surface-1);
1718
- border: 1px dashed var(--border-color);
1719
- border-radius: var(--radius-lg);
1720
- box-sizing: border-box;
1721
- cursor: pointer;
1722
- display: flex;
1723
- justify-content: center;
1724
- min-height: 112px;
1725
- overflow: hidden;
1726
- position: relative;
1727
- transition: border-color var(--transition-base), background var(--transition-base), box-shadow var(--transition-base);
1728
- width: 100%;
1729
- }
1730
- .nb-form__file-upload-zone:hover:not(.nb-form__file-upload-zone--disabled):not(.nb-form__file-upload-zone--filled) {
1731
- background: color-mix(in srgb, var(--accent-color) 4%, var(--surface-1));
1732
- border-color: var(--accent-color);
1733
- }
1734
- .nb-form__file-upload-zone--active {
1735
- background: color-mix(in srgb, var(--accent-color) 8%, var(--surface-1));
1736
- border-color: var(--accent-color);
1737
- }
1738
- .nb-form__file-upload-zone--filled {
1739
- border-style: solid;
1740
- cursor: default;
1741
- min-height: 88px;
1742
- }
1743
- .nb-form__file-upload-zone--uploading {
1744
- pointer-events: none;
1745
- }
1746
- .nb-form__file-upload-zone--disabled {
1747
- cursor: not-allowed;
1748
- opacity: 0.72;
1749
- }
1750
- .nb-form__file-upload-zone:focus-visible {
1751
- box-shadow: 0 0 0 3px var(--focus-ring-color);
1752
- outline: none;
1753
- }
1754
-
1755
- .nb-form__file-upload--image .nb-form__file-upload-zone {
1756
- min-height: 168px;
1757
- }
1758
-
1759
- .nb-form__file-upload--image .nb-form__file-upload-zone--filled {
1760
- min-height: 180px;
1761
- }
1762
-
1763
- .nb-form__file-upload--invalid .nb-form__file-upload-zone {
1764
- border-color: var(--error-color);
1765
- }
1766
-
1767
- .nb-form__file-upload-placeholder {
1768
- align-items: center;
1769
- display: flex;
1770
- flex-direction: column;
1771
- gap: var(--space-2);
1772
- max-width: 320px;
1773
- padding: var(--space-4);
1774
- text-align: center;
1775
- }
1776
-
1777
- .nb-form__file-upload-icon {
1778
- align-items: center;
1779
- background: color-mix(in srgb, var(--accent-color) 10%, transparent);
1780
- border-radius: 999px;
1781
- color: var(--accent-color);
1782
- display: inline-flex;
1783
- font-size: 24px;
1784
- height: 48px;
1785
- justify-content: center;
1786
- width: 48px;
1787
- }
1788
-
1789
- .nb-form__file-upload-title {
1790
- color: var(--text-primary);
1791
- font-size: var(--font-size-sm);
1792
- font-weight: var(--font-weight-semibold);
1793
- }
1794
-
1795
- .nb-form__file-upload-hint {
1796
- color: var(--text-tertiary);
1797
- font-size: var(--font-size-xs);
1798
- line-height: var(--line-height-tight);
1799
- }
1800
-
1801
- .nb-form__file-upload-preview {
1802
- display: block;
1803
- height: 100%;
1804
- max-height: 220px;
1805
- object-fit: contain;
1806
- width: 100%;
1807
- }
1808
-
1809
- .nb-form__file-upload-file {
1810
- align-items: center;
1811
- display: flex;
1812
- gap: var(--space-3);
1813
- max-width: 100%;
1814
- padding: var(--space-3) var(--space-4);
1815
- width: 100%;
1816
- }
1817
-
1818
- .nb-form__file-upload-file-icon {
1819
- align-items: center;
1820
- background: var(--surface-0);
1821
- border: 1px solid var(--border-subtle);
1822
- border-radius: var(--radius-md);
1823
- color: var(--accent-color);
1824
- display: inline-flex;
1825
- flex: 0 0 auto;
1826
- font-size: 22px;
1827
- height: 44px;
1828
- justify-content: center;
1829
- width: 44px;
1830
- }
1831
-
1832
- .nb-form__file-upload-file-meta {
1833
- display: flex;
1834
- flex: 1 1 auto;
1835
- flex-direction: column;
1836
- gap: 2px;
1837
- min-width: 0;
1838
- }
1839
-
1840
- .nb-form__file-upload-file-name {
1841
- color: var(--text-primary);
1842
- font-size: var(--font-size-sm);
1843
- font-weight: var(--font-weight-medium);
1844
- overflow: hidden;
1845
- text-overflow: ellipsis;
1846
- white-space: nowrap;
1847
- }
1848
-
1849
- .nb-form__file-upload-file-link {
1850
- color: var(--accent-color);
1851
- font-size: var(--font-size-xs);
1852
- text-decoration: none;
1853
- }
1854
- .nb-form__file-upload-file-link:hover {
1855
- text-decoration: underline;
1856
- }
1857
-
1858
- .nb-form__file-upload-overlay {
1859
- align-items: center;
1860
- background: rgba(0, 0, 0, 0.42);
1861
- color: #fff;
1862
- display: flex;
1863
- flex-direction: column;
1864
- font-size: var(--font-size-sm);
1865
- gap: var(--space-2);
1866
- inset: 0;
1867
- justify-content: center;
1868
- position: absolute;
1869
- }
1870
-
1871
- .nb-form__file-upload-spinner {
1872
- animation: nb-form-file-spin 700ms linear infinite;
1873
- border: 2px solid rgba(255, 255, 255, 0.35);
1874
- border-radius: 999px;
1875
- border-top-color: #fff;
1876
- height: 24px;
1877
- width: 24px;
1878
- }
1879
-
1880
- @keyframes nb-form-file-spin {
1881
- to {
1882
- transform: rotate(360deg);
1883
- }
1884
- }
1885
- .nb-form__file-upload-actions {
1886
- align-items: center;
1887
- background: linear-gradient(to top, rgba(0, 0, 0, 0.58), transparent);
1888
- bottom: 0;
1889
- display: flex;
1890
- gap: var(--space-2);
1891
- inset-inline: 0;
1892
- justify-content: center;
1893
- opacity: 0;
1894
- padding: var(--space-3);
1895
- position: absolute;
1896
- transition: opacity var(--transition-base);
1897
- }
1898
-
1899
- .nb-form__file-upload-zone--filled:hover .nb-form__file-upload-actions,
1900
- .nb-form__file-upload-zone--filled:focus-within .nb-form__file-upload-actions {
1901
- opacity: 1;
1902
- }
1903
-
1904
- .nb-form__file-upload-action {
1905
- align-items: center;
1906
- background: var(--surface-1);
1907
- border: 1px solid var(--border-subtle);
1908
- border-radius: var(--radius-md);
1909
- color: var(--text-primary);
1910
- cursor: pointer;
1911
- display: inline-flex;
1912
- font: inherit;
1913
- font-size: var(--font-size-xs);
1914
- font-weight: var(--font-weight-medium);
1915
- gap: var(--space-1);
1916
- min-height: 28px;
1917
- padding: 0 var(--space-2);
1918
- transition: background var(--transition-base), border-color var(--transition-base), color var(--transition-base);
1919
- }
1920
- .nb-form__file-upload-action:hover {
1921
- border-color: var(--accent-color);
1922
- color: var(--accent-color);
1923
- }
1924
- .nb-form__file-upload-action--danger:hover {
1925
- border-color: var(--error-color);
1926
- color: var(--error-color);
1927
- }
1928
- .nb-form__file-upload-action:focus-visible {
1929
- box-shadow: 0 0 0 2px var(--focus-ring-color);
1930
- outline: none;
1931
- }
1932
-
1933
- .nb-form__file-upload-error {
1934
- align-items: center;
1935
- color: var(--error-color);
1936
- display: inline-flex;
1937
- font-size: var(--font-size-xs);
1938
- gap: var(--space-1);
1939
- }
1940
- .nb-form__file-upload {
1941
- display: flex;
1942
- flex-direction: column;
1943
- gap: var(--space-1);
1944
- width: 100%;
1945
- }
1946
-
1947
- .nb-form__file-upload-zone {
1948
- align-items: center;
1949
- background: var(--surface-1);
1950
- border: 1px dashed var(--border-color);
1951
- border-radius: var(--radius-lg);
1952
- box-sizing: border-box;
1953
- cursor: pointer;
1954
- display: flex;
1955
- justify-content: center;
1956
- min-height: 112px;
1957
- overflow: hidden;
1958
- position: relative;
1959
- transition: border-color var(--transition-base), background var(--transition-base), box-shadow var(--transition-base);
1960
- width: 100%;
1961
- }
1962
- .nb-form__file-upload-zone:hover:not(.nb-form__file-upload-zone--disabled):not(.nb-form__file-upload-zone--filled) {
1963
- background: color-mix(in srgb, var(--accent-color) 4%, var(--surface-1));
1964
- border-color: var(--accent-color);
1965
- }
1966
- .nb-form__file-upload-zone--active {
1967
- background: color-mix(in srgb, var(--accent-color) 8%, var(--surface-1));
1968
- border-color: var(--accent-color);
1969
- }
1970
- .nb-form__file-upload-zone--filled {
1971
- border-style: solid;
1972
- cursor: default;
1973
- min-height: 88px;
1974
- }
1975
- .nb-form__file-upload-zone--uploading {
1976
- pointer-events: none;
1977
- }
1978
- .nb-form__file-upload-zone--disabled {
1979
- cursor: not-allowed;
1980
- opacity: 0.72;
1981
- }
1982
- .nb-form__file-upload-zone:focus-visible {
1983
- box-shadow: 0 0 0 3px var(--focus-ring-color);
1984
- outline: none;
1985
- }
1986
-
1987
- .nb-form__file-upload--image .nb-form__file-upload-zone {
1988
- min-height: 168px;
1989
- }
1990
-
1991
- .nb-form__file-upload--image .nb-form__file-upload-zone--filled {
1992
- min-height: 180px;
1993
- }
1994
-
1995
- .nb-form__file-upload--invalid .nb-form__file-upload-zone {
1996
- border-color: var(--error-color);
1997
- }
1998
-
1999
- .nb-form__file-upload-placeholder {
2000
- align-items: center;
2001
- display: flex;
2002
- flex-direction: column;
2003
- gap: var(--space-2);
2004
- max-width: 320px;
2005
- padding: var(--space-4);
2006
- text-align: center;
2007
- }
2008
-
2009
- .nb-form__file-upload-icon {
2010
- align-items: center;
2011
- background: color-mix(in srgb, var(--accent-color) 10%, transparent);
2012
- border-radius: 999px;
2013
- color: var(--accent-color);
2014
- display: inline-flex;
2015
- font-size: 24px;
2016
- height: 48px;
2017
- justify-content: center;
2018
- width: 48px;
2019
- }
2020
-
2021
- .nb-form__file-upload-title {
2022
- color: var(--text-primary);
2023
- font-size: var(--font-size-sm);
2024
- font-weight: var(--font-weight-semibold);
2025
- }
2026
-
2027
- .nb-form__file-upload-hint {
2028
- color: var(--text-tertiary);
2029
- font-size: var(--font-size-xs);
2030
- line-height: var(--line-height-tight);
2031
- }
2032
-
2033
- .nb-form__file-upload-preview {
2034
- display: block;
2035
- height: 100%;
2036
- max-height: 220px;
2037
- object-fit: contain;
2038
- width: 100%;
2039
- }
2040
-
2041
- .nb-form__file-upload-file {
2042
- align-items: center;
2043
- display: flex;
2044
- gap: var(--space-3);
2045
- max-width: 100%;
2046
- padding: var(--space-3) var(--space-4);
2047
- width: 100%;
2048
- }
2049
-
2050
- .nb-form__file-upload-file-icon {
2051
- align-items: center;
2052
- background: var(--surface-0);
2053
- border: 1px solid var(--border-subtle);
2054
- border-radius: var(--radius-md);
2055
- color: var(--accent-color);
2056
- display: inline-flex;
2057
- flex: 0 0 auto;
2058
- font-size: 22px;
2059
- height: 44px;
2060
- justify-content: center;
2061
- width: 44px;
2062
- }
2063
-
2064
- .nb-form__file-upload-file-meta {
2065
- display: flex;
2066
- flex: 1 1 auto;
2067
- flex-direction: column;
2068
- gap: 2px;
2069
- min-width: 0;
2070
- }
2071
-
2072
- .nb-form__file-upload-file-name {
2073
- color: var(--text-primary);
2074
- font-size: var(--font-size-sm);
2075
- font-weight: var(--font-weight-medium);
2076
- overflow: hidden;
2077
- text-overflow: ellipsis;
2078
- white-space: nowrap;
2079
- }
2080
-
2081
- .nb-form__file-upload-file-link {
2082
- color: var(--accent-color);
2083
- font-size: var(--font-size-xs);
2084
- text-decoration: none;
2085
- }
2086
- .nb-form__file-upload-file-link:hover {
2087
- text-decoration: underline;
2088
- }
2089
-
2090
- .nb-form__file-upload-overlay {
2091
- align-items: center;
2092
- background: rgba(0, 0, 0, 0.42);
2093
- color: #fff;
2094
- display: flex;
2095
- flex-direction: column;
2096
- font-size: var(--font-size-sm);
2097
- gap: var(--space-2);
2098
- inset: 0;
2099
- justify-content: center;
2100
- position: absolute;
2101
- }
2102
-
2103
- .nb-form__file-upload-spinner {
2104
- animation: nb-form-file-spin 700ms linear infinite;
2105
- border: 2px solid rgba(255, 255, 255, 0.35);
2106
- border-radius: 999px;
2107
- border-top-color: #fff;
2108
- height: 24px;
2109
- width: 24px;
2110
- }
2111
-
2112
- @keyframes nb-form-file-spin {
2113
- to {
2114
- transform: rotate(360deg);
2115
- }
2116
- }
2117
- .nb-form__file-upload-actions {
2118
- align-items: center;
2119
- background: linear-gradient(to top, rgba(0, 0, 0, 0.58), transparent);
2120
- bottom: 0;
2121
- display: flex;
2122
- gap: var(--space-2);
2123
- inset-inline: 0;
2124
- justify-content: center;
2125
- opacity: 0;
2126
- padding: var(--space-3);
2127
- position: absolute;
2128
- transition: opacity var(--transition-base);
2129
- }
2130
-
2131
- .nb-form__file-upload-zone--filled:hover .nb-form__file-upload-actions,
2132
- .nb-form__file-upload-zone--filled:focus-within .nb-form__file-upload-actions {
2133
- opacity: 1;
2134
- }
2135
-
2136
- .nb-form__file-upload-action {
2137
- align-items: center;
2138
- background: var(--surface-1);
2139
- border: 1px solid var(--border-subtle);
2140
- border-radius: var(--radius-md);
2141
- color: var(--text-primary);
2142
- cursor: pointer;
2143
- display: inline-flex;
2144
- font: inherit;
2145
- font-size: var(--font-size-xs);
2146
- font-weight: var(--font-weight-medium);
2147
- gap: var(--space-1);
2148
- min-height: 28px;
2149
- padding: 0 var(--space-2);
2150
- transition: background var(--transition-base), border-color var(--transition-base), color var(--transition-base);
2151
- }
2152
- .nb-form__file-upload-action:hover {
2153
- border-color: var(--accent-color);
2154
- color: var(--accent-color);
2155
- }
2156
- .nb-form__file-upload-action--danger:hover {
2157
- border-color: var(--error-color);
2158
- color: var(--error-color);
2159
- }
2160
- .nb-form__file-upload-action:focus-visible {
2161
- box-shadow: 0 0 0 2px var(--focus-ring-color);
2162
- outline: none;
2163
- }
2164
-
2165
- .nb-form__file-upload-error {
2166
- align-items: center;
2167
- color: var(--error-color);
2168
- display: inline-flex;
2169
- font-size: var(--font-size-xs);
2170
- gap: var(--space-1);
2171
- }
2172
-
2173
1708
  .nb-form {
2174
1709
  color: var(--text-primary);
2175
1710
  display: flex;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubitio/crud",
3
- "version": "0.5.15",
3
+ "version": "0.5.19",
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,10 +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.15",
60
- "@nubitio/ui": "^0.5.15"
61
- },
62
- "dependencies": {
63
- "react-dropzone": "^15.0.0"
59
+ "@nubitio/core": "^0.5.19",
60
+ "@nubitio/ui": "^0.5.19"
64
61
  }
65
62
  }