@wandelbots/wandelbots-js-react-components 2.32.0-pr.feature-robot-precondition-list.372.5d8a86e → 2.32.0-pr.feature-robot-precondition-list.372.82f340f

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wandelbots/wandelbots-js-react-components",
3
- "version": "2.32.0-pr.feature-robot-precondition-list.372.5d8a86e",
3
+ "version": "2.32.0-pr.feature-robot-precondition-list.372.82f340f",
4
4
  "description": "React UI toolkit for building applications on top of the Wandelbots platform",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -139,6 +139,7 @@
139
139
  "dependencies": {
140
140
  "@monaco-editor/react": "^4.7.0",
141
141
  "@mui/x-charts": "^8.9.0",
142
+ "@mui/x-data-grid": "^8.10.1",
142
143
  "@shikijs/monaco": "^3.1.0",
143
144
  "i18next-browser-languagedetector": "^8.0.4",
144
145
  "lodash-es": "^4.17.21",
@@ -1,12 +1,4 @@
1
- import {
2
- Box,
3
- Button,
4
- Card,
5
- Divider,
6
- Typography,
7
- useMediaQuery,
8
- useTheme,
9
- } from "@mui/material"
1
+ import { Box, Button, Card, Divider, Typography, useTheme } from "@mui/material"
10
2
  import { Bounds, useBounds } from "@react-three/drei"
11
3
  import { Canvas } from "@react-three/fiber"
12
4
  import type {
@@ -29,24 +21,17 @@ import { Robot } from "./robots/Robot"
29
21
  function BoundsRefresher({
30
22
  modelRenderTrigger,
31
23
  children,
32
- isPortrait = false,
33
24
  }: {
34
25
  modelRenderTrigger: number
35
26
  children: React.ReactNode
36
- isPortrait?: boolean
37
27
  }) {
38
28
  const bounds = useBounds()
39
29
 
40
30
  useEffect(() => {
41
31
  if (modelRenderTrigger > 0) {
42
- // For portrait mode, use more aggressive fitting
43
- if (isPortrait) {
44
- bounds.refresh().clip().fit()
45
- } else {
46
- bounds.refresh().clip().fit()
47
- }
32
+ bounds.refresh().clip().fit()
48
33
  }
49
- }, [modelRenderTrigger, bounds, isPortrait])
34
+ }, [modelRenderTrigger, bounds])
50
35
 
51
36
  return <>{children}</>
52
37
  }
@@ -90,6 +75,17 @@ export interface RobotCardProps {
90
75
  autoStart?: boolean
91
76
  className?: string
92
77
  }>
78
+ /** Callback to receive cycle timer controls for external timer management */
79
+ onCycleTimerReady?: (controls: {
80
+ startNewCycle: (maxTimeSeconds: number, elapsedSeconds?: number) => void
81
+ pause: () => void
82
+ resume: () => void
83
+ isPaused: () => boolean
84
+ }) => void
85
+ /** Callback fired when a cycle completes (reaches zero) */
86
+ onCycleEnd?: () => void
87
+ /** Whether the cycle timer should auto-start when a new cycle is set */
88
+ cycleTimerAutoStart?: boolean
93
89
  /** Additional CSS class name */
94
90
  className?: string
95
91
  }
