@stonecrop/desktop 0.16.6 → 0.17.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/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @stonecrop/desktop
2
2
 
3
- A three-view UI shell for Stonecrop applications. Renders a doctype list → records list → record form layout driven entirely by the host application's Registry and HST state. Desktop owns no data lifecycle — it emits events and the host app decides what to do.
3
+ A three-view UI shell for Stonecrop applications. Renders a doctype list → records list → record form layout driven by the host application's Registry and HST state.
4
+
5
+ Desktop reads through Stonecrop — on navigating to a list or a record it calls `Stonecrop.getRecords` / `Stonecrop.getRecord`, which fetch through the host's registered `DataClient` and write into HST. Writes are the host's: Desktop emits `action` and the host dispatches it.
4
6
 
5
7
  ## Features
6
8
 
@@ -20,32 +22,42 @@ Desktop requires `@stonecrop/stonecrop` to be installed and the `StonecropPlugin
20
22
 
21
23
  ```typescript
22
24
  import { createApp } from 'vue'
23
- import Stonecrop from '@stonecrop/stonecrop'
24
- import { registry } from './registry'
25
+ import Stonecrop, { Doctype } from '@stonecrop/stonecrop'
26
+ import { RestDataClient } from './client'
27
+ import planDoctype from './doctypes/plan.json'
28
+
29
+ const app = createApp(App)
30
+
31
+ // The plugin constructs the Registry itself and provides it as `$registry` — it does not
32
+ // accept one. Register doctypes on that instance, after install.
33
+ app.use(Stonecrop, { router, client: new RestDataClient() })
34
+
35
+ const registry = app.config.globalProperties.$registry
36
+ registry.addDoctype(Doctype.fromObject(planDoctype))
25
37
 
26
- createApp(App).use(Stonecrop, { registry }).mount('#app')
38
+ app.mount('#app')
27
39
  ```
28
40
 
41
+ `client` is the `DataClient` Desktop reads through. It can also be supplied later with
42
+ `stonecrop.setClient(client)` — Nuxt hosts do this from a plugin via `useStonecropSetup().registerClient`.
43
+
29
44
  ## Basic Usage
30
45
 
31
46
  ```vue
32
47
  <script setup lang="ts">
33
48
  import { Desktop } from '@stonecrop/desktop'
34
- import type { ActionEventPayload } from '@stonecrop/desktop'
35
- import { useStonecrop } from '@stonecrop/stonecrop'
36
-
37
- const { stonecrop } = useStonecrop()
49
+ import { useClientAction } from '@stonecrop/stonecrop'
38
50
 
39
- async function handleAction(payload: ActionEventPayload) {
40
- const node = stonecrop.value?.getRecordById(payload.doctype, payload.recordId)
41
- await node?.triggerTransition(payload.name, { fsmContext: payload.data })
42
- }
51
+ // Runs an action's clientHandler when the doctype declares one, dispatches to the server
52
+ // otherwise, and reconciles the store and the route with the identity the server settled on.
53
+ // In a Nuxt host this is auto-imported — drop the import line.
54
+ const { run } = useClientAction()
43
55
  </script>
44
56
 
45
57
  <template>
46
58
  <Desktop
47
59
  :available-doctypes="['plan', 'recipe', 'resource']"
48
- @action="handleAction"
60
+ @action="run"
49
61
  />
50
62
  </template>
