@airoom/nextmin-react 0.1.9 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/viewer/DynamicViewer.d.ts +7 -0
- package/dist/components/viewer/DynamicViewer.js +223 -0
- package/dist/components/viewer/useViewDrawer.d.ts +12 -0
- package/dist/components/viewer/useViewDrawer.js +24 -0
- package/dist/nextmin.css +1 -1
- package/dist/views/ListPage.js +61 -7
- package/dist/views/list/DataTableHero.d.ts +6 -1
- package/dist/views/list/DataTableHero.js +9 -3
- package/dist/views/list/jsonSummary.d.ts +6 -0
- package/dist/views/list/jsonSummary.js +165 -0
- package/package.json +1 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { formatCell } from '../../views/list/formatters';
|
|
5
|
+
import { summarizeObject } from '../../views/list/jsonSummary';
|
|
6
|
+
// Hide technical keys in the viewer
|
|
7
|
+
const HIDDEN_KEYS = new Set(['id', '_id', 'v', '__v', 'baseId', 'exId', '__childId']);
|
|
8
|
+
// Detect password-like keys generically (no value should be visible)
|
|
9
|
+
function normKey(k) {
|
|
10
|
+
return k.toLowerCase().replace(/[^a-z]/g, '');
|
|
11
|
+
}
|
|
12
|
+
function isPasswordKeyRaw(k) {
|
|
13
|
+
const n = normKey(k);
|
|
14
|
+
// Generic detection: match common variants and substrings
|
|
15
|
+
if (n === 'password' || n === 'pwd')
|
|
16
|
+
return true;
|
|
17
|
+
if (n.includes('password'))
|
|
18
|
+
return true; // e.g., userpassword, passwordhash
|
|
19
|
+
if (n.includes('passwd'))
|
|
20
|
+
return true; // passwd
|
|
21
|
+
if (n.includes('passcode'))
|
|
22
|
+
return true; // passcode
|
|
23
|
+
if (n.includes('secret'))
|
|
24
|
+
return true; // clientsecret, secretKey
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
function isPasswordByAttr(attr) {
|
|
28
|
+
const a = (Array.isArray(attr) ? attr[0] : attr);
|
|
29
|
+
const t = String(a?.type ?? '').toLowerCase();
|
|
30
|
+
const f = String(a?.format ?? '').toLowerCase();
|
|
31
|
+
return t === 'password' || f === 'password';
|
|
32
|
+
}
|
|
33
|
+
// --- small helpers (duplicated minimal logic from table) ---
|
|
34
|
+
const IMG_EXT_RE = /(png|jpe?g|gif|webp|bmp|svg|avif|heic|heif)$/i;
|
|
35
|
+
function isUrlLike(s) {
|
|
36
|
+
return /^https?:\/\//i.test(s) || s.startsWith('/') || s.startsWith('data:');
|
|
37
|
+
}
|
|
38
|
+
function isLikelyImageUrl(s) {
|
|
39
|
+
if (!isUrlLike(s))
|
|
40
|
+
return false;
|
|
41
|
+
if (s.startsWith('data:image/'))
|
|
42
|
+
return true;
|
|
43
|
+
try {
|
|
44
|
+
const url = new URL(s, 'http://x');
|
|
45
|
+
const path = url.pathname || '';
|
|
46
|
+
return IMG_EXT_RE.test(path.split('.').pop() || '');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
const path = s.split('?')[0] || '';
|
|
50
|
+
return IMG_EXT_RE.test(path.split('.').pop() || '');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function extractUrl(v) {
|
|
54
|
+
if (!v)
|
|
55
|
+
return null;
|
|
56
|
+
if (typeof v === 'string' && isLikelyImageUrl(v))
|
|
57
|
+
return v;
|
|
58
|
+
if (typeof v === 'object') {
|
|
59
|
+
const u = (v.url || v.src || v.value);
|
|
60
|
+
return u && isLikelyImageUrl(u) ? u : null;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function extractAllImageUrls(val) {
|
|
65
|
+
if (Array.isArray(val)) {
|
|
66
|
+
const list = val.map(extractUrl).filter(Boolean);
|
|
67
|
+
if (list.length)
|
|
68
|
+
return list;
|
|
69
|
+
return (val.filter((s) => typeof s === 'string' && isLikelyImageUrl(s)) || []);
|
|
70
|
+
}
|
|
71
|
+
const one = extractUrl(val);
|
|
72
|
+
return one ? [one] : [];
|
|
73
|
+
}
|
|
74
|
+
function humanizeLabel(key) {
|
|
75
|
+
const spaced = key.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/_/g, ' ');
|
|
76
|
+
return spaced.slice(0, 1).toUpperCase() + spaced.slice(1);
|
|
77
|
+
}
|
|
78
|
+
function isPrivate(attr) {
|
|
79
|
+
const a = (Array.isArray(attr) ? attr[0] : attr);
|
|
80
|
+
return !!a?.private;
|
|
81
|
+
}
|
|
82
|
+
function FieldRow({ label, children }) {
|
|
83
|
+
return (_jsxs("div", { className: "grid grid-cols-12 gap-3 border-b border-default-100 py-3", children: [_jsx("div", { className: "col-span-4 md:col-span-3 text-foreground/70 text-sm", children: label }), _jsx("div", { className: "col-span-8 md:col-span-9 break-words", children: children })] }));
|
|
84
|
+
}
|
|
85
|
+
function ImageThumbs({ urls }) {
|
|
86
|
+
return (_jsx("div", { className: "flex flex-wrap gap-2", children: urls.map((u, i) => (_jsx("a", { href: u, target: "_blank", rel: "noreferrer", children: _jsx("img", { src: u, alt: "image", width: 64, height: 64, className: "h-16 w-16 rounded-md object-cover border border-default-200 bg-default-100", loading: "lazy" }) }, `${u}-${i}`))) }));
|
|
87
|
+
}
|
|
88
|
+
/** ---------- Generic label + nested-list condensation ---------- */
|
|
89
|
+
const COMMON_LABEL_KEYS = ['name', 'title', 'label', 'display', 'show'];
|
|
90
|
+
function toLabel(v) {
|
|
91
|
+
if (v == null)
|
|
92
|
+
return null;
|
|
93
|
+
if (typeof v === 'string')
|
|
94
|
+
return v.trim() || null;
|
|
95
|
+
if (typeof v === 'number' || typeof v === 'boolean')
|
|
96
|
+
return String(v);
|
|
97
|
+
if (typeof v === 'object') {
|
|
98
|
+
// Prefer common label keys
|
|
99
|
+
for (const k of COMMON_LABEL_KEYS) {
|
|
100
|
+
const val = v[k];
|
|
101
|
+
if (typeof val === 'string' && val.trim())
|
|
102
|
+
return val;
|
|
103
|
+
}
|
|
104
|
+
// Fallbacks
|
|
105
|
+
if (typeof v.value === 'string' && v.value.trim())
|
|
106
|
+
return v.value;
|
|
107
|
+
if (typeof v.id === 'string' && v.id.trim())
|
|
108
|
+
return v.id;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
function listLabels(arr) {
|
|
113
|
+
if (!Array.isArray(arr))
|
|
114
|
+
return null;
|
|
115
|
+
const labels = arr
|
|
116
|
+
.map((it) => toLabel(it))
|
|
117
|
+
.filter((s) => !!s && !!s.trim());
|
|
118
|
+
return labels.length ? labels : null;
|
|
119
|
+
}
|
|
120
|
+
function summarizeList(labels, max = 10) {
|
|
121
|
+
if (labels.length <= max)
|
|
122
|
+
return labels.join(', ');
|
|
123
|
+
const shown = labels.slice(0, max).join(', ');
|
|
124
|
+
const more = labels.length - max;
|
|
125
|
+
return `${shown}, +${more} more`;
|
|
126
|
+
}
|
|
127
|
+
function tryCondenseEntity(obj) {
|
|
128
|
+
if (!obj || typeof obj !== 'object')
|
|
129
|
+
return null;
|
|
130
|
+
// Primary label from the object itself
|
|
131
|
+
const primary = toLabel(obj);
|
|
132
|
+
if (!primary)
|
|
133
|
+
return null;
|
|
134
|
+
// Find the most informative array property that yields labels
|
|
135
|
+
let best = null;
|
|
136
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
137
|
+
if (HIDDEN_KEYS.has(k) || isPasswordKeyRaw(k))
|
|
138
|
+
continue;
|
|
139
|
+
if (!Array.isArray(v))
|
|
140
|
+
continue;
|
|
141
|
+
const labels = listLabels(v);
|
|
142
|
+
if (labels && labels.length) {
|
|
143
|
+
if (!best || labels.length > best.length)
|
|
144
|
+
best = labels;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (best && best.length) {
|
|
148
|
+
return `${primary} — ${summarizeList(best)}`;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
function maybeParseJson(val) {
|
|
153
|
+
if (typeof val !== 'string')
|
|
154
|
+
return val;
|
|
155
|
+
const s = val.trim();
|
|
156
|
+
if (!s)
|
|
157
|
+
return val;
|
|
158
|
+
if (!(s.startsWith('{') || s.startsWith('[')))
|
|
159
|
+
return val;
|
|
160
|
+
try {
|
|
161
|
+
const parsed = JSON.parse(s);
|
|
162
|
+
if (parsed && (typeof parsed === 'object' || Array.isArray(parsed)))
|
|
163
|
+
return parsed;
|
|
164
|
+
return val;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return val;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function renderValue(input, key) {
|
|
171
|
+
const val = maybeParseJson(input);
|
|
172
|
+
if (val == null)
|
|
173
|
+
return _jsx("span", { className: "text-foreground/50", children: "\u2014" });
|
|
174
|
+
// Detect images
|
|
175
|
+
const urls = extractAllImageUrls(val);
|
|
176
|
+
if (urls.length)
|
|
177
|
+
return _jsx(ImageThumbs, { urls: urls });
|
|
178
|
+
// Primitive or simple object/array formatting via shared formatter
|
|
179
|
+
if (Array.isArray(val)) {
|
|
180
|
+
// Prefer shared summarizer for arrays
|
|
181
|
+
if (val.length && typeof val[0] === 'object') {
|
|
182
|
+
// Show each summarized object on its own line for readability
|
|
183
|
+
const lines = val
|
|
184
|
+
.map((v) => (v && typeof v === 'object' ? summarizeObject(v)?.text : null))
|
|
185
|
+
.filter((s) => !!s && !!s.trim());
|
|
186
|
+
if (lines.length) {
|
|
187
|
+
return (_jsx("div", { className: "grid gap-1", children: lines.map((s, i) => (_jsx("div", { className: "text-foreground/90", children: s }, i))) }));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Fallback detailed rendering
|
|
191
|
+
return (_jsx("div", { className: "grid gap-2", children: val.map((v, i) => (_jsx("div", { className: "rounded-medium border border-default-100 p-2", children: typeof v === 'object' && v !== null ? (_jsx(ObjectView, { obj: v })) : (_jsx("span", { children: formatCell(v, key) })) }, i))) }));
|
|
192
|
+
}
|
|
193
|
+
if (typeof val === 'object') {
|
|
194
|
+
const condensed = summarizeObject(val)?.text || tryCondenseEntity(val);
|
|
195
|
+
if (condensed)
|
|
196
|
+
return _jsx("span", { children: condensed });
|
|
197
|
+
return _jsx(ObjectView, { obj: val });
|
|
198
|
+
}
|
|
199
|
+
return _jsx("span", { children: formatCell(val, key) });
|
|
200
|
+
}
|
|
201
|
+
function ObjectView({ obj }) {
|
|
202
|
+
const entries = Object.entries(obj).filter(([k]) => !HIDDEN_KEYS.has(k) && !isPasswordKeyRaw(k));
|
|
203
|
+
if (!entries.length)
|
|
204
|
+
return _jsx("span", { className: "text-foreground/50", children: '{ }' });
|
|
205
|
+
return (_jsx("div", { className: "grid gap-1", children: entries.map(([k, v]) => (_jsxs("div", { className: "flex gap-2 items-start", children: [_jsxs("div", { className: "min-w-24 text-foreground/60 text-sm", children: [humanizeLabel(k), ":"] }), _jsx("div", { className: "flex-1", children: renderValue(v, k) })] }, k))) }));
|
|
206
|
+
}
|
|
207
|
+
export function DynamicViewer({ schema, data }) {
|
|
208
|
+
// Use schema order if available, otherwise object keys order
|
|
209
|
+
const attributes = (schema?.attributes ?? {});
|
|
210
|
+
const keys = React.useMemo(() => {
|
|
211
|
+
const base = Object.keys(attributes).filter((k) => {
|
|
212
|
+
if (HIDDEN_KEYS.has(k) || isPasswordKeyRaw(k))
|
|
213
|
+
return false;
|
|
214
|
+
if (isPasswordByAttr(attributes[k]))
|
|
215
|
+
return false;
|
|
216
|
+
return !isPrivate(attributes[k]);
|
|
217
|
+
});
|
|
218
|
+
if (base.length)
|
|
219
|
+
return base;
|
|
220
|
+
return Object.keys(data || {}).filter((k) => !HIDDEN_KEYS.has(k) && !isPasswordKeyRaw(k));
|
|
221
|
+
}, [attributes, data]);
|
|
222
|
+
return (_jsx("div", { className: "grid gap-1", children: keys.map((key) => (_jsx(FieldRow, { label: humanizeLabel(key), children: renderValue(data?.[key], key) }, key))) }));
|
|
223
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SchemaDef } from '../../lib/types';
|
|
2
|
+
export type OpenViewParams = {
|
|
3
|
+
title?: string;
|
|
4
|
+
model?: string;
|
|
5
|
+
schema?: SchemaDef;
|
|
6
|
+
data: Record<string, any>;
|
|
7
|
+
};
|
|
8
|
+
export declare function useViewDrawer(): {
|
|
9
|
+
readonly open: (params: OpenViewParams) => void;
|
|
10
|
+
readonly close: () => void;
|
|
11
|
+
readonly ViewDrawerPortal: () => import("react/jsx-runtime").JSX.Element | null;
|
|
12
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Drawer, DrawerBody, DrawerContent, DrawerFooter, DrawerHeader, Button, } from '@heroui/react';
|
|
5
|
+
import { DynamicViewer } from './DynamicViewer';
|
|
6
|
+
export function useViewDrawer() {
|
|
7
|
+
const [state, setState] = React.useState(null);
|
|
8
|
+
const open = React.useCallback((params) => {
|
|
9
|
+
setState({ ...params, isOpen: true });
|
|
10
|
+
}, []);
|
|
11
|
+
const close = React.useCallback(() => {
|
|
12
|
+
setState((s) => (s ? { ...s, isOpen: false } : s));
|
|
13
|
+
// delay unmount slightly for animation
|
|
14
|
+
setTimeout(() => setState(null), 200);
|
|
15
|
+
}, []);
|
|
16
|
+
const ViewDrawerPortal = React.useCallback(() => {
|
|
17
|
+
if (!state)
|
|
18
|
+
return null;
|
|
19
|
+
const { isOpen, title, model, schema, data } = state;
|
|
20
|
+
const headerTitle = title || model || 'Details';
|
|
21
|
+
return (_jsx(Drawer, { isOpen: isOpen, onClose: close, placement: "right", size: "lg", children: _jsxs(DrawerContent, { className: "w-[100vw] md:max-w-[50vw]", children: [_jsx(DrawerHeader, { className: "border-b border-default-200", children: headerTitle }), _jsx(DrawerBody, { className: "px-4 py-3", children: _jsx(DynamicViewer, { schema: schema, data: data }) }), _jsx(DrawerFooter, { className: "border-t border-default-200", children: _jsx(Button, { variant: "flat", onPress: close, children: "Close" }) })] }) }));
|
|
22
|
+
}, [state, close]);
|
|
23
|
+
return { open, close, ViewDrawerPortal };
|
|
24
|
+
}
|