@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +21 -0
  3. package/dist/console/AppContent.js +6 -39
  4. package/dist/console/ConsoleShell.js +45 -4
  5. package/dist/console/home/AppCard.d.ts +2 -1
  6. package/dist/console/home/AppCard.js +24 -5
  7. package/dist/console/home/HomePage.js +29 -7
  8. package/dist/console/home/QuickActions.js +13 -7
  9. package/dist/console/home/RecentApps.js +12 -5
  10. package/dist/console/home/StarredApps.js +12 -5
  11. package/dist/context/FavoritesProvider.d.ts +9 -12
  12. package/dist/context/FavoritesProvider.js +83 -37
  13. package/dist/context/RecentItemsProvider.d.ts +45 -0
  14. package/dist/context/RecentItemsProvider.js +139 -0
  15. package/dist/context/UserStateAdapters.d.ts +61 -0
  16. package/dist/context/UserStateAdapters.js +141 -0
  17. package/dist/context/index.d.ts +4 -0
  18. package/dist/context/index.js +2 -0
  19. package/dist/hooks/index.d.ts +1 -0
  20. package/dist/hooks/index.js +1 -0
  21. package/dist/hooks/useRecentItems.d.ts +9 -17
  22. package/dist/hooks/useRecentItems.js +9 -46
  23. package/dist/hooks/useTrackRouteAsRecent.d.ts +18 -0
  24. package/dist/hooks/useTrackRouteAsRecent.js +91 -0
  25. package/dist/index.d.ts +2 -1
  26. package/dist/index.js +1 -1
  27. package/dist/layout/PageHeader.d.ts +29 -2
  28. package/dist/layout/PageHeader.js +20 -2
  29. package/dist/utils/resolveActionParams.d.ts +11 -0
  30. package/dist/utils/resolveActionParams.js +19 -0
  31. package/dist/views/ActionParamDialog.js +31 -2
  32. package/dist/views/ObjectView.js +63 -164
  33. package/dist/views/RecordDetailView.d.ts +20 -1
  34. package/dist/views/RecordDetailView.js +23 -9
  35. 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
- import { useState, useCallback, useEffect } from 'react';
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
- /** Optional leading icon (already-instantiated React element). */
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
- return (_jsxs("div", { className: cn('flex justify-between items-center gap-3 py-1.5 sm:py-2 px-3 sm:px-4 border-b shrink-0 bg-background z-10', className), "data-testid": testId ?? 'page-header', children: [_jsxs("div", { className: "flex items-center gap-2 sm:gap-2.5 min-w-0 flex-1", children: [icon && (_jsx("div", { className: "bg-primary/10 p-1 sm:p-1.5 rounded-md shrink-0 text-primary", children: icon })), _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-sm sm:text-base font-semibold tracking-tight text-foreground truncate", children: title }), description && (_jsx("p", { className: "text-xs text-muted-foreground truncate hidden sm:block max-w-md", children: description }))] })] }), actions && (_jsx("div", { className: "flex items-center gap-1.5 sm:gap-2 shrink-0", children: actions }))] }));
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) => (_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))) })] })) : 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, 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 }))] }, param.name))) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: handleCancel, children: t('actionDialog.cancel') }), _jsx(Button, { onClick: handleSubmit, children: t('actionDialog.confirm') })] })] }) }));
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
  }