51
63
  ```
@@ -56,25 +68,27 @@ async function handleAction(payload: ActionEventPayload) {
56
68
  |------|------|---------|-------------|
57
69
  | `availableDoctypes` | `string[]` | `[]` | Doctype slugs to display in the doctypes list |
58
70
  | `routeAdapter` | `RouteAdapter` | — | Custom routing layer (required for Nuxt/custom hosts) |
59
- | `confirmFn` | `(msg: string) => boolean \| Promise<boolean>` | `window.confirm` | Replacement for the native browser confirm dialog |
60
- | `recordIdField` | `string` | `'id'` | Field name for the canonical record ID in list views |
71
+
72
+ Record identity is not a prop. It is declared per doctype (`primaryKey`, falling back to `id`) and
73
+ resolved through `Doctype.getRecordId`, so a row's link always matches the key the record is stored
74
+ under. One shell renders many doctypes, so a single prop could never answer this correctly.
61
75
 
62
76
  ## Emitted Events
63
77
 
64
78
  | Event | When |
65
79
  |-------|------|
66
- | `action` | User triggers an FSM transition or DELETE |
80
+ | `action` | User triggers a declared action — an FSM transition or a Command |
67
81
  | `navigate` | Desktop wants to change views |
68
82
  | `record:open` | User opens a specific record |
69
- | `load-records` | Desktop navigates to a records list and needs records loaded into HST |
70
- | `load-record` | Desktop navigates to a record form and needs a single record loaded into HST |
83
+ | `load-records` | Desktop is about to read a records list (notification — Desktop performs the read) |
84
+ | `load-record` | Desktop is about to read a single record (notification — Desktop performs the read) |
71
85
 
72
86
  See [api.md](./api.md) for payload type definitions.
73
87
 
74
88
  ### Event Handling Notes
75
89
 
76
- - **action**: Desktop reads available transitions from `Doctype.getAvailableTransitions` using `Stonecrop.getRecordState`. **Desktop never calls `triggerTransition` itself** — that is the host application's responsibility.
77
- - **load-records / load-record**: Desktop reads from HST but doesn't fetch data. Host apps should listen for these events, fetch from their data source, and call `stonecrop.addRecords()` or `stonecrop.addRecord()` to populate HST.
90
+ - **action**: Desktop merges `Doctype.getAvailableTransitions` and `Doctype.getAvailableCommands`, both resolved against `Stonecrop.getRecordState`, into one Actions dropdown. **Desktop never dispatches** — that is the host application's responsibility.
91
+ - **load-records / load-record**: notifications, not fetch requests. Desktop reads through `Stonecrop.getRecords` / `Stonecrop.getRecord` itself, using the registered `DataClient`; these events announce that read so a host can hang analytics off it. A host that fetches here races Desktop's own read into the same HST key. `load-record` is not emitted for a draft, which has nothing to fetch.
78
92
 
79
93
  ## Router Adapter
80
94
 
@@ -111,37 +125,36 @@ function useCustomRouteAdapter(): RouteAdapter {
111
125
 
112
126
  ## Handling `action` Events
113
127
 
114
- The complete host-side pattern for handling an action in a Nuxt context:
128
+ Dispatching is not the whole job: the result has to land in HST under the identity the *server*
129
+ settled on, which for a newly created record is not the id that was dispatched.
115
130
 
116
- ```typescript
117
- import type { ActionEventPayload } from '@stonecrop/desktop'
118
- import { useStonecrop } from '@stonecrop/stonecrop'
131
+ Bind `@action` to `useClientAction`'s `run`, as in Basic Usage above. It runs an action's
132
+ `clientHandler` when it has one, dispatches otherwise, and reconciles the store and the route. It
133
+ lives in `@stonecrop/stonecrop`, so every Vue 3 host gets the same one; Nuxt hosts also get it as an
134
+ auto-import from `@stonecrop/nuxt`.
119
135
 
120
- const { stonecrop } = useStonecrop()
136
+ Three things are adjustable, for the cases that genuinely differ between applications:
121
137
 