@@ -131,6 +127,9 @@ export const RobotCard = externalizeComponent(
131
127
  connectedMotionGroup,
132
128
  robotComponent: RobotComponent = Robot,
133
129
  cycleTimerComponent: CycleTimerComponent = CycleTimer,
130
+ onCycleTimerReady,
131
+ onCycleEnd,
132
+ cycleTimerAutoStart = true,
134
133
  className,
135
134
  }: RobotCardProps) => {
136
135
  const theme = useTheme()
@@ -146,9 +145,13 @@ export const RobotCard = externalizeComponent(
146
145
  }>({ width: 400, height: 600 })
147
146
  const [modelRenderTrigger, setModelRenderTrigger] = useState(0)
148
147
 
149
- // Responsive breakpoints
150
- const isExtraSmall = useMediaQuery(theme.breakpoints.down("sm"))
151
- const isSmall = useMediaQuery(theme.breakpoints.down("md"))
148
+ // Store cycle timer controls for external control
149
+ const cycleControlsRef = useRef<{
150
+ startNewCycle: (maxTimeSeconds: number, elapsedSeconds?: number) => void
151
+ pause: () => void
152
+ resume: () => void
153
+ isPaused: () => boolean
154
+ } | null>(null)
152
155
 
153
156
  // Hook to detect aspect ratio and size changes
154
157
  useEffect(() => {
@@ -198,9 +201,9 @@ export const RobotCard = externalizeComponent(
198
201
  }
199
202
  }, [isDriveToHomePressed, onDriveToHomeRelease])
200
203
 
201
- // Mock cycle timer controls for now
204
+ // Store and provide cycle timer controls for external use
202
205
  const handleCycleComplete = useCallback(
203
- (_controls: {
206
+ (controls: {
204
207
  startNewCycle: (
205
208
  maxTimeSeconds: number,
206
209
  elapsedSeconds?: number,
@@ -209,10 +212,15 @@ export const RobotCard = externalizeComponent(
209
212
  resume: () => void
210
213
  isPaused: () => boolean
211
214
  }) => {
212
- // TODO: Implement cycle timer integration if needed
213
- // Controls are available here for future integration
215
+ // Store the controls for potential future use
216
+ cycleControlsRef.current = controls
217
+
218
+ // Notify parent component that timer controls are ready
219
+ if (onCycleTimerReady) {
220
+ onCycleTimerReady(controls)
221
+ }
214
222
  },
215
- [],
223
+ [onCycleTimerReady],
216
224
  )
217
225
 
218
226
  // Determine if robot should be hidden at small sizes to save space
@@ -301,10 +309,7 @@ export const RobotCard = externalizeComponent(
301
309
  >
302
310
  <PresetEnvironment />
303
311
  <Bounds fit clip observe margin={1}>
304
- <BoundsRefresher
305
- modelRenderTrigger={modelRenderTrigger}
306
- isPortrait={false}
307
- >
312
+ <BoundsRefresher modelRenderTrigger={modelRenderTrigger}>
308
313
  <group ref={robotRef} scale={robotScale}>
309
314
  <RobotComponent
310
315
  connectedMotionGroup={connectedMotionGroup}
@@ -375,6 +380,8 @@ export const RobotCard = externalizeComponent(
375
380
  variant="small"
376
381
  compact
377
382
  onCycleComplete={handleCycleComplete}
383
+ onCycleEnd={onCycleEnd}
384
+ autoStart={cycleTimerAutoStart}
378
385
  />
379
386
  </Box>
380
387
 
@@ -484,7 +491,6 @@ export const RobotCard = externalizeComponent(
484
491
  <Bounds fit observe margin={1}>
485
492
  <BoundsRefresher
486
493
  modelRenderTrigger={modelRenderTrigger}
487
- isPortrait={true}
488
494
  >
489
495
  <group ref={robotRef} scale={robotScale}>
490
496
  <RobotComponent
@@ -516,6 +522,8 @@ export const RobotCard = externalizeComponent(
516
522
  variant="small"
517
523
  compact
518
524
  onCycleComplete={handleCycleComplete}
525
+ onCycleEnd={onCycleEnd}
526
+ autoStart={cycleTimerAutoStart}
519
527
  />
520
528
 
521
529
  {/* Divider */}
@@ -0,0 +1,461 @@
1
+ import ClearIcon from "@mui/icons-material/Clear"
2
+ import FilterListIcon from "@mui/icons-material/FilterList"
3
+ import SearchIcon from "@mui/icons-material/Search"
4
+ import { Box, Divider, Typography } from "@mui/material"
5
+ import {
6
+ DataGrid,
7
+ type DataGridProps,
8
+ FilterPanelTrigger,
9
+ type GridColDef,
10
+ type GridRowParams,
11
+ QuickFilter,
12
+ QuickFilterClear,
13
+ QuickFilterControl,
14
+ QuickFilterTrigger,
15
+ Toolbar,
16
+ ToolbarButton,
17
+ } from "@mui/x-data-grid"
18
+ import { observer } from "mobx-react-lite"
19
+ import { useMemo, useState } from "react"
20
+ import { externalizeComponent } from "../externalizeComponent"
21
+
22
+ export interface SearchableDataGridProps<T = Record<string, unknown>> {
23
+ /**
24
+ * Array of data items to display in the grid
25
+ */
26
+ data: T[]
27
+
28
+ /**
29
+ * Column definitions for the DataGrid
30
+ */
31
+ columns: GridColDef[]
32
+
33
+ /**
34
+ * Function to transform data items into DataGrid rows
35
+ * Should return an object with an 'id' field and other fields matching column definitions
36
+ */
37
+ getRowData: (item: T) => Record<string, unknown> & { id: string | number }
38
+
39
+ /**
40
+ * Callback when a row is clicked
41
+ */
42
+ onRowClick?: (item: T, params: GridRowParams) => void
43
+
44
+ /**
45
+ * Currently selected item (for highlighting)
46
+ */
47
+ selectedItem?: T | null
48
+
49
+ /**
50
+ * Function to get the ID of an item (used for selection highlighting)
51
+ */
52
+ getItemId?: (item: T) => string | number
53
+
54
+ /**
55
+ * Title displayed in the toolbar
56
+ */
57
+ title?: string
58
+
59
+ /**
60
+ * Show item count in title
61
+ * @default true
62
+ */
63
+ showCount?: boolean
64
+
65
+ /**
66
+ * Placeholder text for the search input
67
+ * @default "Search"
68
+ */
69
+ searchPlaceholder?: string
70
+
71
+ /**
72
+ * Additional DataGrid props to pass through
73
+ */
74
+ dataGridProps?: Partial<DataGridProps>
75
+
76
+ /**
77
+ * Custom toolbar component to replace the default one
78
+ */
79
+ CustomToolbar?: React.ComponentType
80
+
81
+ /**
82
+ * Background color for the component
83
+ * @default "transparent"
84
+ */
85
+ backgroundColor?: string
86
+
87
+ /**
88
+ * Select the first item by default
89
+ * @default false
90
+ */
91
+ selectFirstByDefault?: boolean
92
+ }
93
+
94
+ export const SearchableDataGrid = externalizeComponent(
95
+ observer(
96
+ <T,>({
97
+ data,
98
+ columns,
99
+ getRowData,
100
+ onRowClick,
101
+ selectedItem,
102
+ getItemId,
103
+ title,
104
+ showCount = true,
105
+ searchPlaceholder = "Search",
106
+ dataGridProps,
107
+ CustomToolbar,
108
+ backgroundColor = "transparent",
109
+ selectFirstByDefault = false,
110
+ }: SearchableDataGridProps<T>) => {
111
+ // Internal state for selection when not controlled
112
+ const [internalSelectedItem, setInternalSelectedItem] =
113
+ useState<T | null>(null)
114
+
115
+ // Prepare rows for the DataGrid
116
+ const rows = useMemo(() => data.map(getRowData), [data, getRowData])
117
+
118
+ // Handle default selection - only use if no selectedItem is explicitly provided
119
+ const effectiveSelectedItem = useMemo(() => {
120
+ // If selectedItem is explicitly provided, use it (including null)
121
+ if (selectedItem !== undefined) {
122
+ return selectedItem
123
+ }
124
+ // If we have an internal selection, use it
125
+ if (internalSelectedItem !== null) {
126
+ return internalSelectedItem
127
+ }
128
+ // Otherwise, use first item if selectFirstByDefault is true
129
+ if (selectFirstByDefault && data.length > 0) {
130
+ const firstItem = data[0]
131
+ // Set internal state to first item on initial load
132
+ setInternalSelectedItem(firstItem)
133
+ return firstItem
134
+ }
135
+ return null
136
+ }, [selectFirstByDefault, data, selectedItem, internalSelectedItem])
137
+
138
+ // Handle row click
139
+ const handleRowClick = (params: GridRowParams) => {
140
+ const item = data.find((item) => {
141
+ const rowData = getRowData(item)
142
+ return rowData.id === params.id
143
+ })
144
+
145
+ if (item) {
146
+ // Update internal selection state if not controlled by props
147
+ if (selectedItem === undefined) {
148
+ setInternalSelectedItem(item)
149
+ }
150
+
151
+ // Call the user's onRowClick callback
152
+ if (onRowClick) {
153
+ onRowClick(item, params)
154
+ }
155
+ }
156
+ }
157
+
158
+ // Get selected row ID for highlighting
159
+ const selectedRowId = useMemo(() => {
160
+ if (!effectiveSelectedItem || !getItemId) return null
161
+ return getItemId(effectiveSelectedItem)
162
+ }, [effectiveSelectedItem, getItemId])
163
+
164
+ // Default toolbar with filter and quick filter
165
+ function DefaultToolbar() {
166
+ return (
167
+ <Toolbar>
168
+ <Box
169
+ sx={{
170
+ display: "flex",
171
+ width: "100%",
172
+ gap: 1,
173
+ p: 1,
174
+ alignItems: "center",
175
+ }}
176
+ >
177
+ {title && (
178
+ <Typography
179
+ variant="h6"
180
+ sx={{
181
+ fontWeight: 500,
182
+ color: "white",
183
+ }}
184
+ >
185
+ {title}
186
+ {showCount && ` (${data.length})`}
187
+ </Typography>
188
+ )}
189
+ <Box
190
+ sx={{
191
+ ml: "auto",
192
+ display: "flex",
193
+ gap: 1,
194
+ alignItems: "center",
195
+ }}
196
+ >
197
+ <FilterPanelTrigger
198
+ render={
199
+ <ToolbarButton aria-label="Show filters">
200
+ <FilterListIcon fontSize="small" />
201
+ </ToolbarButton>
202
+ }
203
+ />
204
+ <Divider
205
+ orientation="vertical"
206
+ flexItem
207
+ sx={{
208
+ height: "24px",
209
+ alignSelf: "center",
210
+ }}
211
+ />
212
+ <QuickFilter
213
+ render={(props, state) => (
214
+ <Box
215
+ {...props}
216
+ sx={{
217
+ display: "flex",
218
+ overflow: "hidden",
219
+ }}
220
+ >
221
+ <Box
222
+ sx={{
223
+ borderRadius: state.expanded ? "4px 0 0 4px" : "4px",
224
+ borderRight: state.expanded ? "none" : undefined,
225
+ }}
226
+ >
227
+ <QuickFilterTrigger
228
+ render={
229
+ <ToolbarButton aria-label="Search">
230
+ <SearchIcon fontSize="small" />
231
+ </ToolbarButton>
232
+ }
233
+ />
234
+ </Box>
235
+ <Box
236
+ sx={{
237
+ display: "flex",
238
+ overflow: "hidden",
239
+ transition: "all 0.3s ease-in-out",
240
+ width: state.expanded ? "192px" : "0px",
241
+ }}
242
+ >
243
+ <Box
244
+ sx={{
245
+ flex: 1,
246
+ "& .MuiInputBase-root": {
247
+ height: "32px",
248
+ borderRadius:
249
+ state.expanded && state.value !== ""
250
+ ? "0"
251
+ : "0 4px 4px 0",
252
+ },
253
+ }}
254
+ >
255
+ <QuickFilterControl placeholder={searchPlaceholder} />
256
+ </Box>
257
+ {state.expanded && state.value !== "" && (
258
+ <QuickFilterClear
259
+ render={
260
+ <Box sx={{ borderRadius: "0 4px 4px 0" }}>
261
+ <ToolbarButton aria-label="Clear">
262
+ <ClearIcon fontSize="small" />
263
+ </ToolbarButton>
264
+ </Box>
265
+ }
266
+ />
267
+ )}
268
+ </Box>
269
+ </Box>
270
+ )}
271
+ />
272
+ </Box>
273
+ </Box>
274
+ </Toolbar>
275
+ )
276
+ }
277
+
278
+ const ToolbarComponent = CustomToolbar || DefaultToolbar
279
+
280
+ return (
281
+ <Box
282
+ sx={{
283
+ height: "100%",
284
+ display: "flex",
285
+ flexDirection: "column",
286
+ background: "var(--background-paper-elevation-5, #202233)",
287
+ }}
288
+ >
289
+ <DataGrid
290
+ rows={rows}
291
+ columns={columns}
292
+ onRowClick={handleRowClick}
293
+ disableColumnMenu={false}
294
+ disableRowSelectionOnClick={true}
295
+ disableMultipleRowSelection={true}
296
+ hideFooterSelectedRowCount={true}
297
+ filterMode="client"
298
+ sortingOrder={["desc", "asc"]}
299
+ hideFooter={false}
300
+ showToolbar={true}
301
+ slots={{
302
+ toolbar: ToolbarComponent,
303
+ }}
304
+ initialState={{
305
+ sorting: {
306
+ sortModel: [],
307
+ },
308
+ filter: {
309
+ filterModel: {
310
+ items: [],
311
+ },
312
+ },
313
+ ...dataGridProps?.initialState,
314
+ }}
315
+ {...dataGridProps}
316
+ // Ensure autosize properties are not overridden by dataGridProps
317
+ autosizeOnMount={dataGridProps?.autosizeOnMount ?? true}
318
+ autosizeOptions={{
319
+ includeOutliers: true,
320
+ includeHeaders: true,
321
+ expand: true,
322
+ ...dataGridProps?.autosizeOptions,
323
+ }}
324
+ sx={{
325
+ border: "none",
326
+ backgroundColor: "transparent",
327
+ width: "100%",
328
+ "& .MuiDataGrid-main": {
329
+ border: "none",
330
+ backgroundColor: "transparent",
331
+ },
332
+ "& .MuiDataGrid-container--top [role=row]": {
333
+ backgroundColor: "transparent !important",
334
+ },
335
+ "& .MuiDataGrid-topContainer": {
336
+ borderBottom: "none !important",
337
+ },
338
+ "& .MuiDataGrid-columnHeaders": {
339
+ border: "none",
340
+ borderBottom: "none !important",
341
+ backgroundColor: "var(--background-paper-elevation-5, #202233)",
342
+ },
343
+ "& .MuiDataGrid-row": {
344
+ cursor: onRowClick ? "pointer" : "default",
345
+ border: "none",
346
+ margin: "0px 0",
347
+ position: "relative",
348
+ "&:hover": {
349
+ backgroundColor: "transparent !important",
350
+ "&::before": {
351
+ content: '""',
352
+ position: "absolute",
353
+ top: 0,
354
+ left: "16px",
355
+ right: "16px",
356
+ bottom: 0,
357
+ backgroundColor: "action.hover",
358
+ borderRadius: "16px",
359
+ zIndex: 0,
360
+ },
361
+ },
362
+ // Disable MUI's built-in selection styling completely
363
+ "&.Mui-selected": {
364
+ backgroundColor: "transparent !important",
365
+ "&:hover": {
366
+ backgroundColor: "transparent !important",
367
+ },
368
+ },
369
+ // Highlight selected row with a distinct color using data attribute
370
+ ...(selectedRowId !== null && {
371
+ [`&[data-id="${selectedRowId}"]`]: {
372
+ "&::before": {
373
+ content: '""',
374
+ position: "absolute",
375
+ top: 0,
376
+ left: "16px",
377
+ right: "16px",
378
+ bottom: 0,
379
+ backgroundColor: "rgba(255, 255, 255, 0.08) !important",
380
+ borderRadius: "16px",
381
+ zIndex: 0,
382
+ },
383
+ "&:hover::before": {
384
+ backgroundColor: "rgba(255, 255, 255, 0.12) !important",
385
+ },
386
+ },
387
+ }),
388
+ },
389
+ "& .MuiDataGrid-cell--textLeft": {
390
+ paddingLeft: "40px",
391
+ },
392
+ "& .MuiDataGrid-cell": {
393
+ border: "none",
394
+ position: "relative",
395
+ zIndex: 1,
396
+ "&:focus": {
397
+ outline: "none",
398
+ },
399
+ "&:focus-within": {
400
+ outline: "none",
401
+ },
402
+ },
403
+ "& .MuiDataGrid-columnHeader": {
404
+ border: "none",
405
+ paddingLeft: "40px",
406
+ paddingRight: "40px",
407
+ backgroundColor: "transparent !important",
408
+ "& .MuiDataGrid-columnHeaderTitle": {
409
+ color: "rgba(255, 255, 255, 0.6)",
410
+ },
411
+ },
412
+ "& .MuiDataGrid-toolbarContainer": {
413
+ padding: "8px",
414
+ border: "none !important",
415
+ borderBottom: "none !important",
416
+ "& .MuiBox-root": {
417
+ backgroundColor: "transparent !important",
418
+ },
419
+ "& .MuiFormControl-root": {
420
+ backgroundColor: "transparent !important",
421
+ },
422
+ "& .MuiInputBase-root": {
423
+ backgroundColor: "transparent !important",
424
+ },
425
+ "& *": {
426
+ borderBottom: "none !important",
427
+ },
428
+ },
429
+ "& .MuiDataGrid-toolbar": {
430
+ borderBottom: "none !important",
431
+ },
432
+ "& .MuiDataGrid-toolbarFilterList": {
433
+ border: "none",
434
+ },
435
+ "& .MuiDataGrid-withBorderColor": {
436
+ borderColor: "transparent !important",
437
+ },
438
+ "& .MuiDataGrid-columnSeparator": {
439
+ display: "none",
440
+ },
441
+ "& .MuiDataGrid-footerContainer": {
442
+ display: "none",
443
+ },
444
+ "& .MuiDataGrid-filler": {
445
+ border: "none !important",
446
+ borderTop: "none !important",
447
+ borderBottom: "none !important",
448
+ borderLeft: "none !important",
449
+ borderRight: "none !important",
450
+ "--rowBorderColor": "none !important",
451
+ },
452
+ ...dataGridProps?.sx,
453
+ }}
454
+ />
455
+ </Box>
456
+ )
457
+ },
458
+ ),
459
+ )
460
+
461
+ SearchableDataGrid.displayName = "SearchableDataGrid"
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ export { defaultGetModel } from "./components/robots/robotModelLogic"
20
20
  export * from "./components/robots/SupportedRobot"
21
21
  export * from "./components/RobotSetupReadinessIndicator"
22
22
  export * from "./components/safetyBar/SafetyBar"
23
+ export * from "./components/SearchableDataGrid"
23
24
  export * from "./components/SelectableFab"
24
25
  export * from "./components/utils/hooks"
25
26
  export * from "./components/utils/interpolation"