@object-ui/components 3.1.0 → 3.1.1

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.
@@ -36,6 +36,7 @@ import { ComponentRegistry } from '@object-ui/core';
36
36
  import type { ActionSchema, ActionLocation, ActionComponent } from '@object-ui/types';
37
37
  import { useCondition } from '@object-ui/react';
38
38
  import { cn } from '../../lib/utils';
39
+ import { useIsMobile } from '../../hooks/use-mobile';
39
40
 
40
41
  export interface ActionBarSchema {
41
42
  type: 'action:bar';
@@ -45,6 +46,8 @@ export interface ActionBarSchema {
45
46
  location?: ActionLocation;
46
47
  /** Maximum visible inline actions before overflow into "More" menu (default: 3) */
47
48
  maxVisible?: number;
49
+ /** Maximum visible inline actions on mobile devices (default: 1). Desktop uses maxVisible instead. */
50
+ mobileMaxVisible?: number;
48
51
  /** Visibility condition expression */
49
52
  visible?: string;
50
53
  /** Layout direction */
@@ -66,22 +69,36 @@ const ActionBarRenderer = forwardRef<HTMLDivElement, { schema: ActionBarSchema;
66
69
  'data-obj-id': dataObjId,
67
70
  'data-obj-type': dataObjType,
68
71
  style,
72
+ data,
69
73
  ...rest
70
74
  } = props;
71
75
 
72
76
  const isVisible = useCondition(schema.visible ? `\${${schema.visible}}` : undefined);
77
+ const isMobile = useIsMobile();
73
78
 
74
- // Filter actions by location
79
+ // Filter actions by location and deduplicate by name
75
80
  const filteredActions = useMemo(() => {
76
81
  const actions = schema.actions || [];
77
- if (!schema.location) return actions;
78
- return actions.filter(
79
- a => !a.locations || a.locations.length === 0 || a.locations.includes(schema.location!),
80
- );
82
+ const located = !schema.location
83
+ ? actions
84
+ : actions.filter(
85
+ a => !a.locations || a.locations.length === 0 || a.locations.includes(schema.location!),
86
+ );
87
+ // Deduplicate by action name — keep first occurrence
88
+ const seen = new Set<string>();
89
+ return located.filter(a => {
90
+ if (!a.name) return true;
91
+ if (seen.has(a.name)) return false;
92
+ seen.add(a.name);
93
+ return true;
94
+ });
81
95
  }, [schema.actions, schema.location]);
82
96
 
83
97
  // Split into visible inline actions and overflow
84
- const maxVisible = schema.maxVisible ?? 3;
98
+ // On mobile, show fewer actions inline (default: 1)
99
+ const maxVisible = isMobile
100
+ ? (schema.mobileMaxVisible ?? 1)
101
+ : (schema.maxVisible ?? 3);
85
102
  const { inlineActions, overflowActions } = useMemo(() => {
86
103
  if (filteredActions.length <= maxVisible) {
87
104
  return { inlineActions: filteredActions, overflowActions: [] as ActionSchema[] };
@@ -137,9 +154,11 @@ const ActionBarRenderer = forwardRef<HTMLDivElement, { schema: ActionBarSchema;
137
154
  schema={{
138
155
  ...action,
139
156
  type: componentType,
157
+ actionType: action.type,
140
158
  variant: action.variant || schema.variant,
141
159
  size: action.size || schema.size,
142
160
  }}
161
+ data={data}
143
162
  />
144
163
  );
145
164
  })}
@@ -28,7 +28,7 @@ import { Loader2 } from 'lucide-react';
28
28
  import { resolveIcon } from './resolve-icon';
29
29
 
30
30
  export interface ActionButtonProps {
31
- schema: ActionSchema & { type: string; className?: string };
31
+ schema: ActionSchema & { type: string; className?: string; actionType?: string };
32
32
  className?: string;
33
33
  /** Override context for this specific action */
34
34
  context?: Record<string, any>;
@@ -41,15 +41,19 @@ const ActionButtonRenderer = forwardRef<HTMLButtonElement, ActionButtonProps>(
41
41
  'data-obj-id': dataObjId,
42
42
  'data-obj-type': dataObjType,
43
43
  style,
44
+ data,
44
45
  ...rest
45
46
  } = props;
46
47
 
47
48
  const { execute } = useAction();
48
49
  const [loading, setLoading] = useState(false);
49
50
 
50
- // Evaluate visibility and enabled conditions
51
- const isVisible = useCondition(schema.visible ? `\${${schema.visible}}` : undefined);
52
- const isEnabled = useCondition(schema.enabled ? `\${${schema.enabled}}` : undefined);
51
+ // Record data may be passed from SchemaRenderer (e.g. DetailView passes record data)
52
+ const recordData = data != null && typeof data === 'object' ? data as Record<string, any> : {};
53
+
54
+ // Evaluate visibility and enabled conditions with record data context
55
+ const isVisible = useCondition(schema.visible ? `\${${schema.visible}}` : undefined, recordData);
56
+ const isEnabled = useCondition(schema.enabled ? `\${${schema.enabled}}` : undefined, recordData);
53
57
 
54
58
  // Resolve icon
55
59
  const Icon = resolveIcon(schema.icon);
@@ -63,14 +67,21 @@ const ActionButtonRenderer = forwardRef<HTMLButtonElement, ActionButtonProps>(
63
67
  setLoading(true);
64
68
 
65
69
  try {
70
+ // Route params correctly:
71
+ // - Array of objects with name+type → ActionParamDef[] → pass as actionParams for collection
72
+ // - Otherwise → pass as actual param values
73
+ const paramsPayload = Array.isArray(schema.params)
74
+ ? { actionParams: schema.params as any }
75
+ : { params: schema.params as Record<string, any> | undefined };
76
+
66
77
  await execute({
67
- type: schema.type,
78
+ type: schema.actionType || schema.type,
68
79
  name: schema.name,
69
80
  target: schema.target,
70
81
  execute: schema.execute,
71
82
  endpoint: schema.endpoint,
72
83
  method: schema.method,
73
- params: schema.params as Record<string, any> | undefined,
84
+ ...paramsPayload,
74
85
  confirmText: schema.confirmText,
75
86
  successMessage: schema.successMessage,
76
87
  errorMessage: schema.errorMessage,
@@ -796,7 +796,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
796
796
  {col.headerIcon && (
797
797
  <span className="text-muted-foreground flex-shrink-0">{col.headerIcon}</span>
798
798
  )}
799
- <span className="text-xs font-normal text-muted-foreground">{col.header}</span>
799
+ <span className="text-xs font-normal text-muted-foreground whitespace-nowrap truncate">{col.header}</span>
800
800
  {sortable && col.sortable !== false && getSortIcon(col.accessorKey)}
801
801
  </div>
802
802
  {resizableColumns && col.resizable !== false && (
@@ -858,7 +858,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
858
858
  key={rowId}
859
859
  data-state={isSelected ? 'selected' : undefined}
860
860
  className={cn(
861
- "bg-background border-b border-border hover:bg-muted/30 group/row",
861
+ "bg-background border-b border-border hover:bg-muted/50 group/row",
862
862
  schema.onRowClick && "cursor-pointer",
863
863
  rowHasChanges && "bg-amber-50 dark:bg-amber-950/20",
864
864
  rowClassName && rowClassName(row, rowIndex)