@stonecrop/desktop 0.10.1 → 0.10.2

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.
@@ -33,20 +33,58 @@
33
33
  <script setup lang="ts">
34
34
  import { useStonecrop } from '@stonecrop/stonecrop'
35
35
  import { AForm, type SchemaTypes, type TableColumn, type TableConfig } from '@stonecrop/aform'
36
- import { computed, nextTick, onMounted, provide, ref, unref, watch } from 'vue'
36
+ import { computed, onMounted, provide, ref, unref, watch } from 'vue'
37
37
 
38
38
  import ActionSet from './ActionSet.vue'
39
39
  import SheetNav from './SheetNav.vue'
40
40
  import CommandPalette from './CommandPalette.vue'
41
- import type { ActionElements } from '../types'
42
-
43
- const { availableDoctypes = [] } = defineProps<{ availableDoctypes?: string[] }>()
41
+ import type {
42
+ ActionElements,
43
+ RouteAdapter,
44
+ NavigationTarget,
45
+ ActionEventPayload,
46
+ RecordOpenEventPayload,
47
+ } from '../types'
48
+
49
+ const props = defineProps<{
50
+ availableDoctypes?: string[]
51
+ /**
52
+ * Pluggable router adapter. When provided, Desktop uses these functions for all
53
+ * routing instead of reaching into the registry's internal Vue Router instance.
54
+ * Nuxt hosts (or any host with custom route conventions) should supply this.
55
+ */
56
+ routeAdapter?: RouteAdapter
57
+ /**
58
+ * Replacement for the native `confirm()` dialog. Desktop calls this before
59
+ * performing a destructive action. Return `true` to proceed.
60
+ * Defaults to the native `window.confirm` if omitted.
61
+ */
62
+ confirmFn?: (message: string) => boolean | Promise<boolean>
63
+ }>()
64
+
65
+ const emit = defineEmits<{
66
+ /**
67
+ * Fired when the user triggers an FSM transition (action button click).
68
+ * The host app is responsible for calling the server, persisting state, etc.
69
+ */
70
+ action: [payload: ActionEventPayload]
71
+ /**
72
+ * Fired when Desktop wants to navigate to a different view.
73
+ * Also calls routeAdapter.navigate() if an adapter is provided.
74
+ */
75
+ navigate: [target: NavigationTarget]
76
+ /**
77
+ * Fired when the user opens a specific record.
78
+ */
79
+ 'record:open': [payload: RecordOpenEventPayload]
80
+ }>()
81
+
82
+ const { availableDoctypes = [] } = props
44
83
 
45
84
  const { stonecrop } = useStonecrop()
46
85
 
47
86
  // State
48
87
  const loading = ref(false)
49
- const saving = ref(false)
50
88
  const commandPaletteOpen = ref(false)
51
89
 
52
90
  // HST-based form data management - field triggers are handled automatically by HST
@@ -90,12 +128,12 @@ const currentViewData = computed<Record<string, any>>({
90
128
  },
91
129
  })
92
130
 