122
- async function handleAction(payload: ActionEventPayload) {
123
- if (!stonecrop.value) return
138
+ | Option | Replaces | Use it for |
139
+ |--------|----------|------------|
140
+ | `buildArgs` | the `[{ id, data }]` envelope | a backend expecting another argument shape |
141
+ | `followRecord` | `router.replace('/{doctype}/{id}')` | a locale prefix, a nested route, or staying put |
142
+ | `onError` | a blocking `window.alert` | your own notification system |
124
143
 
125
- // 1. Optionally persist field changes to HST before the transition
126
- const store = stonecrop.value.getStore()
127
- for (const [field, value] of Object.entries(payload.data)) {
128
- const path = `${payload.doctype}.${payload.recordId}.${field}`
129
- if (store.has(path) && store.get(path) !== value) {
130
- store.set(path, value)
131
- }
132
- }
144
+ `args` is an opaque JSON array: nothing validates it, so both ends of your own stack have to agree.
145
+ `examples/desktop` uses positional `[recordId, data]` and supplies `buildArgs` to say so.
133
146
 
134
- // 2. Call the server (StonecropClient, $fetch, tRPC — whatever your stack uses)
135
- const result = await client.runAction({ name: payload.doctype }, payload.name, [
136
- { id: payload.recordId, data: payload.data },
137
- ])
147
+ Resolving a record's identity and keying it into HST are deliberately **not** adjustable. That rule
148
+ is declared on the doctype and re-derived server-side by the adapter, and every host that re-derived
149
+ it client-side got it wrong. If you dispatch through `Stonecrop.dispatchAction` directly instead of
150
+ using this composable, that method still files the returned record under the settled identity — you
151
+ cannot store it under the wrong key by accident. What you lose is the stale-key cleanup and the
152
+ route-follow, which need the id you dispatched.
138
153
 
139
- // 3. Sync the server response back into HST
140
- if (result.success && result.data) {
141
- stonecrop.value.addRecord(payload.doctype, payload.recordId, result.data)
142
- }
143
- }
144
- ```
154
+ Do not copy form data into HST before dispatching. Desktop already hands you the current form
155
+ snapshot in `payload.data`, and an unsaved record has no HST node to write to.
156
+
157
+ See the [host integration guide](../docs/guides/desktop-integration.md) for the full wiring.
145
158
 
146
159
  ## Provide / Inject
147
160
 
@@ -150,8 +163,10 @@ Desktop provides a `desktopMethods` object that child components (slot content)
150
163
  ```typescript
151
164
  import { inject } from 'vue'
152
165
 
153
- const { navigateToDoctype, openRecord, createNewRecord, handleDelete, emitAction } =
166
+ const { navigateToDoctype, openRecord, createNewRecord, emitAction } =
154
167
  inject('desktopMethods')!
155
168
  ```
156
169
 
157
- `emitAction(name, data?)` is a convenience wrapper for emitting an `action` event from deeply nested slot content without passing refs down manually.
170
+ `emitAction(name, data?)` is a convenience wrapper for emitting an `action` event from deeply nested slot content without passing refs down manually.
171
+
172
+ Desktop blesses no action name. It used to expose a `handleDelete` method and a `confirmFn` prop, which together emitted a hardcoded `DELETE` action and prompted before it — but no doctype declares `DELETE`, so it failed on every click, and only the host knows which of its actions are destructive. Removal is a workflow outcome: declare an action with a `nextState` such as `Archived` or `CANCELLED`, and confirm inside your own `@action` handler before dispatching.
package/dist/desktop.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ActionEventPayload } from '@stonecrop/stonecrop';
1
2
  import ActionSet from './components/ActionSet.vue';
2
3
  import CommandPalette from './components/CommandPalette.vue';
3
4
  import Desktop from './components/Desktop.vue';
@@ -10,18 +11,7 @@ import SheetNav from './components/SheetNav.vue';
10
11
  */
11
12
  export declare type ActionElements = ButtonElement | DropdownElement;
12
13
 
13
- /**
14
- * Payload emitted with the 'action' event when the user triggers an FSM transition
15
- * @public
16
- */
17
- export declare type ActionEventPayload = {
18
- /** The FSM transition name (e.g. 'SAVE', 'SUBMIT', 'APPROVE') */
19
- name: string;
20
- doctype: string;
21
- recordId: string;
22
- /** Snapshot of the form data at the time the action was triggered */
23
- data: Record<string, any>;
24
- };
14
+ export { ActionEventPayload }
25
15
 
26
16
  export { ActionSet }
27
17