@airoom/nextmin-react 2.0.2 → 2.0.3

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.
Files changed (54) hide show
  1. package/README.md +57 -0
  2. package/dist/components/AddressAutocomplete.d.ts +4 -1
  3. package/dist/components/AddressAutocomplete.js +2 -2
  4. package/dist/components/AdminApp.js +86 -1
  5. package/dist/components/FileUploader.d.ts +3 -0
  6. package/dist/components/FileUploader.js +84 -28
  7. package/dist/components/PasswordInput.d.ts +2 -0
  8. package/dist/components/PasswordInput.js +2 -2
  9. package/dist/components/PhoneInput.d.ts +2 -0
  10. package/dist/components/PhoneInput.js +2 -2
  11. package/dist/components/QRCodeModal.d.ts +10 -0
  12. package/dist/components/QRCodeModal.js +153 -0
  13. package/dist/components/RefSelect.d.ts +4 -1
  14. package/dist/components/RefSelect.js +163 -65
  15. package/dist/components/SchemaForm.d.ts +2 -1
  16. package/dist/components/SchemaForm.js +132 -55
  17. package/dist/components/Sidebar.js +96 -7
  18. package/dist/components/TableFilters.js +1 -1
  19. package/dist/components/editor/TiptapEditor.js +43 -12
  20. package/dist/components/editor/Toolbar.js +195 -70
  21. package/dist/components/editor/components/CustomFontSizeModal.d.ts +13 -0
  22. package/dist/components/editor/components/CustomFontSizeModal.js +81 -0
  23. package/dist/components/editor/components/DistrictGridModal.js +1 -1
  24. package/dist/components/editor/components/SchemaInsertionModal.js +1 -1
  25. package/dist/components/editor/editor.css +2 -0
  26. package/dist/components/editor/extensions/FontSize.d.ts +19 -0
  27. package/dist/components/editor/extensions/FontSize.js +41 -0
  28. package/dist/components/viewer/DynamicViewer.js +50 -15
  29. package/dist/components/viewer/useViewDrawer.js +42 -2
  30. package/dist/hooks/useAppNavigation.d.ts +3 -0
  31. package/dist/hooks/useAppNavigation.js +37 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +1 -0
  34. package/dist/lib/QueryBuilder.d.ts +3 -1
  35. package/dist/lib/QueryBuilder.js +10 -0
  36. package/dist/lib/api.d.ts +6 -0
  37. package/dist/lib/api.js +45 -24
  38. package/dist/lib/schemaUtils.js +1 -1
  39. package/dist/lib/types.d.ts +6 -1
  40. package/dist/nextmin.css +1 -1
  41. package/dist/providers/NextMinProvider.d.ts +5 -1
  42. package/dist/providers/NextMinProvider.js +12 -2
  43. package/dist/providers/themeResolver.d.ts +25 -0
  44. package/dist/providers/themeResolver.js +174 -0
  45. package/dist/router/NextMinRouter.d.ts +0 -1
  46. package/dist/router/NextMinRouter.js +0 -1
  47. package/dist/views/CreateEditPage.js +20 -11
  48. package/dist/views/DashboardPage.js +1 -1
  49. package/dist/views/ListPage.js +37 -6
  50. package/dist/views/SettingsEdit.js +1 -1
  51. package/dist/views/list/DataTableHero.d.ts +5 -5
  52. package/dist/views/list/DataTableHero.js +154 -41
  53. package/dist/views/list/ListHeader.js +9 -1
  54. package/package.json +4 -2
