@asteby/metacore-runtime-react 37.5.0 → 37.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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 37.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8231b21: Fix `ensureHref` mangling every root-relative asset URL (the platform's own
8
+ convention for locally-served files — uploads, hub-generated images,
9
+ printable documents) into a broken `https:///storage/…` link (scheme + empty
10
+ host). Root-relative paths (`/storage/…`), protocol-relative (`//…`) and
11
+ fully-qualified URLs now pass through unchanged; only a bare host like
12
+ `github.com/x` still gets `https://` prefixed.
13
+
14
+ Also: clicking an image thumbnail (table cell, detail dialog, linkified free
15
+ text — anywhere `ImageThumbnail`/`MediaValue` renders) now opens a full-size
16
+ **preview dialog** instead of navigating to the raw file in a new tab. This
17
+ is the platform-wide click-to-zoom behavior; no caller builds its own
18
+ lightbox.
19
+
20
+ ## 37.5.1
21
+
22
+ ### Patch Changes
23
+
24
+ - 2582a3a: El multi-select declarativo (`field.multiple: true`, ver #881/#883) no se veía en el modal REAL de editar/crear registro (`dialogs/dynamic-record.tsx`, usado por la tabla genérica del host) — nunca se había cableado ahí, solo en `DynamicForm` (dynamic-form.tsx, usado por acciones). Además, `isLineItemsField` trataba CUALQUIER valor array (incluido el `[]` default de un campo `multiple` recién creado) como un documento de line-items y lo renderizaba "Solo lectura". Se agrega el branch de `DynamicMultiSelectField` antes del picker single-value y se excluye `field.multiple` de la heurística de line-items.
25
+
3
26
  ## 37.5.0
4
27
 
5
28
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AA+C1C,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAEjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAoB3C,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEnE,OAAO,EAAqC,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAK1F,YAAY,EAAE,WAAW,EAAE,CAAA;AAE3B,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,QAAQ;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAA;IACpH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAA;IACvB,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;OAQG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,SAAS,EAAE,CAAA;IACxB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,SAAS,EAAE,CAAA;IACzB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,WAAW,CAAA;IAC1B,0CAA0C;IAC1C,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AA0CD,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC;;;;OAIG;IACH,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAA;IACzB,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;2DAEuD;IACvD,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAClF;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACpG;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAA;IAC1C;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AASD,wBAAgB,WAAW,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAK1D;AAaD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAG,GAAG,CAgBtE;AAID,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,EAAE,GAAG,SAAS,CAExE;AAQD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAerE;AASD,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAG,cAAc,GAAG,IAAI,CAa5F;AA+GD,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAUjE;AAaD,wBAAgB,mBAAmB,CAC/B,MAAM,EAAE,QAAQ,EAAE,GAAG,SAAS,EAC9B,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,EAChC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjC,QAAQ,EAAE,CAOZ;AAUD,wBAAgB,sBAAsB,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,MAAM,EAAE,QAAQ,EAAE,GAAG,SAAS,EAC9B,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GACjC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CASrB;AAiBD,wBAAgB,mBAAmB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,sBAAsB,EACtB,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,MAAM,EACN,QAAQ,EACR,MAAM,EACN,cAAc,EACd,aAAa,EACb,WAA8B,EAC9B,QAAQ,EACR,QAAQ,EACR,QAAQ,GACX,EAAE,wBAAwB,+BAkkB1B;AAuED,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,GAAG,CAAA;CAAE,+BAelF;AA+GD,wBAAgB,SAAS,CAAC,EACtB,KAAK,EACL,KAAK,EAAE,QAAQ,EACf,MAAM,EACN,WAAW,EAAE,eAAe,EAC5B,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,YAAY,GACzB,EAAE;IACC,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,MAAM,EAAE,GAAG,CAAA;IACX,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB,+BA6SA;AAwLD,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;IACnE,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC5B,iFAAiF;IACjF,MAAM,CAAC,EAAE,GAAG,CAAA;IACZ,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB,+BA+NA"}
@@ -26,6 +26,7 @@ import { useApi } from '../api-context';
26
26
  import { useBranchCreateGate } from '../branch-create-gate';
27
27
  import { toastServerError, extractFieldErrors, localizeFieldIssue, localizeFieldErrorMap } from '../server-error';
28
28
  import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-field';
29
+ import { DynamicMultiSelectField } from '../dynamic-multi-select-field';
29
30
  import { DynamicRelations } from '../dynamic-relations';
30
31
  import { useOptionsResolver } from '../use-options-resolver';
31
32
  import { getFieldRef, getVisibleWhen, evaluateVisibleWhen } from '../dynamic-form-schema';
@@ -114,6 +115,13 @@ export function fieldItemFields(field) {
114
115
  export function isLineItemsField(field, value) {
115
116
  if (field.type === 'image' || field.widget === 'upload')
116
117
  return false;
118
+ // A `ref` field declared `multiple: true` (DynamicMultiSelectField) is a
119
+ // plain array of target ids, not a structured line-items document — it
120
+ // has its own editable picker and must not fall into the read-only
121
+ // inline-table branch just because its current value is an array
122
+ // (including the [] default on a freshly-created record).
123
+ if (field.multiple)
124
+ return false;
117
125
  if (fieldItemFields(field)?.length)
118
126
  return true;
119
127
  if (Array.isArray(value))
@@ -1133,6 +1141,13 @@ export function EditField({ field, value, onChange, record, invalid }) {
1133
1141
  // else optionsConfig.source → /api/options/<source>). Without the `type`
1134
1142
  // check a source-backed picker with no `ref` silently degraded to a raw text
1135
1143
  // input. Static inline `options` are handled by the enum <Select> branch below.
1144
+ // Multi-value ref field (field.multiple:true — see DynamicMultiSelectField):
1145
+ // checked BEFORE the single-value dynamic_select branch below, which would
1146
+ // otherwise claim it too (both trigger on the same `ref`/`getFieldRef`
1147
+ // check) and render a picker that stores a single id instead of an array.
1148
+ if (getFieldRef(field) && field.multiple) {
1149
+ return (_jsx(DynamicMultiSelectField, { field: field, value: value, onChange: onChange }));
1150
+ }
1136
1151
  if ((getFieldRef(field) ||
1137
1152
  field.type === 'dynamic_select' ||
1138
1153
  field.widget === 'dynamic_select') &&
@@ -24,7 +24,18 @@ export declare function isImageUrl(url: string): boolean;
24
24
  export declare function isFileUrl(url: string): boolean;
25
25
  /** Classify a URL so the right primitive (thumbnail / file chip / link) renders. */
26
26
  export declare function classifyUrl(url: string): UrlKind;
27
- /** Ensure a URL is absolute so `<a href>` and `new URL()` behave. */
27
+ /**
28
+ * Ensure a URL is safe to drop into `<a href>` / `new URL(..., base)`.
29
+ *
30
+ * Three shapes pass through unchanged: fully qualified (`http(s)://…`),
31
+ * protocol-relative (`//…`), and — the platform's own convention for every
32
+ * locally-served asset (uploads, hub-generated images, printable documents:
33
+ * see handlers.UploadFile, hub_image_provider_bridge.go, …) — ROOT-RELATIVE
34
+ * (`/storage/…`). A root-relative path already resolves correctly against
35
+ * the current origin; treating it as a bare host (the old behavior) produced
36
+ * "https:///storage/…" (scheme + empty host), a broken link. Anything else
37
+ * (`github.com/foo`) is assumed to be a bare host missing its scheme.
38
+ */
28
39
  export declare function ensureHref(url: string): string;
29
40
  /**
30
41
  * A short, human label for a URL. For a file-like URL (last segment carries an
@@ -58,10 +69,11 @@ export declare const FileChip: React.FC<{
58
69
  className?: string;
59
70
  }>;
60
71
  /**
61
- * Inline image thumbnail — rounded, bordered, `object-cover` — that opens the
62
- * full image in a new tab. On load error it degrades to a normal link chip so a
63
- * dead image URL is still reachable (and never a broken-image icon). `maxHeight`
64
- * keeps a cell thumbnail small (~h-8) while the dialog shows a larger preview.
72
+ * Inline image thumbnail — rounded, bordered, `object-cover` — that opens a
73
+ * full-size PREVIEW (a lightbox dialog, not a new tab). On load error it
74
+ * degrades to a normal link chip so a dead image URL is still reachable (and
75
+ * never a broken-image icon). `maxHeight` keeps a cell thumbnail small
76
+ * (~h-8) while the dialog shows a larger preview.
65
77
  */
66
78
  export declare const ImageThumbnail: React.FC<{
67
79
  url: string;
@@ -1 +1 @@
1
- {"version":3,"file":"rich-url.d.ts","sourceRoot":"","sources":["../src/rich-url.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"rich-url.d.ts","sourceRoot":"","sources":["../src/rich-url.tsx"],"names":[],"mappings":"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"}
package/dist/rich-url.js CHANGED
@@ -19,6 +19,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
19
19
  */
20
20
  import React from 'react';
21
21
  import { cn } from '@asteby/metacore-ui/lib';
22
+ import { Dialog, DialogContent, DialogTitle } from '@asteby/metacore-ui/primitives';
22
23
  import { DynamicIcon } from './dynamic-icon';
23
24
  /** Image file extensions we render as an inline thumbnail. */
24
25
  const IMAGE_EXT_RE = /\.(jpe?g|png|gif|webp|avif|svg|bmp|ico)(?:[?#].*)?$/i;
@@ -46,9 +47,22 @@ export function classifyUrl(url) {
46
47
  return 'file';
47
48
  return 'link';
48
49
  }
49
- /** Ensure a URL is absolute so `<a href>` and `new URL()` behave. */
50
+ /**
51
+ * Ensure a URL is safe to drop into `<a href>` / `new URL(..., base)`.
52
+ *
53
+ * Three shapes pass through unchanged: fully qualified (`http(s)://…`),
54
+ * protocol-relative (`//…`), and — the platform's own convention for every
55
+ * locally-served asset (uploads, hub-generated images, printable documents:
56
+ * see handlers.UploadFile, hub_image_provider_bridge.go, …) — ROOT-RELATIVE
57
+ * (`/storage/…`). A root-relative path already resolves correctly against
58
+ * the current origin; treating it as a bare host (the old behavior) produced
59
+ * "https:///storage/…" (scheme + empty host), a broken link. Anything else
60
+ * (`github.com/foo`) is assumed to be a bare host missing its scheme.
61
+ */
50
62
  export function ensureHref(url) {
51
- return /^(https?:)?\/\//i.test(url) || /^(mailto|tel):/i.test(url)
63
+ return /^(https?:)?\/\//i.test(url) ||
64
+ /^(mailto|tel):/i.test(url) ||
65
+ url.startsWith('/')
52
66
  ? url
53
67
  : `https://${url}`;
54
68
  }
@@ -106,20 +120,39 @@ export const FileChip = ({ url, maxLabelWidth = 240, className }) => {
106
120
  return (_jsxs("a", { href: href, target: "_blank", rel: "noopener noreferrer", title: url, onClick: stop, className: cn('inline-flex max-w-full items-center gap-1.5 rounded-md border bg-muted/40 px-2 py-1 align-middle text-sm font-medium text-foreground hover:bg-muted', className), children: [_jsx(DynamicIcon, { name: "FileText", className: "h-3.5 w-3.5 shrink-0 opacity-70" }), _jsx("span", { className: "truncate", style: { maxWidth: maxLabelWidth }, children: fileNameFromUrl(url) })] }));
107
121
  };
108
122
  /**
109
- * Inline image thumbnail — rounded, bordered, `object-cover` — that opens the
110
- * full image in a new tab. On load error it degrades to a normal link chip so a
111
- * dead image URL is still reachable (and never a broken-image icon). `maxHeight`
112
- * keeps a cell thumbnail small (~h-8) while the dialog shows a larger preview.
123
+ * Full-size image preview — a Dialog overlay, not a new tab. Click-to-zoom is
124
+ * the platform-wide behavior for every inline thumbnail (table cell, detail
125
+ * dialog, linkified free text): the caller never builds its own lightbox.
126
+ * Download / "open original" stays one click away in the footer for the rare
127
+ * case someone wants the raw file.
128
+ */
129
+ const ImageLightbox = ({ src, alt, open, onOpenChange }) => (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-[min(92vw,1100px)] max-h-[92dvh] p-2 overflow-hidden border-0 bg-transparent shadow-none flex items-center justify-center", onClick: stop, children: [_jsx(DialogTitle, { className: "sr-only", children: alt }), _jsx("img", { src: src, alt: alt, className: "max-h-[86dvh] max-w-full rounded-lg object-contain shadow-2xl" })] }) }));
130
+ /**
131
+ * Inline image thumbnail — rounded, bordered, `object-cover` — that opens a
132
+ * full-size PREVIEW (a lightbox dialog, not a new tab). On load error it
133
+ * degrades to a normal link chip so a dead image URL is still reachable (and
134
+ * never a broken-image icon). `maxHeight` keeps a cell thumbnail small
135
+ * (~h-8) while the dialog shows a larger preview.
113
136
  */
114
137
  export const ImageThumbnail = ({ url, getImageUrl, maxHeight = 160, className }) => {
115
138
  const [failed, setFailed] = React.useState(false);
116
- const href = ensureHref(url);
139
+ const [open, setOpen] = React.useState(false);
117
140
  // Absolute URLs are used verbatim; only relative storage paths go through
118
- // the host's image resolver (which prefixes the media base).
119
- const src = /^(https?:)?\/\//i.test(url) ? url : getImageUrl ? getImageUrl(url) : url;
141
+ // the host's image resolver (which prefixes the media base). Anything
142
+ // that opens the raw file (download, the lightbox <img>) MUST resolve the
143
+ // same way — otherwise a relative "/storage/…" path (the platform's own
144
+ // convention: uploads, hub-generated assets, etc.) never matches
145
+ // ensureHref's "//"-prefixed check and gets misread as a bare host,
146
+ // producing "https:///storage/…".
147
+ const isAbsolute = /^(https?:)?\/\//i.test(url);
148
+ const src = isAbsolute ? url : getImageUrl ? getImageUrl(url) : url;
149
+ const label = smartUrlLabel(url);
120
150
  if (failed)
121
151
  return _jsx(UrlChip, { url: url, icon: "Image" });
122
- return (_jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", title: url, onClick: stop, className: "inline-block max-w-full align-middle", children: _jsx("img", { src: src, alt: smartUrlLabel(url), onError: () => setFailed(true), style: { maxHeight }, className: cn('max-w-full rounded-md border object-cover', className) }) }));
152
+ return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", title: label, onClick: (e) => {
153
+ stop(e);
154
+ setOpen(true);
155
+ }, className: "inline-block max-w-full cursor-zoom-in appearance-none border-0 bg-transparent p-0 align-middle", children: _jsx("img", { src: src, alt: label, onError: () => setFailed(true), style: { maxHeight }, className: cn('max-w-full rounded-md border object-cover', className) }) }), _jsx(ImageLightbox, { src: src, alt: label, open: open, onOpenChange: setOpen })] }));
123
156
  };
124
157
  /**
125
158
  * One URL value → the right primitive (thumbnail / file chip / link chip).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "37.5.0",
3
+ "version": "37.6.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -86,6 +86,17 @@ describe('smartUrlLabel / fileNameFromUrl / ensureHref', () => {
86
86
  expect(ensureHref('https://a.com')).toBe('https://a.com')
87
87
  expect(ensureHref('mailto:a@b.com')).toBe('mailto:a@b.com')
88
88
  })
89
+
90
+ it('leaves a root-relative storage path unchanged (never "https:///…")', () => {
91
+ // The platform's own convention for every locally-served asset —
92
+ // uploads, hub-generated images (hub_image_provider_bridge.go),
93
+ // printable documents. Regression for the bug where this produced
94
+ // "https:///storage/…" (scheme + empty host, a dead link).
95
+ expect(ensureHref('/storage/hub-generations/x.png')).toBe(
96
+ '/storage/hub-generations/x.png'
97
+ )
98
+ expect(ensureHref('//cdn.example.com/x.png')).toBe('//cdn.example.com/x.png')
99
+ })
89
100
  })
90
101
 
91
102
  describe('splitTrailingPunct', () => {
@@ -173,6 +184,30 @@ describe('primitives render', () => {
173
184
  expect(screen.queryByRole('link')).toBeTruthy()
174
185
  })
175
186
 
187
+ it('ImageThumbnail resolves a relative storage path through getImageUrl', () => {
188
+ const getImageUrl = (path: string) => `http://api.local${path}`
189
+ const { container } = render(
190
+ <ImageThumbnail
191
+ url="/storage/hub-generations/x.png"
192
+ getImageUrl={getImageUrl}
193
+ />
194
+ )
195
+ const img = container.querySelector('img') as HTMLImageElement
196
+ // Regression: this used to leak into an <a href> built by ensureHref
197
+ // as "https:///storage/hub-generations/x.png" (bare-host misread of a
198
+ // relative path). There's no <a> anymore — a click opens the lightbox.
199
+ expect(img.getAttribute('src')).toBe('http://api.local/storage/hub-generations/x.png')
200
+ })
201
+
202
+ it('ImageThumbnail opens a lightbox preview on click instead of navigating', () => {
203
+ render(<ImageThumbnail url="https://cdn.x/photo.png" />)
204
+ expect(screen.queryByRole('dialog')).toBeFalsy()
205
+ fireEvent.click(screen.getByRole('button'))
206
+ const dialog = screen.getByRole('dialog')
207
+ expect(dialog).toBeTruthy()
208
+ expect(dialog.querySelector('img')?.getAttribute('src')).toBe('https://cdn.x/photo.png')
209
+ })
210
+
176
211
  it('MediaValue dispatches by kind', () => {
177
212
  const { rerender, container } = render(
178
213
  <MediaValue url="https://cdn.x/a.png" />
@@ -56,6 +56,7 @@ import { useApi } from '../api-context'
56
56
  import { useBranchCreateGate } from '../branch-create-gate'
57
57
  import { toastServerError, extractFieldErrors, localizeFieldIssue, localizeFieldErrorMap } from '../server-error'
58
58
  import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-field'
59
+ import { DynamicMultiSelectField } from '../dynamic-multi-select-field'
59
60
  import { DynamicRelations } from '../dynamic-relations'
60
61
  import { useOptionsResolver, type ResolvedOption } from '../use-options-resolver'
61
62
  import { getFieldRef, getVisibleWhen, evaluateVisibleWhen } from '../dynamic-form-schema'
@@ -376,6 +377,12 @@ export function fieldItemFields(field: FieldDef): ItemField[] | undefined {
376
377
  // editable widgets (media/upload, color, dates) are NOT line-items.
377
378
  export function isLineItemsField(field: FieldDef, value: any): boolean {
378
379
  if (field.type === 'image' || field.widget === 'upload') return false
380
+ // A `ref` field declared `multiple: true` (DynamicMultiSelectField) is a
381
+ // plain array of target ids, not a structured line-items document — it
382
+ // has its own editable picker and must not fall into the read-only
383
+ // inline-table branch just because its current value is an array
384
+ // (including the [] default on a freshly-created record).
385
+ if ((field as ActionFieldDef).multiple) return false
379
386
  if (fieldItemFields(field)?.length) return true
380
387
  if (Array.isArray(value)) return true
381
388
  return (
@@ -1990,6 +1997,20 @@ export function EditField({ field, value, onChange, record, invalid }: {
1990
1997
  // else optionsConfig.source → /api/options/<source>). Without the `type`
1991
1998
  // check a source-backed picker with no `ref` silently degraded to a raw text
1992
1999
  // input. Static inline `options` are handled by the enum <Select> branch below.
2000
+ // Multi-value ref field (field.multiple:true — see DynamicMultiSelectField):
2001
+ // checked BEFORE the single-value dynamic_select branch below, which would
2002
+ // otherwise claim it too (both trigger on the same `ref`/`getFieldRef`
2003
+ // check) and render a picker that stores a single id instead of an array.
2004
+ if (getFieldRef(field as ActionFieldDef) && (field as ActionFieldDef).multiple) {
2005
+ return (
2006
+ <DynamicMultiSelectField
2007
+ field={field as ActionFieldDef}
2008
+ value={value}
2009
+ onChange={onChange}
2010
+ />
2011
+ )
2012
+ }
2013
+
1993
2014
  if (
1994
2015
  (getFieldRef(field as ActionFieldDef) ||
1995
2016
  field.type === 'dynamic_select' ||
package/src/rich-url.tsx CHANGED
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import React from 'react'
20
20
  import { cn } from '@asteby/metacore-ui/lib'
21
+ import { Dialog, DialogContent, DialogTitle } from '@asteby/metacore-ui/primitives'
21
22
  import { DynamicIcon } from './dynamic-icon'
22
23
 
23
24
  /** Image file extensions we render as an inline thumbnail. */
@@ -53,9 +54,22 @@ export function classifyUrl(url: string): UrlKind {
53
54
  return 'link'
54
55
  }
55
56
 
56
- /** Ensure a URL is absolute so `<a href>` and `new URL()` behave. */
57
+ /**
58
+ * Ensure a URL is safe to drop into `<a href>` / `new URL(..., base)`.
59
+ *
60
+ * Three shapes pass through unchanged: fully qualified (`http(s)://…`),
61
+ * protocol-relative (`//…`), and — the platform's own convention for every
62
+ * locally-served asset (uploads, hub-generated images, printable documents:
63
+ * see handlers.UploadFile, hub_image_provider_bridge.go, …) — ROOT-RELATIVE
64
+ * (`/storage/…`). A root-relative path already resolves correctly against
65
+ * the current origin; treating it as a bare host (the old behavior) produced
66
+ * "https:///storage/…" (scheme + empty host), a broken link. Anything else
67
+ * (`github.com/foo`) is assumed to be a bare host missing its scheme.
68
+ */
57
69
  export function ensureHref(url: string): string {
58
- return /^(https?:)?\/\//i.test(url) || /^(mailto|tel):/i.test(url)
70
+ return /^(https?:)?\/\//i.test(url) ||
71
+ /^(mailto|tel):/i.test(url) ||
72
+ url.startsWith('/')
59
73
  ? url
60
74
  : `https://${url}`
61
75
  }
@@ -162,10 +176,39 @@ export const FileChip: React.FC<{
162
176
  }
163
177
 
164
178
  /**
165
- * Inline image thumbnail — rounded, bordered, `object-cover` — that opens the
166
- * full image in a new tab. On load error it degrades to a normal link chip so a
167
- * dead image URL is still reachable (and never a broken-image icon). `maxHeight`
168
- * keeps a cell thumbnail small (~h-8) while the dialog shows a larger preview.
179
+ * Full-size image preview — a Dialog overlay, not a new tab. Click-to-zoom is
180
+ * the platform-wide behavior for every inline thumbnail (table cell, detail
181
+ * dialog, linkified free text): the caller never builds its own lightbox.
182
+ * Download / "open original" stays one click away in the footer for the rare
183
+ * case someone wants the raw file.
184
+ */
185
+ const ImageLightbox: React.FC<{
186
+ src: string
187
+ alt: string
188
+ open: boolean
189
+ onOpenChange: (open: boolean) => void
190
+ }> = ({ src, alt, open, onOpenChange }) => (
191
+ <Dialog open={open} onOpenChange={onOpenChange}>
192
+ <DialogContent
193
+ className="max-w-[min(92vw,1100px)] max-h-[92dvh] p-2 overflow-hidden border-0 bg-transparent shadow-none flex items-center justify-center"
194
+ onClick={stop}
195
+ >
196
+ <DialogTitle className="sr-only">{alt}</DialogTitle>
197
+ <img
198
+ src={src}
199
+ alt={alt}
200
+ className="max-h-[86dvh] max-w-full rounded-lg object-contain shadow-2xl"
201
+ />
202
+ </DialogContent>
203
+ </Dialog>
204
+ )
205
+
206
+ /**
207
+ * Inline image thumbnail — rounded, bordered, `object-cover` — that opens a
208
+ * full-size PREVIEW (a lightbox dialog, not a new tab). On load error it
209
+ * degrades to a normal link chip so a dead image URL is still reachable (and
210
+ * never a broken-image icon). `maxHeight` keeps a cell thumbnail small
211
+ * (~h-8) while the dialog shows a larger preview.
169
212
  */
170
213
  export const ImageThumbnail: React.FC<{
171
214
  url: string
@@ -175,32 +218,42 @@ export const ImageThumbnail: React.FC<{
175
218
  className?: string
176
219
  }> = ({ url, getImageUrl, maxHeight = 160, className }) => {
177
220
  const [failed, setFailed] = React.useState(false)
178
- const href = ensureHref(url)
221
+ const [open, setOpen] = React.useState(false)
179
222
  // Absolute URLs are used verbatim; only relative storage paths go through
180
- // the host's image resolver (which prefixes the media base).
181
- const src =
182
- /^(https?:)?\/\//i.test(url) ? url : getImageUrl ? getImageUrl(url) : url
223
+ // the host's image resolver (which prefixes the media base). Anything
224
+ // that opens the raw file (download, the lightbox <img>) MUST resolve the
225
+ // same way — otherwise a relative "/storage/…" path (the platform's own
226
+ // convention: uploads, hub-generated assets, etc.) never matches
227
+ // ensureHref's "//"-prefixed check and gets misread as a bare host,
228
+ // producing "https:///storage/…".
229
+ const isAbsolute = /^(https?:)?\/\//i.test(url)
230
+ const src = isAbsolute ? url : getImageUrl ? getImageUrl(url) : url
231
+ const label = smartUrlLabel(url)
183
232
  if (failed) return <UrlChip url={url} icon="Image" />
184
233
  return (
185
- <a
186
- href={href}
187
- target="_blank"
188
- rel="noopener noreferrer"
189
- title={url}
190
- onClick={stop}
191
- className="inline-block max-w-full align-middle"
192
- >
193
- <img
194
- src={src}
195
- alt={smartUrlLabel(url)}
196
- onError={() => setFailed(true)}
197
- style={{ maxHeight }}
198
- className={cn(
199
- 'max-w-full rounded-md border object-cover',
200
- className
201
- )}
202
- />
203
- </a>
234
+ <>
235
+ <button
236
+ type="button"
237
+ title={label}
238
+ onClick={(e) => {
239
+ stop(e)
240
+ setOpen(true)
241
+ }}
242
+ className="inline-block max-w-full cursor-zoom-in appearance-none border-0 bg-transparent p-0 align-middle"
243
+ >
244
+ <img
245
+ src={src}
246
+ alt={label}
247
+ onError={() => setFailed(true)}
248
+ style={{ maxHeight }}
249
+ className={cn(
250
+ 'max-w-full rounded-md border object-cover',
251
+ className
252
+ )}
253
+ />
254
+ </button>
255
+ <ImageLightbox src={src} alt={label} open={open} onOpenChange={setOpen} />
256
+ </>
204
257
  )
205
258
  }
206
259