93
- // HST-based form data management - field triggers are handled automatically by HST
94
-
95
- // Computed properties for current route context
96
- const route = computed(() => unref(stonecrop.value?.registry.router?.currentRoute))
97
- const router = computed(() => stonecrop.value?.registry.router)
131
+ // Computed properties for current route context.
132
+ // When a routeAdapter is provided it takes full precedence over the registry's internal router.
133
+ const route = computed(() => (props.routeAdapter ? null : unref(stonecrop.value?.registry.router?.currentRoute)))
134
+ const router = computed(() => (props.routeAdapter ? null : stonecrop.value?.registry.router))
98
135
  const currentDoctype = computed(() => {
136
+ if (props.routeAdapter) return props.routeAdapter.getCurrentDoctype()
99
137
  if (!route.value) return ''
100
138
 
101
139
  // First check if we have actualDoctype in meta (from registered routes)
@@ -119,6 +157,7 @@ const currentDoctype = computed(() => {
119
157
 
120
158
  // The route doctype for display and navigation (e.g., 'todo')
121
159
  const routeDoctype = computed(() => {
160
+ if (props.routeAdapter) return props.routeAdapter.getCurrentDoctype()
122
161
  if (!route.value) return ''
123
162
 
124
163
  // Check route meta first
@@ -141,6 +180,7 @@ const routeDoctype = computed(() => {
141
180
  })
142
181
 
143
182
  const currentRecordId = computed(() => {
183
+ if (props.routeAdapter) return props.routeAdapter.getCurrentRecordId()
144
184
  if (!route.value) return ''
145
185
 
146
186
  // For named routes, use params.recordId
@@ -160,6 +200,7 @@ const isNewRecord = computed(() => currentRecordId.value?.startsWith('new-'))
160
200
 
161
201
  // Determine current view based on route
162
202
  const currentView = computed(() => {
203
+ if (props.routeAdapter) return props.routeAdapter.getCurrentView()
163
204
  if (!route.value) {
164
205
  return 'doctypes'
165
206
  }
@@ -190,58 +231,39 @@ const currentView = computed(() => {
190
231
  })
191
232
 
192
233
  // Computed properties (now that all helper functions are defined)
193
- // Helper function to get available transitions for current record
234
+ // Helper function to get available transitions for current record.
235
+ // Reads the actual FSM state from the record's `status` field (or falls back to the
236
+ // workflow initial state) so the available action buttons always reflect reality.
194
237
  const getAvailableTransitions = () => {
195
238
  if (!stonecrop.value || !currentDoctype.value || !currentRecordId.value) {
196
239
  return []
197
240
  }
198
241
 
199
242
  try {
200
- const registry = stonecrop.value.registry
201
- const meta = registry.registry[currentDoctype.value]
202
-
203
- if (!meta?.workflow?.states) {
204
- return []
205
- }
206
-
207
- // Get current FSM state (for now, use workflow initial state or 'editing')
208
- // In a full implementation, this would track actual FSM state
209
- const currentState = isNewRecord.value ? 'creating' : 'editing'
210
- const stateConfig = meta.workflow.states[currentState]
211
-
212
- if (!stateConfig?.on) {
213
- return []
214
- }
215
-
216
- // Get available transitions from current state
217
- const transitions = Object.keys(stateConfig.on)
218
-
219
- // Create action elements for each transition
220
- const actionElements = transitions.map(transition => {
221
- const targetState = stateConfig.on?.[transition]
222
- const targetStateName = typeof targetState === 'string' ? targetState : 'unknown'
223
-
224
- const actionFn = async () => {
225
- const node = stonecrop.value?.getRecordById(currentDoctype.value, currentRecordId.value)
226
- if (node) {
227
- const recordData = currentViewData.value || {}
228
- await node.triggerTransition(transition, {
229
- currentState,
230
- targetState: targetStateName,
231
- fsmContext: recordData,
232
- })
233
- }
234
- }
235
-
236
- const element = {
237
- label: `${transition} (→ ${targetStateName})`,
238
- action: actionFn,
239
- }
240
-
241
- return element
242
- })
243
-
244
- return actionElements
243
+ const meta = stonecrop.value.registry.getDoctype(currentDoctype.value)
244
+ if (!meta?.workflow) return []
245
+
246
+ // Delegate state resolution to Stonecrop — reads record 'status', falls back to workflow.initial
247
+ const currentState = stonecrop.value.getRecordState(currentDoctype.value, currentRecordId.value)
248
+
249
+ // Delegate transition lookup to DoctypeMeta — no more manual workflow introspection
250
+ const transitions = meta.getAvailableTransitions(currentState)
251
+
252
+ const recordData = currentViewData.value || {}
253
+
254
+ // Each transition emits an 'action' event. The host app decides what to do
255
+ // (call the server, trigger an FSM actor, update HST, etc.).
256
+ return transitions.map(({ name, targetState }) => ({
257
+ label: `${name} (→ ${targetState})`,
258
+ action: () => {
259
+ emit('action', {
260
+ name,
261
+ doctype: currentDoctype.value,
262
+ recordId: currentRecordId.value,
263
+ data: recordData,
264
+ })
265
+ },
266
+ }))
245
267
  } catch (error) {
246
268
  // eslint-disable-next-line no-console
247
269
  console.warn('Error getting available transitions:', error)
@@ -249,40 +271,20 @@ const getAvailableTransitions = () => {
249
271
  }
250
272
  }
251
273
 
252
- // New component reactive properties// New component reactive properties
253
274
  const actionElements = computed<ActionElements[]>(() => {
254
275
  const elements: ActionElements[] = []
255
276
 
256
277
  switch (currentView.value) {
257
- case 'doctypes':
278
+ case 'records':
258
279
  elements.push({
259
280
  type: 'button',
260
- label: 'Refresh',
261
- action: () => {
262
- // Refresh doctypes
263
- window.location.reload()
264
- },
281
+ label: 'New Record',
282
+ action: () => void createNewRecord(),
265
283
  })
266
284
  break
267
- case 'records':
268
- elements.push(
269
- {
270
- type: 'button',
271
- label: 'New Record',
272
- action: () => void createNewRecord(),
273
- },
274
- {
275
- type: 'button',
276
- label: 'Refresh',
277
- action: () => {
278
- // Refresh records
279
- window.location.reload()
280
- },
281
- }
282
- )
283
- break
284
285
  case 'record': {
285
- // Add XState Transitions dropdown for record view
286
+ // Populate the Actions dropdown with every FSM transition available in the
287
+ // record's current state. Clicking a transition emits 'action'.
286
288
  const transitionActions = getAvailableTransitions()
287
289
  if (transitionActions.length > 0) {
288
290
  elements.push({
@@ -307,10 +309,13 @@ const navigationBreadcrumbs = computed(() => {
307
309
  { title: formatDoctypeName(routeDoctype.value), to: `/${routeDoctype.value}` }
308
310
  )
309
311
  } else if (currentView.value === 'record' && routeDoctype.value) {
312
+ const recordPath = currentRecordId.value
313
+ ? `/${routeDoctype.value}/${currentRecordId.value}`
314
+ : route.value?.fullPath ?? ''
310
315
  breadcrumbs.push(
311
316
  { title: 'Home', to: '/' },
312
317
  { title: formatDoctypeName(routeDoctype.value), to: `/${routeDoctype.value}` },
313
- { title: isNewRecord.value ? 'New Record' : 'Edit Record', to: route.value?.fullPath || '' }
318
+ { title: isNewRecord.value ? 'New Record' : 'Edit Record', to: recordPath }
314
319
  )
315
320
  }
316
321
 
@@ -329,7 +334,7 @@ const searchCommands = (query: string): Command[] => {
329
334
  {
330
335
  title: 'Go Home',
331
336
  description: 'Navigate to the home page',
332
- action: () => void router.value?.push('/'),
337
+ action: () => void doNavigate({ view: 'doctypes' }),
333
338
  },
334
339
  {
335
340
  title: 'Toggle Command Palette',
@@ -343,7 +348,7 @@ const searchCommands = (query: string): Command[] => {
343
348
  commands.push({
344
349
  title: `View ${formatDoctypeName(routeDoctype.value)} Records`,
345
350
  description: `Navigate to ${routeDoctype.value} list`,
346
- action: () => void router.value?.push(`/${routeDoctype.value}`),
351
+ action: () => void doNavigate({ view: 'records', doctype: routeDoctype.value }),
347
352
  })
348
353
 
349
354
  commands.push({
@@ -358,7 +363,7 @@ const searchCommands = (query: string): Command[] => {
358
363
  commands.push({
359
364
  title: `View ${formatDoctypeName(doctype)}`,
360
365
  description: `Navigate to ${doctype} list`,
361
- action: () => void router.value?.push(`/${doctype}`),
366
+ action: () => void doNavigate({ view: 'records', doctype }),
362
367
  })
363
368
  })
364
369
 
@@ -391,30 +396,36 @@ const getRecordCount = (doctype: string): number => {
391
396
  return recordIds.length
392
397
  }
393
398
 
399
+ // Internal navigation helper: emits 'navigate', then calls the adapter (if any)
400
+ // or falls back to the registry's Vue Router instance.
401
+ const doNavigate = async (target: NavigationTarget) => {
402
+ emit('navigate', target)
403
+ if (props.routeAdapter) {
404
+ await props.routeAdapter.navigate(target)
405
+ } else {
406
+ if (target.view === 'doctypes') {
407
+ await router.value?.push('/')
408
+ } else if (target.view === 'records' && target.doctype) {
409
+ await router.value?.push(`/${target.doctype}`)
410
+ } else if (target.view === 'record' && target.doctype && target.recordId) {
411
+ await router.value?.push(`/${target.doctype}/${target.recordId}`)
412
+ }
413
+ }
414
+ }
415
+
394
416
  const navigateToDoctype = async (doctype: string) => {
395
- await router.value?.push(`/${doctype}`)
417
+ await doNavigate({ view: 'records', doctype })
396
418
  }
397
419
 
398
420
  const openRecord = async (recordId: string) => {
399
- await router.value?.push(`/${routeDoctype.value}/${recordId}`)
421
+ const doctype = routeDoctype.value
422
+ emit('record:open', { doctype, recordId })
423
+ await doNavigate({ view: 'record', doctype, recordId })
400
424
  }
401
425
 
402
426
  const createNewRecord = async () => {
403
427
  const newId = `new-${Date.now()}`
404
- await router.value?.push(`/${routeDoctype.value}/${newId}`)
405
- }
406
-
407
- // Doctype metadata loader - simplified since router handles most of this
408
- const loadDoctypeMetadata = (doctype: string) => {
409
- if (!stonecrop.value) return
410
-
411
- // Ensure the doctype structure exists in HST
412
- // The router should have already loaded the metadata, but this ensures the HST structure exists
413
- try {
414
- stonecrop.value.records(doctype)
415
- } catch {
416
- // Silent error handling - structure will be created if needed
417
- }
428
+ await doNavigate({ view: 'record', doctype: routeDoctype.value, recordId: newId })
418
429
  }
419
430
 
420
431
  // Schema generator functions - moved here to be available to computed properties
@@ -598,101 +609,30 @@ const currentViewSchema = computed<SchemaTypes[]>(() => {
598
609
  }
599
610
  })
600
611
 
601
- // Action handlers (will be triggered by button clicks in the UI)
602
- const handleSave = async () => {
603
- if (!stonecrop.value) return
604
-
605
- saving.value = true
606
-
607
- try {
608
- const formData = currentViewData.value || {}
609
-
610
- if (isNewRecord.value) {
611
- const newId = `record-${Date.now()}`
612
- const recordData = { id: newId, ...formData }
613
-
614
- stonecrop.value.addRecord(currentDoctype.value, newId, recordData)
615
-
616
- // Trigger SAVE transition for new record
617
- const node = stonecrop.value.getRecordById(currentDoctype.value, newId)
618
- if (node) {
619
- await node.triggerTransition('SAVE', {
620
- currentState: 'creating',
621
- targetState: 'saved',
622
- fsmContext: recordData,
623
- })
624
- }
625
-
626
- await router.value?.replace(`/${routeDoctype.value}/${newId}`)
627
- } else {
628
- const recordData = { id: currentRecordId.value, ...formData }
629
- stonecrop.value.addRecord(currentDoctype.value, currentRecordId.value, recordData)
630
-
631
- // Trigger SAVE transition for existing record
632
- const node = stonecrop.value.getRecordById(currentDoctype.value, currentRecordId.value)
633
- if (node) {
634
- await node.triggerTransition('SAVE', {
635
- currentState: 'editing',
636
- targetState: 'saved',
637
- fsmContext: recordData,
638
- })
639
- }
640
- }
641
- } catch {
642
- // Silently handle error
643
- } finally {
644
- saving.value = false
645
- }
646
- }
647
-
648
- const handleCancel = async () => {
649
- if (isNewRecord.value) {
650
- // For new records, we don't have a specific record node yet
651
- // Just navigate back without triggering transition
652
- await router.value?.push(`/${routeDoctype.value}`)
653
- } else {
654
- // Trigger CANCEL transition for existing record
655
- if (stonecrop.value) {
656
- const node = stonecrop.value.getRecordById(currentDoctype.value, currentRecordId.value)
657
- if (node) {
658
- await node.triggerTransition('CANCEL', {
659
- currentState: 'editing',
660
- targetState: 'cancelled',
661
- })
662
- }
663
- }
664
- // Reload current record data
665
- loadRecordData()
666
- }
667
- }
668
-
669
612
  const handleActionClick = (_label: string, action: (() => void | Promise<void>) | undefined) => {
670
613
  if (action) {
671
614
  void action()
672
615
  }
673
616
  }
674
617
 
618
+ // Desktop does NOT own the delete lifecycle — it asks for confirmation, then emits
619
+ // an 'action' event. The host app is responsible for removing the record from HST
620
+ // and calling the server.
675
621
  const handleDelete = async (recordId?: string) => {
676
- if (!stonecrop.value) return
677
-
678
622
  const targetRecordId = recordId || currentRecordId.value
679
623
  if (!targetRecordId) return
680
624
 
681
- if (confirm('Are you sure you want to delete this record?')) {
682
- // Trigger DELETE transition before removing
683
- const node = stonecrop.value.getRecordById(currentDoctype.value, targetRecordId)
684
- if (node) {
685
- await node.triggerTransition('DELETE', {
686
- currentState: 'editing',
687
- targetState: 'deleted',
688
- })
689
- }
625
+ const confirmed = props.confirmFn
626
+ ? await props.confirmFn('Are you sure you want to delete this record?')
627
+ : confirm('Are you sure you want to delete this record?')
690
628
 
691
- stonecrop.value.removeRecord(currentDoctype.value, targetRecordId)
692
-
693
- if (currentView.value === 'record') {
694
- await router.value?.push(`/${routeDoctype.value}`)
695
- }
629
+ if (confirmed) {
630
+ emit('action', {
631
+ name: 'DELETE',
632
+ doctype: currentDoctype.value,
633
+ recordId: targetRecordId,
634
+ data: currentViewData.value || {},
635
+ })
696
636
  }
697
637
  }
698
638
 
@@ -701,21 +641,8 @@ const handleClick = async (event: Event) => {
701
641
  const target = event.target as HTMLElement
702
642
  const action = target.getAttribute('data-action')
703
643
 
704
- if (action) {
705
- switch (action) {
706
- case 'create':
707
- await createNewRecord()
708
- break
709
- case 'save':
710
- await handleSave()
711
- break
712
- case 'cancel':
713
- await handleCancel()
714
- break
715
- case 'delete':
716
- await handleDelete()
717
- break
718
- }
644
+ if (action === 'create') {
645
+ await createNewRecord()
719
646
  }
720
647
 
721
648
  // Handle table cell clicks for actions
@@ -758,41 +685,6 @@ const handleClick = async (event: Event) => {
758
685
  }
759
686
  }
760
687
 
761
- // Watch for route changes to load appropriate data
762
- watch(
763
- [currentView, currentDoctype, currentRecordId],
764
- () => {
765
- if (currentView.value === 'record') {
766
- loadRecordData()
767
- }
768
- },
769
- { immediate: true }
770
- )
771
-
772
- // Watch for Stonecrop instance to become available
773
- watch(
774
- stonecrop,
775
- newStonecrop => {
776
- if (newStonecrop) {
777
- // Force a re-evaluation of the current view schema when Stonecrop becomes available
778
- // This is handled automatically by the reactive computed properties
779
- }
780
- },
781
- { immediate: true }
782
- )
783
-
784
- // Watch for when we need to load data for records view
785
- watch(
786
- [currentView, currentDoctype, stonecrop],
787
- ([view, doctype, stonecropInstance]) => {
788
- if (view === 'records' && doctype && stonecropInstance) {
789
- // Ensure doctype metadata is loaded
790
- loadDoctypeMetadata(doctype)
791
- }
792
- },
793
- { immediate: true }
794
- )
795
-
796
688
  const loadRecordData = () => {
797
689
  if (!stonecrop.value || !currentDoctype.value) return
798
690
 
@@ -800,11 +692,11 @@ const loadRecordData = () => {
800
692
 
801
693
  try {
802
694
  if (!isNewRecord.value) {
803
- // For existing records, ensure the record exists in HST
804
- // The computed currentViewData will automatically read from HST
695
+ // For existing records, ensure the record exists in HST.
696
+ // The computed currentViewData will automatically read from HST.
805
697
  stonecrop.value.getRecordById(currentDoctype.value, currentRecordId.value)
806
698
  }
807
- // For new records, currentViewData computed property will return {} automatically
699
+ // For new records, currentViewData computed property will return {} automatically.
808
700
  } catch (error) {
809
701
  // eslint-disable-next-line no-console
810
702
  console.warn('Error loading record data:', error)
@@ -813,29 +705,43 @@ const loadRecordData = () => {
813
705
  }
814
706
  }
815
707
 
816
- // Provide methods for action components
708
+ // Watch for route changes to load appropriate data
709
+ watch(
710
+ [currentView, currentDoctype, currentRecordId],
711
+ () => {
712
+ if (currentView.value === 'record') {
713
+ loadRecordData()
714
+ }
715
+ },
716
+ { immediate: true }
717
+ )
718
+
719
+ // Stonecrop reactive computed properties update automatically when the instance
720
+ // becomes available — no manual watcher needed.
721
+
722
+ // Provide navigation helpers and an emitAction convenience function to child components.
817
723
  const desktopMethods = {
818
724
  navigateToDoctype,
819
725
  openRecord,
820
726
  createNewRecord,
821
- handleSave,
822
- handleCancel,
823
727
  handleDelete,
728
+ /**
729
+ * Convenience wrapper so child components (e.g. slot content) can emit
730
+ * an action event without needing a direct reference to the emit function.
731
+ */
732
+ emitAction: (name: string, data?: Record<string, any>) => {
733
+ emit('action', {
734
+ name,
735
+ doctype: currentDoctype.value,
736
+ recordId: currentRecordId.value,
737
+ data: data ?? currentViewData.value ?? {},
738
+ })
739
+ },
824
740
  }
825
741
 
826
742
  provide('desktopMethods', desktopMethods)
827
743
 
828
- // Register action components in Vue app
829
744
  onMounted(() => {
830
- // Wait a tick for stonecrop to be ready, then load initial data
831
- void nextTick(() => {
832
- if (currentView.value === 'records' && currentDoctype.value && stonecrop.value) {
833
- loadDoctypeMetadata(currentDoctype.value)
834
- }
835
- })
836
-
837
- // Components will be automatically registered via the global component system
838
-
839
745
  // Add keyboard shortcuts
840
746
  const handleKeydown = (event: KeyboardEvent) => {
841
747
  // Ctrl+K or Cmd+K to open command palette
package/src/index.ts CHANGED
@@ -4,5 +4,6 @@ import Desktop from './components/Desktop.vue'
4
4
  import SheetNav from './components/SheetNav.vue'
5
5
  import StonecropDesktop from './plugins'
6
6
  export type * from './types'
7
+ export type { RouteAdapter, NavigationTarget, ActionEventPayload, RecordOpenEventPayload } from './types'
7
8
 
8
9
  export { ActionSet, CommandPalette, Desktop, SheetNav, StonecropDesktop }
@@ -40,3 +40,52 @@ export type DropdownElement = BaseElement & {
40
40
  * @public
41
41
  */
42
42
  export type ActionElements = ButtonElement | DropdownElement
43
+
44
+ /**
45
+ * Navigation target passed to RouteAdapter.navigate and emitted with the 'navigate' event
46
+ * @public
47
+ */
48
+ export type NavigationTarget = {
49
+ view: 'doctypes' | 'records' | 'record'
50
+ doctype?: string
51
+ recordId?: string
52
+ }
53
+
54
+ /**
55
+ * Adapter that lets host applications (Nuxt, etc.) supply their own routing layer.
56
+ * When provided as a prop, Desktop uses these functions instead of reaching into
57
+ * the Vue Router instance baked into the Stonecrop registry.
58
+ * @public
59
+ */
60
+ export type RouteAdapter = {
61
+ /** Returns the active doctype key (e.g. 'plan', 'recipe'). Called inside computed — should read reactive state. */
62
+ getCurrentDoctype: () => string
63
+ /** Returns the active record ID, or '' when viewing a list. Called inside computed. */
64
+ getCurrentRecordId: () => string
65
+ /** Returns which of the three views is currently active. Called inside computed. */
66
+ getCurrentView: () => 'doctypes' | 'records' | 'record'
67
+ /** Perform the navigation. Called after the host app has handled any side effects. */
68
+ navigate: (target: NavigationTarget) => void | Promise<void>
69
+ }
70
+
71
+ /**
72
+ * Payload emitted with the 'action' event when the user triggers an FSM transition
73
+ * @public
74
+ */
75
+ export type ActionEventPayload = {
76
+ /** The FSM transition name (e.g. 'SAVE', 'SUBMIT', 'APPROVE') */
77
+ name: string
78
+ doctype: string
79
+ recordId: string
80
+ /** Snapshot of the form data at the time the action was triggered */
81
+ data: Record<string, any>
82
+ }
83
+
84
+ /**
85
+ * Payload emitted with the 'record:open' event
86
+ * @public
87
+ */
88
+ export type RecordOpenEventPayload = {
89
+ doctype: string
90
+ recordId: string
91
+ }