@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 +23 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +15 -0
- package/dist/rich-url.d.ts +17 -5
- package/dist/rich-url.d.ts.map +1 -1
- package/dist/rich-url.js +43 -10
- package/package.json +1 -1
- package/src/__tests__/rich-url.test.tsx +35 -0
- package/src/dialogs/dynamic-record.tsx +21 -0
- package/src/rich-url.tsx +82 -29
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":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;
|
|
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') &&
|
package/dist/rich-url.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
62
|
-
* full
|
|
63
|
-
* dead image URL is still reachable (and
|
|
64
|
-
*
|
|
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;
|
package/dist/rich-url.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rich-url.d.ts","sourceRoot":"","sources":["../src/rich-url.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,MAAM,OAAO,CAAA;
|
|
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
|
-
/**
|
|
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) ||
|
|
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
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
@@ -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
|
-
/**
|
|
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) ||
|
|
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
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
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
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
className
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|