@object-ui/app-shell 4.5.0 → 4.7.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 +33 -0
- package/README.md +21 -0
- package/dist/console/AppContent.js +6 -39
- package/dist/console/ConsoleShell.js +45 -4
- package/dist/console/home/AppCard.d.ts +2 -1
- package/dist/console/home/AppCard.js +24 -5
- package/dist/console/home/HomePage.js +29 -7
- package/dist/console/home/QuickActions.js +13 -7
- package/dist/console/home/RecentApps.js +12 -5
- package/dist/console/home/StarredApps.js +12 -5
- package/dist/context/FavoritesProvider.d.ts +9 -12
- package/dist/context/FavoritesProvider.js +83 -37
- package/dist/context/RecentItemsProvider.d.ts +45 -0
- package/dist/context/RecentItemsProvider.js +139 -0
- package/dist/context/UserStateAdapters.d.ts +61 -0
- package/dist/context/UserStateAdapters.js +141 -0
- package/dist/context/index.d.ts +4 -0
- package/dist/context/index.js +2 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useRecentItems.d.ts +9 -17
- package/dist/hooks/useRecentItems.js +9 -46
- package/dist/hooks/useTrackRouteAsRecent.d.ts +18 -0
- package/dist/hooks/useTrackRouteAsRecent.js +91 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/layout/PageHeader.d.ts +29 -2
- package/dist/layout/PageHeader.js +20 -2
- package/dist/utils/resolveActionParams.d.ts +11 -0
- package/dist/utils/resolveActionParams.js +19 -0
- package/dist/views/ActionParamDialog.js +31 -2
- package/dist/views/ObjectView.js +63 -164
- package/dist/views/RecordDetailView.d.ts +20 -1
- package/dist/views/RecordDetailView.js +23 -9
- package/package.json +24 -24
|
@@ -1,50 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* useRecentItems
|
|
2
|
+
* useRecentItems — re-export shim
|
|
3
|
+
*
|
|
4
|
+
* The recent-items state has been migrated to a React Context so all consumers
|
|
5
|
+
* share a single state instance and can be backed by an optional backend
|
|
6
|
+
* persistence adapter. See `../context/RecentItemsProvider`.
|
|
7
|
+
*
|
|
8
|
+
* All existing imports of `useRecentItems` and `RecentItem` from this path
|
|
9
|
+
* continue to work without any changes at the call sites.
|
|
3
10
|
*
|
|
4
|
-
* Tracks recently visited items (objects, dashboards, pages) with
|
|
5
|
-
* localStorage persistence. Exposes helpers to add and retrieve items.
|
|
6
11
|
* @module
|
|
7
12
|
*/
|
|
8
|
-
|
|
9
|
-
const STORAGE_KEY = 'objectui-recent-items';
|
|
10
|
-
const MAX_RECENT = 8;
|
|
11
|
-
function loadRecent() {
|
|
12
|
-
try {
|
|
13
|
-
const raw = localStorage.getItem(STORAGE_KEY);
|
|
14
|
-
return raw ? JSON.parse(raw) : [];
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return [];
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
function saveRecent(items) {
|
|
21
|
-
try {
|
|
22
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
// Storage full — silently ignore
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
export function useRecentItems() {
|
|
29
|
-
const [recentItems, setRecentItems] = useState(loadRecent);
|
|
30
|
-
// Sync from storage on mount
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
setRecentItems(loadRecent());
|
|
33
|
-
}, []);
|
|
34
|
-
const addRecentItem = useCallback((item) => {
|
|
35
|
-
setRecentItems(prev => {
|
|
36
|
-
const filtered = prev.filter(r => r.id !== item.id);
|
|
37
|
-
const updated = [
|
|
38
|
-
{ ...item, visitedAt: new Date().toISOString() },
|
|
39
|
-
...filtered,
|
|
40
|
-
].slice(0, MAX_RECENT);
|
|
41
|
-
saveRecent(updated);
|
|
42
|
-
return updated;
|
|
43
|
-
});
|
|
44
|
-
}, []);
|
|
45
|
-
const clearRecentItems = useCallback(() => {
|
|
46
|
-
setRecentItems([]);
|
|
47
|
-
saveRecent([]);
|
|
48
|
-
}, []);
|
|
49
|
-
return { recentItems, addRecentItem, clearRecentItems };
|
|
50
|
-
}
|
|
13
|
+
export { useRecentItems } from '../context/RecentItemsProvider';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface ObjectLike {
|
|
2
|
+
name: string;
|
|
3
|
+
label?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface UseTrackRouteAsRecentOptions {
|
|
6
|
+
/** Active route path. Usually `useLocation().pathname`. */
|
|
7
|
+
pathname: string;
|
|
8
|
+
/** Currently selected app name. Used to build the `href` and namespace. */
|
|
9
|
+
appName: string | undefined;
|
|
10
|
+
/** Objects available in the current app — used to resolve labels. */
|
|
11
|
+
objects?: ObjectLike[];
|
|
12
|
+
/** Optional override; defaults to `/apps`. */
|
|
13
|
+
basePathSegment?: string;
|
|
14
|
+
/** When `true`, the effect is suspended (e.g. when shell is hydrating). */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function useTrackRouteAsRecent({ pathname, appName, objects, basePathSegment, disabled, }: UseTrackRouteAsRecentOptions): void;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTrackRouteAsRecent
|
|
3
|
+
*
|
|
4
|
+
* Watches `pathname` and records the current entity (object / dashboard /
|
|
5
|
+
* page / report) into `RecentItemsProvider`. Encapsulates the URL parsing
|
|
6
|
+
* logic that previously lived inline in `AppContent.tsx` so it can be reused
|
|
7
|
+
* by other shells and tested in isolation.
|
|
8
|
+
*
|
|
9
|
+
* The hook understands the standard console URL layout:
|
|
10
|
+
*
|
|
11
|
+
* /apps/:appName/:objectName
|
|
12
|
+
* /apps/:appName/dashboard/:id
|
|
13
|
+
* /apps/:appName/page/:id
|
|
14
|
+
* /apps/:appName/report/:id
|
|
15
|
+
*
|
|
16
|
+
* Pass `objects` (the list available in the current app) so we can resolve
|
|
17
|
+
* a human-readable label for object routes.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
import { useEffect, useRef } from 'react';
|
|
22
|
+
import { useRecentItems } from '../context/RecentItemsProvider';
|
|
23
|
+
/** Segments after `appName` that are NOT object names but route prefixes. */
|
|
24
|
+
const ROUTE_PREFIXES = new Set(['view', 'record', 'page', 'dashboard', 'design', 'report']);
|
|
25
|
+
function titleize(slug) {
|
|
26
|
+
return slug.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
27
|
+
}
|
|
28
|
+
export function useTrackRouteAsRecent({ pathname, appName, objects = [], basePathSegment = 'apps', disabled = false, }) {
|
|
29
|
+
const { addRecentItem } = useRecentItems();
|
|
30
|
+
// Hold `objects` in a ref so we don't re-fire the tracking effect every
|
|
31
|
+
// time the parent passes a new array reference (which would happen on
|
|
32
|
+
// every render in idiomatic React). Only the route should drive the effect.
|
|
33
|
+
const objectsRef = useRef(objects);
|
|
34
|
+
objectsRef.current = objects;
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (disabled || !appName)
|
|
37
|
+
return;
|
|
38
|
+
const parts = pathname.split('/').filter(Boolean);
|
|
39
|
+
// Expect: [basePathSegment, appName, ...rest]
|
|
40
|
+
if (parts[0] !== basePathSegment || parts[1] !== appName)
|
|
41
|
+
return;
|
|
42
|
+
const seg2 = parts[2];
|
|
43
|
+
const seg3 = parts[3];
|
|
44
|
+
const basePath = `/${basePathSegment}/${appName}`;
|
|
45
|
+
if (seg2 && !ROUTE_PREFIXES.has(seg2)) {
|
|
46
|
+
const obj = objectsRef.current.find(o => o.name === seg2);
|
|
47
|
+
if (obj) {
|
|
48
|
+
addRecentItem({
|
|
49
|
+
id: `object:${obj.name}`,
|
|
50
|
+
label: obj.label || obj.name,
|
|
51
|
+
href: `${basePath}/${obj.name}`,
|
|
52
|
+
type: 'object',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!seg3)
|
|
58
|
+
return;
|
|
59
|
+
switch (seg2) {
|
|
60
|
+
case 'dashboard':
|
|
61
|
+
addRecentItem({
|
|
62
|
+
id: `dashboard:${seg3}`,
|
|
63
|
+
label: titleize(seg3),
|
|
64
|
+
href: `${basePath}/dashboard/${seg3}`,
|
|
65
|
+
type: 'dashboard',
|
|
66
|
+
});
|
|
67
|
+
break;
|
|
68
|
+
case 'page':
|
|
69
|
+
addRecentItem({
|
|
70
|
+
id: `page:${seg3}`,
|
|
71
|
+
label: titleize(seg3),
|
|
72
|
+
href: `${basePath}/page/${seg3}`,
|
|
73
|
+
type: 'page',
|
|
74
|
+
});
|
|
75
|
+
break;
|
|
76
|
+
case 'report':
|
|
77
|
+
addRecentItem({
|
|
78
|
+
id: `report:${seg3}`,
|
|
79
|
+
label: titleize(seg3),
|
|
80
|
+
href: `${basePath}/report/${seg3}`,
|
|
81
|
+
type: 'report',
|
|
82
|
+
});
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
// Intentionally drives off route changes only. `addRecentItem` is stable
|
|
88
|
+
// per provider, and `objects` is read through a ref to avoid loops.
|
|
89
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
90
|
+
}, [pathname, appName, basePathSegment, disabled]);
|
|
91
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,8 @@ export { ObjectView, RecordDetailView, RecordFormPage, DashboardView, PageView,
|
|
|
26
26
|
export type { RecordFormPageProps } from './views';
|
|
27
27
|
export { useFavorites, useMetadataService, useNavPins, useNavigationSync, NavigationSyncEffect, addNavigationItem, removeNavigationItems, renameNavigationItems, navigationEqual, generateNavId, useResponsiveSidebar, } from './hooks';
|
|
28
28
|
export type { FavoriteItem } from './hooks';
|
|
29
|
-
export { NavigationProvider, useNavigationContext, FavoritesProvider } from './context';
|
|
29
|
+
export { NavigationProvider, useNavigationContext, FavoritesProvider, RecentItemsProvider, UserStateAdaptersProvider, useUserStateAdapter, useAttachUserStateAdapters, } from './context';
|
|
30
|
+
export type { UserDataAdapter, UserStateKind } from './context';
|
|
30
31
|
export { AppContent as DefaultAppContent } from './console/AppContent';
|
|
31
32
|
export { LoginPage as DefaultLoginPage } from './console/auth/LoginPage';
|
|
32
33
|
export { RegisterPage as DefaultRegisterPage } from './console/auth/RegisterPage';
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,7 @@ export { ObjectView, RecordDetailView, RecordFormPage, DashboardView, PageView,
|
|
|
29
29
|
// Hooks
|
|
30
30
|
export { useFavorites, useMetadataService, useNavPins, useNavigationSync, NavigationSyncEffect, addNavigationItem, removeNavigationItems, renameNavigationItems, navigationEqual, generateNavId, useResponsiveSidebar, } from './hooks';
|
|
31
31
|
// Context providers
|
|
32
|
-
export { NavigationProvider, useNavigationContext, FavoritesProvider } from './context';
|
|
32
|
+
export { NavigationProvider, useNavigationContext, FavoritesProvider, RecentItemsProvider, UserStateAdaptersProvider, useUserStateAdapter, useAttachUserStateAdapters, } from './context';
|
|
33
33
|
// Default page implementations (consumers can partial-override slots)
|
|
34
34
|
export { AppContent as DefaultAppContent } from './console/AppContent';
|
|
35
35
|
export { LoginPage as DefaultLoginPage } from './console/auth/LoginPage';
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* individual screens don't reinvent the title row.
|
|
14
14
|
*
|
|
15
15
|
* Layout:
|
|
16
|
+
* [accent hairline]
|
|
16
17
|
* [icon] Title [actions...]
|
|
17
18
|
* description
|
|
18
19
|
*/
|
|
@@ -22,10 +23,32 @@ export interface PageHeaderProps {
|
|
|
22
23
|
title: React.ReactNode;
|
|
23
24
|
/** Optional secondary description shown beneath the title. */
|
|
24
25
|
description?: React.ReactNode;
|
|
25
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Optional leading icon (already-instantiated React element).
|
|
28
|
+
* The icon is auto-sized via CSS — pass it with whatever default sizing,
|
|
29
|
+
* the chip will scale child `<svg>` elements to fit.
|
|
30
|
+
*/
|
|
26
31
|
icon?: React.ReactNode;
|
|
27
32
|
/** Right-aligned actions (buttons, dropdowns, etc.). */
|
|
28
33
|
actions?: React.ReactNode;
|
|
34
|
+
/**
|
|
35
|
+
* CSS color used to tint the top hairline, icon chip background, and
|
|
36
|
+
* icon stroke. Defaults to the Shadcn `--primary` token so the header
|
|
37
|
+
* automatically follows the active app's branding.
|
|
38
|
+
*
|
|
39
|
+
* Accepts any CSS color value (e.g. `'#8b5cf6'`, `'hsl(280 70% 55%)'`,
|
|
40
|
+
* `'var(--my-token)'`).
|
|
41
|
+
*/
|
|
42
|
+
accentColor?: string;
|
|
43
|
+
/**
|
|
44
|
+
* When true, the header is `position: sticky; top: 0` and gains a
|
|
45
|
+
* translucent backdrop blur so content scrolling underneath remains
|
|
46
|
+
* partly visible. Opt-in — the default layout used by ObjectView already
|
|
47
|
+
* pins the header by virtue of being the first child of a non-scrolling
|
|
48
|
+
* flex column, so flipping this on there would be a no-op (or worse,
|
|
49
|
+
* conflict with z-index of overlays).
|
|
50
|
+
*/
|
|
51
|
+
sticky?: boolean;
|
|
29
52
|
/** Additional className applied to the outer container. */
|
|
30
53
|
className?: string;
|
|
31
54
|
/** Optional data-testid for E2E selectors. */
|
|
@@ -37,6 +60,10 @@ export interface PageHeaderProps {
|
|
|
37
60
|
* Mirrors Shadcn / Linear / Notion conventions: subtle border, comfortable
|
|
38
61
|
* vertical padding, primary-tinted icon chip, monolithic action cluster on
|
|
39
62
|
* the right with consistent gap.
|
|
63
|
+
*
|
|
64
|
+
* The accent color cascades through a CSS custom property
|
|
65
|
+
* (`--page-header-accent`) so consumers can override it without rebuilding
|
|
66
|
+
* Tailwind classes.
|
|
40
67
|
*/
|
|
41
|
-
export declare function PageHeader({ title, description, icon, actions, className, 'data-testid': testId, }: PageHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
68
|
+
export declare function PageHeader({ title, description, icon, actions, accentColor, sticky, className, 'data-testid': testId, }: PageHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
42
69
|
export default PageHeader;
|
|
@@ -6,8 +6,26 @@ import { cn } from '@object-ui/components';
|
|
|
6
6
|
* Mirrors Shadcn / Linear / Notion conventions: subtle border, comfortable
|
|
7
7
|
* vertical padding, primary-tinted icon chip, monolithic action cluster on
|
|
8
8
|
* the right with consistent gap.
|
|
9
|
+
*
|
|
10
|
+
* The accent color cascades through a CSS custom property
|
|
11
|
+
* (`--page-header-accent`) so consumers can override it without rebuilding
|
|
12
|
+
* Tailwind classes.
|
|
9
13
|
*/
|
|
10
|
-
export function PageHeader({ title, description, icon, actions, className, 'data-testid': testId, }) {
|
|
11
|
-
|
|
14
|
+
export function PageHeader({ title, description, icon, actions, accentColor, sticky = false, className, 'data-testid': testId, }) {
|
|
15
|
+
// Resolve accent → CSS var. Falls back to the Shadcn primary token so the
|
|
16
|
+
// header follows whatever brand the active app has injected.
|
|
17
|
+
const accent = accentColor || 'hsl(var(--primary))';
|
|
18
|
+
const style = { '--page-header-accent': accent };
|
|
19
|
+
return (_jsxs("div", { style: style, className: cn('relative shrink-0 border-b bg-background z-10', sticky &&
|
|
20
|
+
'sticky top-0 bg-background/85 backdrop-blur supports-[backdrop-filter]:bg-background/75', className), "data-testid": testId ?? 'page-header', children: [_jsx("div", { "aria-hidden": true, className: "pointer-events-none absolute inset-x-0 top-0 h-[2px] opacity-80", style: {
|
|
21
|
+
background: 'linear-gradient(90deg, transparent, var(--page-header-accent), transparent)',
|
|
22
|
+
} }), _jsxs("div", { className: "flex justify-between items-center gap-3 py-2.5 sm:py-3 px-4 sm:px-6", children: [_jsxs("div", { className: "flex items-center gap-3 min-w-0 flex-1", children: [icon && (_jsx("div", { className: cn('shrink-0 inline-flex items-center justify-center rounded-lg', 'h-9 w-9 sm:h-10 sm:w-10',
|
|
23
|
+
// Auto-size any nested SVG so callers don't have to think
|
|
24
|
+
// about sizing inside the chip.
|
|
25
|
+
'[&_svg]:h-[18px] [&_svg]:w-[18px] sm:[&_svg]:h-5 sm:[&_svg]:w-5'), style: {
|
|
26
|
+
color: 'var(--page-header-accent)',
|
|
27
|
+
background: 'linear-gradient(135deg, color-mix(in srgb, var(--page-header-accent) 18%, transparent), color-mix(in srgb, var(--page-header-accent) 6%, transparent))',
|
|
28
|
+
boxShadow: 'inset 0 0 0 1px color-mix(in srgb, var(--page-header-accent) 22%, transparent)',
|
|
29
|
+
}, children: icon })), _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-base sm:text-lg lg:text-xl font-semibold tracking-tight text-foreground truncate leading-tight", children: title }), description && (_jsx("p", { className: "text-xs sm:text-sm text-muted-foreground truncate hidden sm:block max-w-xl mt-0.5", children: description }))] })] }), actions && (_jsx("div", { className: "flex items-center gap-1.5 sm:gap-2 shrink-0", children: actions }))] })] }));
|
|
12
30
|
}
|
|
13
31
|
export default PageHeader;
|
|
@@ -54,6 +54,17 @@ interface RuntimeField {
|
|
|
54
54
|
} | string>;
|
|
55
55
|
multiple?: boolean;
|
|
56
56
|
defaultValue?: unknown;
|
|
57
|
+
reference_to?: string;
|
|
58
|
+
reference?: string;
|
|
59
|
+
display_field?: string;
|
|
60
|
+
reference_field?: string;
|
|
61
|
+
id_field?: string;
|
|
62
|
+
description_field?: string;
|
|
63
|
+
title_format?: string;
|
|
64
|
+
lookup_columns?: unknown[];
|
|
65
|
+
lookup_filters?: unknown[];
|
|
66
|
+
lookup_page_size?: number;
|
|
67
|
+
depends_on?: unknown[];
|
|
57
68
|
}
|
|
58
69
|
interface RuntimeObject {
|
|
59
70
|
name?: string;
|
|
@@ -58,6 +58,24 @@ export function resolveActionParam(param, ctx) {
|
|
|
58
58
|
?? normaliseOptions(field.options, ownerName, param.field, ctx.fieldOptionLabel);
|
|
59
59
|
const resolvedLabel = param.label
|
|
60
60
|
?? ctx.fieldLabel(ownerName, param.field, field.label ?? param.field);
|
|
61
|
+
/** Lookup/reference params carry extra picker config that the dialog
|
|
62
|
+
* forwards to `<LookupField>`. Without these the picker would fall back
|
|
63
|
+
* to a plain text input. */
|
|
64
|
+
const isLookupResolvedType = resolvedType === 'lookup' || resolvedType === 'reference';
|
|
65
|
+
const lookupExtras = isLookupResolvedType
|
|
66
|
+
? {
|
|
67
|
+
referenceTo: field.reference_to ?? field.reference,
|
|
68
|
+
displayField: field.display_field ?? field.reference_field,
|
|
69
|
+
idField: field.id_field,
|
|
70
|
+
descriptionField: field.description_field,
|
|
71
|
+
multiple: field.multiple,
|
|
72
|
+
titleFormat: field.title_format,
|
|
73
|
+
lookupColumns: field.lookup_columns,
|
|
74
|
+
lookupFilters: field.lookup_filters,
|
|
75
|
+
lookupPageSize: field.lookup_page_size,
|
|
76
|
+
dependsOn: field.depends_on,
|
|
77
|
+
}
|
|
78
|
+
: {};
|
|
61
79
|
return {
|
|
62
80
|
name: param.name ?? param.field,
|
|
63
81
|
label: resolvedLabel,
|
|
@@ -67,6 +85,7 @@ export function resolveActionParam(param, ctx) {
|
|
|
67
85
|
placeholder: param.placeholder ?? field.placeholder,
|
|
68
86
|
helpText: param.helpText ?? field.help ?? field.description,
|
|
69
87
|
defaultValue: rowDefault ?? param.defaultValue ?? field.defaultValue,
|
|
88
|
+
...lookupExtras,
|
|
70
89
|
};
|
|
71
90
|
}
|
|
72
91
|
/** Resolve an array of raw action params. */
|
|
@@ -11,8 +11,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
* Returns collected param values or null on cancel.
|
|
12
12
|
*/
|
|
13
13
|
import { useState, useEffect } from 'react';
|
|
14
|
-
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Button, Input, Label, Textarea, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
|
|
14
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Button, Input, Label, Textarea, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Checkbox, } from '@object-ui/components';
|
|
15
15
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
16
|
+
import { LookupField } from '@object-ui/fields';
|
|
16
17
|
export function ActionParamDialog({ state, onOpenChange }) {
|
|
17
18
|
const { t } = useObjectTranslation();
|
|
18
19
|
const [values, setValues] = useState({});
|
|
@@ -37,6 +38,9 @@ export function ActionParamDialog({ state, onOpenChange }) {
|
|
|
37
38
|
return value.trim() === '';
|
|
38
39
|
if (Array.isArray(value))
|
|
39
40
|
return value.length === 0;
|
|
41
|
+
// Boolean false is a VALID value — only treat undefined/null as missing.
|
|
42
|
+
if (typeof value === 'boolean')
|
|
43
|
+
return false;
|
|
40
44
|
return false;
|
|
41
45
|
};
|
|
42
46
|
const handleSubmit = () => {
|
|
@@ -65,5 +69,30 @@ export function ActionParamDialog({ state, onOpenChange }) {
|
|
|
65
69
|
return (_jsx(Dialog, { open: state.open, onOpenChange: (open) => {
|
|
66
70
|
if (!open)
|
|
67
71
|
handleCancel();
|
|
68
|
-
}, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('actionDialog.title') }), _jsx(DialogDescription, { children: t('actionDialog.description') })] }), _jsx("div", { className: "grid gap-4 py-4", children: state.params.map((param) =>
|
|
72
|
+
}, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('actionDialog.title') }), _jsx(DialogDescription, { children: t('actionDialog.description') })] }), _jsx("div", { className: "grid gap-4 py-4", children: state.params.map((param) => {
|
|
73
|
+
const isBooleanParam = param.type === 'boolean' || param.type === 'checkbox';
|
|
74
|
+
// Boolean → render as inline checkbox row (label sits beside the
|
|
75
|
+
// control instead of above it; help text appears underneath).
|
|
76
|
+
if (isBooleanParam) {
|
|
77
|
+
const checked = values[param.name] === true;
|
|
78
|
+
return (_jsxs("div", { className: "grid gap-1", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx(Checkbox, { id: param.name, checked: checked, onCheckedChange: (c) => updateValue(param.name, c === true), className: "mt-0.5" }), _jsxs(Label, { htmlFor: param.name, className: "font-normal cursor-pointer", children: [param.label, param.required && _jsx("span", { className: "text-destructive ml-1", children: "*" })] })] }), errors[param.name] && (_jsx("p", { className: "text-xs text-destructive ml-6", children: t('actionDialog.requiredError', { label: param.label }) })), param.helpText && (_jsx("p", { className: "text-xs text-muted-foreground ml-6", children: param.helpText }))] }, param.name));
|
|
79
|
+
}
|
|
80
|
+
const isLookupParam = param.type === 'lookup' || param.type === 'reference';
|
|
81
|
+
return (_jsxs("div", { className: "grid gap-2", children: [_jsxs(Label, { htmlFor: param.name, children: [param.label, param.required && _jsx("span", { className: "text-destructive ml-1", children: "*" })] }), param.type === 'select' && param.options ? (_jsxs(Select, { value: values[param.name] ?? '', onValueChange: (val) => updateValue(param.name, val), children: [_jsx(SelectTrigger, { id: param.name, className: errors[param.name] ? 'border-destructive' : '', children: _jsx(SelectValue, { placeholder: param.placeholder || t('actionDialog.selectPlaceholder', { label: param.label }) }) }), _jsx(SelectContent, { children: param.options.map((opt) => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] })) : isLookupParam && param.referenceTo ? (_jsx(LookupField, { value: values[param.name] ?? null, onChange: (v) => updateValue(param.name, v), field: {
|
|
82
|
+
name: param.name,
|
|
83
|
+
type: 'lookup',
|
|
84
|
+
reference_to: param.referenceTo,
|
|
85
|
+
display_field: param.displayField,
|
|
86
|
+
id_field: param.idField,
|
|
87
|
+
description_field: param.descriptionField,
|
|
88
|
+
multiple: param.multiple,
|
|
89
|
+
title_format: param.titleFormat,
|
|
90
|
+
lookup_columns: param.lookupColumns,
|
|
91
|
+
lookup_filters: param.lookupFilters,
|
|
92
|
+
lookup_page_size: param.lookupPageSize,
|
|
93
|
+
depends_on: param.dependsOn,
|
|
94
|
+
placeholder: param.placeholder,
|
|
95
|
+
} })) : param.type === 'textarea' ? (_jsx(Textarea, { id: param.name, value: values[param.name] ?? '', onChange: (e) => updateValue(param.name, e.target.value), placeholder: param.placeholder, className: errors[param.name] ? 'border-destructive' : '' })) : param.type === 'number' ? (_jsx(Input, { id: param.name, type: "number", value: values[param.name] ?? '', onChange: (e) => updateValue(param.name, e.target.value === '' ? undefined : e.target.valueAsNumber), placeholder: param.placeholder, className: errors[param.name] ? 'border-destructive' : '' })) : (_jsx(Input, { id: param.name, type: ['email', 'url', 'date', 'datetime-local', 'time', 'password'].includes(param.type) ? param.type : 'text', value: values[param.name] ?? '', onChange: (e) => updateValue(param.name, e.target.value), placeholder: param.placeholder ||
|
|
96
|
+
(isLookupParam ? t('actionDialog.lookupPlaceholder', { label: param.label }) : undefined), className: errors[param.name] ? 'border-destructive' : '' })), errors[param.name] && (_jsx("p", { className: "text-xs text-destructive", children: t('actionDialog.requiredError', { label: param.label }) })), param.helpText && (_jsx("p", { className: "text-xs text-muted-foreground", children: param.helpText })), isLookupParam && !param.helpText && (_jsx("p", { className: "text-xs text-muted-foreground", children: t('actionDialog.lookupHelpText') }))] }, param.name));
|
|
97
|
+
}) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: handleCancel, children: t('actionDialog.cancel') }), _jsx(Button, { onClick: handleSubmit, children: t('actionDialog.confirm') })] })] }) }));
|
|
69
98
|
}
|