@@ -0,0 +1,41 @@
1
+ import { Extension } from '@tiptap/core';
2
+ export const FontSize = Extension.create({
3
+ name: 'fontSize',
4
+ addOptions() {
5
+ return {
6
+ types: ['textStyle'],
7
+ };
8
+ },
9
+ addGlobalAttributes() {
10
+ return [
11
+ {
12
+ types: this.options.types,
13
+ attributes: {
14
+ fontSize: {
15
+ default: null,
16
+ parseHTML: element => element.style.fontSize?.replace(/['"]/g, '') || null,
17
+ renderHTML: attributes => {
18
+ if (!attributes.fontSize) {
19
+ return {};
20
+ }
21
+ return {
22
+ style: `font-size: ${attributes.fontSize}`,
23
+ };
24
+ },
25
+ },
26
+ },
27
+ },
28
+ ];
29
+ },
30
+ addCommands() {
31
+ return {
32
+ setFontSize: (fontSize) => ({ chain }) => {
33
+ const formatted = typeof fontSize === 'number' || /^\d+$/.test(fontSize) ? `${fontSize}px` : fontSize;
34
+ return chain().setMark('textStyle', { fontSize: formatted }).run();
35
+ },
36
+ unsetFontSize: () => ({ chain }) => {
37
+ return chain().setMark('textStyle', { fontSize: null }).removeEmptyTextStyle().run();
38
+ },
39
+ };
40
+ },
41
+ });
@@ -52,25 +52,46 @@ function isLikelyImageUrl(s) {
52
52
  return IMG_EXT_RE.test(path.split('.').pop() || '');
53
53
  }
54
54
  }
55
- function extractUrl(v) {
55
+ function isImageKeyOrAttr(key, attr) {
56
+ if (key) {
57
+ const k = key.toLowerCase();
58
+ if (['image', 'img', 'avatar', 'photo', 'thumbnail', 'logo', 'cover', 'banner', 'pic', 'picture', 'icon'].some((term) => k.includes(term))) {
59
+ return true;
60
+ }
61
+ }
62
+ const a = Array.isArray(attr) ? attr[0] : attr;
63
+ const t = String(a?.type ?? '').toLowerCase();
64
+ const f = String(a?.format ?? '').toLowerCase();
65
+ return t === 'image' || f === 'image' || t === 'media' || f === 'media';
66
+ }
67
+ function extractUrl(v, key, attr) {
56
68
  if (!v)
57
69
  return null;
58
- if (typeof v === 'string' && isLikelyImageUrl(v))
59
- return v;
70
+ if (typeof v === 'string') {
71
+ if (isLikelyImageUrl(v))
72
+ return v;
73
+ if (isImageKeyOrAttr(key, attr) && isUrlLike(v))
74
+ return v;
75
+ }
60
76
  if (typeof v === 'object') {
61
77
  const u = (v.url || v.src || v.value);
62
- return u && isLikelyImageUrl(u) ? u : null;
78
+ if (u) {
79
+ if (isLikelyImageUrl(u))
80
+ return u;
81
+ if (isImageKeyOrAttr(key, attr) && isUrlLike(u))
82
+ return u;
83
+ }
63
84
  }
64
85
  return null;
65
86
  }
66
- function extractAllImageUrls(val) {
87
+ function extractAllImageUrls(val, key, attr) {
67
88
  if (Array.isArray(val)) {
68
- const list = val.map(extractUrl).filter(Boolean);
89
+ const list = val.map((item) => extractUrl(item, key, attr)).filter(Boolean);
69
90
  if (list.length)
70
91
  return list;
71
- return (val.filter((s) => typeof s === 'string' && isLikelyImageUrl(s)) || []);
92
+ return (val.filter((s) => typeof s === 'string' && (isLikelyImageUrl(s) || (isImageKeyOrAttr(key, attr) && isUrlLike(s)))) || []);
72
93
  }
73
- const one = extractUrl(val);
94
+ const one = extractUrl(val, key, attr);
74
95
  return one ? [one] : [];
75
96
  }
76
97
  function humanizeLabel(key) {
@@ -275,7 +296,7 @@ function renderValue(input, key, attr) {
275
296
  if (localizedTime)
276
297
  return _jsx("span", { children: localizedTime });
277
298
  // Detect images
278
- const urls = extractAllImageUrls(val);
299
+ const urls = extractAllImageUrls(val, key, attr);
279
300
  if (urls.length)
280
301
  return _jsx(ImageThumbs, { urls: urls });
281
302
  // Primitive or simple object/array formatting via shared formatter
@@ -319,17 +340,31 @@ function ObjectView({ obj, attributes, showKey }) {
319
340
  export function DynamicViewer({ schema, data }) {
320
341
  // Use schema order if available, otherwise object keys order
321
342
  const attributes = (schema?.attributes ?? {});
343
+ // Detect if this is a User-extending schema with no linked user
344
+ const isUserExt = schema?.extends?.toLowerCase() === 'users';
345
+ const hasLinkedUser = !!(data?.exId || data?.baseId);
322
346
  const keys = React.useMemo(() => {
323
- const base = Object.keys(attributes).filter((k) => {
347
+ const attrKeys = Object.keys(attributes).filter((k) => {
324
348
  if (HIDDEN_KEYS.has(k) || isPasswordKeyRaw(k))
325
349
  return false;
326
350
  if (isPasswordByAttr(attributes[k]))
327
351
  return false;
328
- return !isPrivate(attributes[k]);
352
+ if (isPrivate(attributes[k]))
353
+ return false;
354
+ // Hide inherited fields when no base user is linked
355
+ if (isUserExt && !hasLinkedUser) {
356
+ const head = Array.isArray(attributes[k]) ? attributes[k][0] : attributes[k];
357
+ if (head?.inherited)
358
+ return false;
359
+ }
360
+ return true;
361
+ });
362
+ const extraDataKeys = Object.keys(data || {}).filter((k) => {
363
+ if (HIDDEN_KEYS.has(k) || isPasswordKeyRaw(k))
364
+ return false;
365
+ return !attrKeys.includes(k);
329
366
  });
330
- if (base.length)
331
- return base;
332
- return Object.keys(data || {}).filter((k) => !HIDDEN_KEYS.has(k) && !isPasswordKeyRaw(k));
333
- }, [attributes, data]);
367
+ return [...attrKeys, ...extraDataKeys];
368
+ }, [attributes, data, isUserExt, hasLinkedUser]);
334
369
  return (_jsx("div", { className: "grid gap-1", children: keys.map((key) => (_jsx(FieldRow, { label: humanizeLabel(key), children: renderValue(data?.[key], key, attributes[key]) }, key))) }));
335
370
  }
@@ -1,8 +1,48 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import React from 'react';
4
- import { Drawer, DrawerBody, DrawerContent, DrawerFooter, DrawerHeader, Button, } from '@heroui/react';
4
+ import { Drawer, DrawerBody, DrawerContent, DrawerFooter, DrawerHeader, Button, Spinner, } from '@heroui/react';
5
+ import { api } from '../../lib/api';
5
6
  import { DynamicViewer } from './DynamicViewer';
7
+ function ViewDrawerContent({ model, schema, data: initialData, }) {
8
+ const [data, setData] = React.useState(initialData);
9
+ const [loading, setLoading] = React.useState(false);
10
+ React.useEffect(() => {
11
+ setData(initialData);
12
+ const id = initialData?.id || initialData?._id;
13
+ if (!model || !id)
14
+ return;
15
+ let cancelled = false;
16
+ setLoading(true);
17
+ api
18
+ .get(model, String(id))
19
+ .then((res) => {
20
+ if (cancelled)
21
+ return;
22
+ const fetched = res?.data || res;
23
+ if (fetched && typeof fetched === 'object') {
24
+ let merged = { ...initialData, ...fetched };
25
+ const baseObj = fetched?.baseId;
26
+ if (baseObj && typeof baseObj === 'object') {
27
+ merged = { ...baseObj, ...merged };
28
+ delete merged.baseId;
29
+ }
30
+ setData(merged);
31
+ }
32
+ })
33
+ .catch((err) => {
34
+ console.error('Failed to fetch full record for view drawer:', err);
35
+ })
36
+ .finally(() => {
37
+ if (!cancelled)
38
+ setLoading(false);
39
+ });
40
+ return () => {
41
+ cancelled = true;
42
+ };
43
+ }, [model, initialData?.id, initialData?._id]);
44
+ return (_jsxs("div", { className: "relative", children: [loading && (_jsxs("div", { className: "mb-3 flex items-center gap-2 text-xs text-primary font-medium", children: [_jsx(Spinner, { size: "sm" }), " Loading full details..."] })), _jsx(DynamicViewer, { schema: schema, data: data })] }));
45
+ }
6
46
  export function useViewDrawer() {
7
47
  const [state, setState] = React.useState(null);
8
48
  const open = React.useCallback((params) => {
@@ -18,7 +58,7 @@ export function useViewDrawer() {
18
58
  return null;
19
59
  const { isOpen, title, model, schema, data } = state;
20
60
  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" }) })] }) }));
61
+ 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 overflow-y-auto", children: _jsx(ViewDrawerContent, { model: model, schema: schema, data: data }) }), _jsx(DrawerFooter, { className: "border-t border-default-200", children: _jsx(Button, { variant: "flat", onPress: close, children: "Close" }) })] }) }));
22
62
  }, [state, close]);
23
63
  return { open, close, ViewDrawerPortal };
24
64
  }
@@ -0,0 +1,3 @@
1
+ export declare function useAppNavigation(): (url: string, opts?: {
2
+ replace?: boolean;
3
+ }) => void;
@@ -0,0 +1,37 @@
1
+ 'use client';
2
+ import { useCallback } from 'react';
3
+ import { useRouter } from 'next/navigation';
4
+ import { useNextMin } from '../providers/NextMinProvider';
5
+ export function useAppNavigation() {
6
+ const { navigate } = useNextMin();
7
+ const router = useRouter();
8
+ return useCallback((url, opts) => {
9
+ if (navigate) {
10
+ navigate(url);
11
+ }
12
+ else if (router) {
13
+ if (opts?.replace && typeof router.replace === 'function') {
14
+ router.replace(url);
15
+ }
16
+ else if (typeof router.push === 'function') {
17
+ router.push(url);
18
+ }
19
+ else if (typeof window !== 'undefined') {
20
+ if (opts?.replace) {
21
+ window.location.replace(url);
22
+ }
23
+ else {
24
+ window.location.href = url;
25
+ }
26
+ }
27
+ }
28
+ else if (typeof window !== 'undefined') {
29
+ if (opts?.replace) {
30
+ window.location.replace(url);
31
+ }
32
+ else {
33
+ window.location.href = url;
34
+ }
35
+ }
36
+ }, [navigate, router]);
37
+ }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,9 @@ export * from './providers/NextMinProvider';
2
2
  export * from './components/AdminApp';
3
3
  export * from './components/ArchitectureDemo';
4
4
  export * from './hooks/useRealtime';
5
+ export * from './hooks/useAppNavigation';
5
6
  export * from './lib/QueryBuilder';
6
7
  export * from './lib/AuthClient';
7
8
  export * from './lib/RealtimeClient';
8
9
  export * from './lib/api';
10
+ export type { ThemeConfig } from './providers/themeResolver';
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export * from './providers/NextMinProvider';
2
2
  export * from './components/AdminApp';
3
3
  export * from './components/ArchitectureDemo';
4
4
  export * from './hooks/useRealtime';
5
+ export * from './hooks/useAppNavigation';
5
6
  export * from './lib/QueryBuilder';
6
7
  export * from './lib/AuthClient';
7
8
  export * from './lib/RealtimeClient';
@@ -10,7 +10,8 @@ export interface QueryOptions {
10
10
  sortType?: string;
11
11
  page?: number;
12
12
  limit?: number;
13
- fields?: string[];
13
+ fields?: string | string[];
14
+ select?: string | string[];
14
15
  q?: string;
15
16
  searchKeys?: string;
16
17
  }
@@ -21,6 +22,7 @@ export declare class QueryBuilder {
21
22
  private options;
22
23
  where(field: string, operator: FilterOperator, value: any): QueryBuilder;
23
24
  orWhere(field: string, operator: FilterOperator, value: any): QueryBuilder;
25
+ select(fields: string | string[]): QueryBuilder;
24
26
  sort(field: string, direction?: 'asc' | 'desc'): QueryBuilder;
25
27
  page(page: number, limit?: number): QueryBuilder;
26
28
  limit(limit: number): QueryBuilder;
@@ -33,6 +33,10 @@ export class QueryBuilder {
33
33
  this.options.where = { $or: [currentWhere, newCondition] };
34
34
  return this;
35
35
  }
36
+ select(fields) {
37
+ this.options.select = fields;
38
+ return this;
39
+ }
36
40
  sort(field, direction = 'asc') {
37
41
  this.options.sort = field;
38
42
  this.options.sortType = direction;
@@ -69,6 +73,12 @@ export class QueryBuilder {
69
73
  params.q = this.options.q;
70
74
  if (this.options.searchKeys)
71
75
  params.searchKeys = this.options.searchKeys;
76
+ const sel = this.options.select ?? this.options.fields;
77
+ if (sel) {
78
+ const strSel = Array.isArray(sel) ? sel.join(',') : String(sel);
79
+ if (strSel.trim())
80
+ params.select = strSel.trim();
81
+ }
72
82
  return params;
73
83
  }
74
84
  }
package/dist/lib/api.d.ts CHANGED
@@ -9,6 +9,7 @@ export declare class ApiError extends Error {
9
9
  export declare class NextMinClient {
10
10
  private baseURL;
11
11
  private apiKey;
12
+ private static inFlightRequests;
12
13
  constructor(options?: {
13
14
  baseURL?: string;
14
15
  apiKey?: string;
@@ -30,6 +31,11 @@ export type ListParams = {
30
31
  dateFrom?: string;
31
32
  dateTo?: string;
32
33
  dateKey?: string;
34
+ sort?: string;
35
+ sortType?: string;
36
+ where?: Record<string, any>;
37
+ select?: string | string[];
38
+ fields?: string | string[];
33
39
  };
34
40
  export declare const api: {
35
41
  getSchemas: () => Promise<unknown>;
package/dist/lib/api.js CHANGED
@@ -25,39 +25,56 @@ export class NextMinClient {
25
25
  headers.Authorization = `Bearer ${bearer}`;
26
26
  const queryString = query ? qs(query) : '';
27
27
  const url = `${this.baseURL || normalizeBase(AuthClient.getBaseURL())}${path}${queryString}`;
28
- let res;
29
- try {
30
- res = await fetch(url, {
31
- method,
32
- headers,
33
- body: body && json && !isForm ? JSON.stringify(body) : body,
34
- });
35
- }
36
- catch (networkErr) {
37
- const err = new ApiError(networkErr?.message || 'Network error', 0);
38
- err.request = { url, method, headers };
39
- throw err;
40
- }
41
- const raw = await res.text();
42
- let payload = {};
43
- try {
44
- payload = raw ? JSON.parse(raw) : {};
45
- }
46
- catch {
47
- payload = raw || {};
28
+ if (method === 'GET') {
29
+ const pending = NextMinClient.inFlightRequests.get(url);
30
+ if (pending) {
31
+ return pending;
32
+ }
48
33
  }
49
- if (!res.ok) {
50
- throw payload;
34
+ const execRequest = (async () => {
35
+ let res;
36
+ try {
37
+ res = await fetch(url, {
38
+ method,
39
+ headers,
40
+ body: body && json && !isForm ? JSON.stringify(body) : body,
41
+ });
42
+ }
43
+ catch (networkErr) {
44
+ const err = new ApiError(networkErr?.message || 'Network error', 0);
45
+ err.request = { url, method, headers };
46
+ throw err;
47
+ }
48
+ const raw = await res.text();
49
+ let payload = {};
50
+ try {
51
+ payload = raw ? JSON.parse(raw) : {};
52
+ }
53
+ catch {
54
+ payload = raw || {};
55
+ }
56
+ if (!res.ok) {
57
+ const msg = payload?.message || payload?.error || res.statusText || 'API error';
58
+ throw new ApiError(typeof msg === 'string' ? msg : 'API error', res.status, payload);
59
+ }
60
+ return payload;
61
+ })();
62
+ if (method === 'GET') {
63
+ NextMinClient.inFlightRequests.set(url, execRequest);
64
+ execRequest.finally(() => {
65
+ NextMinClient.inFlightRequests.delete(url);
66
+ });
51
67
  }
52
- return payload;
68
+ return execRequest;
53
69
  }
54
70
  }
71
+ NextMinClient.inFlightRequests = new Map();
55
72
  // Keep the global request for backward compatibility, but make it use AuthClient config
56
73
  async function request(path, opts = {}) {
57
74
  return new NextMinClient().request(path, opts);
58
75
  }
59
76
  function modelPath(modelName) {
60
- return modelName.toLowerCase() === 'user' ? '/auth/user' : `/${modelName}`;
77
+ return `/${modelName}`;
61
78
  }
62
79
  function qs(params) {
63
80
  const sp = new URLSearchParams();
@@ -95,6 +112,10 @@ export const api = {
95
112
  builder.page(Math.max(1, options.page), options.limit);
96
113
  if (options.q)
97
114
  builder.search(options.q, options.searchKeys?.split(','));
115
+ if (options.select)
116
+ builder.select(options.select);
117
+ else if (options.fields)
118
+ builder.select(options.fields);
98
119
  const params = builder.build();
99
120
  return request(`${modelPath(modelName)}${qs(params)}`);
100
121
  },
@@ -12,7 +12,7 @@ export function inputTypeFor(attr) {
12
12
  return 'number';
13
13
  if (a.type === 'boolean')
14
14
  return 'checkbox';
15
- if (a.longtext)
15
+ if (a.longtext || a.format === 'textarea')
16
16
  return 'textarea';
17
17
  return 'text';
18
18
  }
@@ -5,6 +5,7 @@ export type AttributeBase = {
5
5
  private?: boolean;
6
6
  ref?: string;
7
7
  longtext?: boolean;
8
+ format?: string;
8
9
  show?: string;
9
10
  populateSlugFrom?: string;
10
11
  minLength?: number;
@@ -17,7 +18,11 @@ export type ArrayAttribute = AttributeBase[];
17
18
  export type Attribute = AttributeBase | ArrayAttribute;
18
19
  export type CustomAction = {
19
20
  label: string;
20
- href: string;
21
+ url?: string;
22
+ href?: string;
23
+ type?: string;
24
+ actionType?: string;
25
+ urlTemplate?: string;
21
26
  icon?: string;
22
27
  variant?: 'light' | 'flat' | 'solid' | 'bordered';
23
28
  color?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger';