@live-change/frontend-template 0.9.199 → 0.9.200

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 (44) hide show
  1. package/.claude/rules/live-change-backend-actions-views-triggers.md +62 -0
  2. package/.claude/rules/live-change-backend-event-sourcing.md +186 -0
  3. package/.claude/rules/live-change-backend-models-and-relations.md +72 -0
  4. package/.claude/rules/live-change-frontend-vue-primevue.md +26 -0
  5. package/.claude/settings.json +32 -0
  6. package/.claude/skills/create-skills-and-rules/SKILL.md +248 -0
  7. package/.claude/skills/live-change-backend-change-triggers/SKILL.md +186 -0
  8. package/.claude/skills/live-change-design-actions-views-triggers/SKILL.md +462 -0
  9. package/.claude/skills/live-change-design-models-relations/SKILL.md +230 -0
  10. package/.claude/skills/live-change-design-service/SKILL.md +133 -0
  11. package/.claude/skills/live-change-frontend-accessible-objects/SKILL.md +384 -0
  12. package/.claude/skills/live-change-frontend-accessible-objects.md +383 -0
  13. package/.claude/skills/live-change-frontend-action-buttons/SKILL.md +129 -0
  14. package/.claude/skills/live-change-frontend-action-form/SKILL.md +149 -0
  15. package/.claude/skills/live-change-frontend-analytics/SKILL.md +147 -0
  16. package/.claude/skills/live-change-frontend-command-forms/SKILL.md +216 -0
  17. package/.claude/skills/live-change-frontend-data-views/SKILL.md +183 -0
  18. package/.claude/skills/live-change-frontend-editor-form/SKILL.md +240 -0
  19. package/.claude/skills/live-change-frontend-locale-time/SKILL.md +172 -0
  20. package/.claude/skills/live-change-frontend-page-list-detail/SKILL.md +201 -0
  21. package/.claude/skills/live-change-frontend-range-list/SKILL.md +129 -0
  22. package/.claude/skills/live-change-frontend-ssr-setup/SKILL.md +119 -0
  23. package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +88 -0
  24. package/.cursor/rules/live-change-backend-event-sourcing.mdc +185 -0
  25. package/.cursor/rules/live-change-backend-models-and-relations.mdc +62 -0
  26. package/.cursor/skills/create-skills-and-rules.md +248 -0
  27. package/.cursor/skills/live-change-backend-change-triggers.md +186 -0
  28. package/.cursor/skills/live-change-design-actions-views-triggers.md +178 -79
  29. package/.cursor/skills/live-change-design-models-relations.md +112 -50
  30. package/.cursor/skills/live-change-design-service.md +1 -0
  31. package/.cursor/skills/live-change-frontend-accessible-objects.md +384 -0
  32. package/.cursor/skills/live-change-frontend-action-buttons.md +1 -0
  33. package/.cursor/skills/live-change-frontend-action-form.md +9 -3
  34. package/.cursor/skills/live-change-frontend-analytics.md +1 -0
  35. package/.cursor/skills/live-change-frontend-command-forms.md +1 -0
  36. package/.cursor/skills/live-change-frontend-data-views.md +1 -0
  37. package/.cursor/skills/live-change-frontend-editor-form.md +135 -72
  38. package/.cursor/skills/live-change-frontend-locale-time.md +1 -0
  39. package/.cursor/skills/live-change-frontend-page-list-detail.md +1 -0
  40. package/.cursor/skills/live-change-frontend-range-list.md +1 -0
  41. package/.cursor/skills/live-change-frontend-ssr-setup.md +1 -0
  42. package/front/src/router.js +2 -1
  43. package/opencode.json +10 -0
  44. package/package.json +52 -50
@@ -0,0 +1,383 @@
1
+ ---
2
+ description: List entities accessible via access control views (myAccessibleObjects, myAccessesByObjectType, invitations, roles)
3
+ ---
4
+
5
+ # Skill: live-change-frontend-accessible-objects (Claude Code)
6
+
7
+ Use this skill when you need to **list entities accessible to the current user** using the access-control service:
8
+
9
+ - objects with `entity` access control (not simple `userItem` relations),
10
+ - objects the user has roles on via `accessControl_setAccess` / `accessControl_setPublicAccess`,
11
+ - invitations to objects,
12
+ - role-filtered lists of accessible objects.
13
+
14
+ This skill complements:
15
+
16
+ - backend skill `live-change-design-actions-views-triggers` (how to grant access and enable indexes),
17
+ - frontend skills `live-change-frontend-data-views` and `live-change-frontend-page-list-detail`.
18
+
19
+ ## When to use
20
+
21
+ - You want **all objects of a given type** that the current user can access (not just those they created).
22
+ - You are building a list like “My companies”, “My projects”, “Events I can join”.
23
+ - You already grant access through access-control triggers (owner / member / reader roles).
24
+ - The objects are **entities** with access control, not simple `userItem` / `itemOf` relations.
25
+
26
+ If you simply need “records owned by user”, prefer the `userItem` / `itemOf` relation patterns from `live-change-design-models-relations`. Use this skill when access is controlled by roles in the access-control service.
27
+
28
+ ---
29
+
30
+ ## Step 1 – Understand the views you can use
31
+
32
+ There are two main ways to list accessible objects on the frontend.
33
+
34
+ ### 1. Indexed pipeline (requires `indexed: true` on access-control)
35
+
36
+ Enabled when the access-control service is configured with:
37
+
38
+ ```js
39
+ // app.server/app.config.js (service config)
40
+ {
41
+ name: 'accessControl',
42
+ createSessionOnUpdate: true,
43
+ contactTypes,
44
+ indexed: true
45
+ }
46
+ ```
47
+
48
+ Then the following views become available (from `access-control-service/indexes.js`):
49
+
50
+ - `myAccessibleObjects({ objectType?, ...range })`
51
+ - `myAccessibleObjectsByRole({ role, objectType?, ...range })`
52
+ - `accessibleObjects(...)` / `accessibleObjectsByRole(...)` – admin-only variants with explicit `sessionOrUserType` + `sessionOrUser`
53
+ - `objectAccesses({ objectType, object, role?, ...range })` – list owners/roles for a specific object
54
+
55
+ Use these when you want:
56
+
57
+ - “all my accessible objects of type X” (`myAccessibleObjects` with `objectType`),
58
+ - “all my objects where I have role Y” (`myAccessibleObjectsByRole` with `role`),
59
+ - admin tools for inspecting who has access to what (`accessibleObjects`, `objectAccesses`).
60
+
61
+ ### 2. Non-indexed views (always available)
62
+
63
+ Even without `indexed: true`, `view.js` provides:
64
+
65
+ - `myAccessesByObjectType({ objectType, ...range })`
66
+ - `myAccessesByObjectTypeAndRole({ objectType, role, ...range })`
67
+ - `myAccessInvitationsByObjectType({ objectType, ...range })`
68
+ - `myAccessInvitationsByObjectTypeAndRole({ objectType, role, ...range })`
69
+
70
+ Use these when:
71
+
72
+ - you need invitations in addition to accepted accesses,
73
+ - the project has not enabled `indexed: true` yet,
74
+ - you are building paginated lists with `<RangeViewer>` based on access entries.
75
+
76
+ **Object type format:** always `serviceName_ModelName` (for example `company_Company`, `speedDating_Event`).
77
+
78
+ ---
79
+
80
+ ## Step 2 – Simple list with `myAccessibleObjects` (indexed)
81
+
82
+ This is the most convenient way to list all entities of one type accessible to the current user, when indexes are enabled.
83
+
84
+ Example: list companies the user can access (similar to auto-firma `companies/index.vue`):
85
+
86
+ ```vue
87
+ <route>
88
+ { "name": "companies", "meta": { "signedIn": true } }
89
+ </route>
90
+
91
+ <template>
92
+ <div class="container mx-auto p-4">
93
+ <div class="flex items-center justify-between mb-6">
94
+ <h1 class="text-2xl font-bold">{{ t('companies.list.title') }}</h1>
95
+ <Button :label="t('companies.list.addButton')" icon="pi pi-plus"
96
+ @click="router.push({ name: 'companiesNew' })" />
97
+ </div>
98
+
99
+ <Card v-if="!accessibleCompanies?.length">
100
+ <template #content>
101
+ <p class="text-center text-gray-500">
102
+ {{ t('companies.list.empty') }}
103
+ </p>
104
+ </template>
105
+ </Card>
106
+
107
+ <div class="grid gap-4">
108
+ <Card v-for="accessible in accessibleCompanies" :key="accessible.id"
109
+ class="cursor-pointer hover:shadow-md transition-shadow">
110
+ <template #content>
111
+ <div class="flex items-center justify-between">
112
+ <div>
113
+ <div class="text-lg font-semibold">{{ accessible.company.name }}</div>
114
+ <div class="text-sm text-gray-500">
115
+ {{ t('companies.list.nipLabel') }} {{ accessible.company.nip }}
116
+ </div>
117
+ </div>
118
+ <router-link :to="{ name: 'company', params: { company: accessible.company.id } }">
119
+ <Button :label="t('companies.list.details')"
120
+ icon="pi pi-arrow-right" severity="secondary" />
121
+ </router-link>
122
+ </div>
123
+ </template>
124
+ </Card>
125
+ </div>
126
+ </div>
127
+ </template>
128
+
129
+ <script setup>
130
+ import { live, usePath, useClient } from '@live-change/vue3-ssr'
131
+ import { useRouter } from 'vue-router'
132
+ import { computed } from 'vue'
133
+ import { useI18n } from 'vue-i18n'
134
+ import Button from 'primevue/button'
135
+ import Card from 'primevue/card'
136
+
137
+ const router = useRouter()
138
+ const path = usePath()
139
+ const client = useClient()
140
+ const { t } = useI18n()
141
+
142
+ const accessibleCompaniesPath = computed(() =>
143
+ client.value.user
144
+ ? path.accessControl.myAccessibleObjects({ objectType: 'company_Company' })
145
+ .with(accessible =>
146
+ path.company.company({ company: accessible.object }).bind('company')
147
+ )
148
+ : null
149
+ )
150
+
151
+ const [accessibleCompanies] = await Promise.all([
152
+ live(accessibleCompaniesPath)
153
+ ])
154
+ </script>
155
+ ```
156
+
157
+ Key points:
158
+
159
+ - **Guard with `client.value.user`** so that unauthenticated users do not try to load the view.
160
+ - Pass `objectType: 'service_Model'`, e.g. `'company_Company'`.
161
+ - Use `.with()` to hydrate each access entry with the full entity (`accessible.object` → `company`).
162
+
163
+ ---
164
+
165
+ ## Step 3 – Paginated list with `myAccessesByObjectType` + `RangeViewer`
166
+
167
+ When you want an infinite scroll or very large lists, use the non-indexed range views and `<RangeViewer>` (see also `live-change-frontend-range-list` and `speed-dating/front/src/pages/events/index.vue`).
168
+
169
+ Example: “My events” with invitations and accepted accesses:
170
+
171
+ ```vue
172
+ <template>
173
+ <div class="w-full max-w-6xl">
174
+ <div class="w-full" v-if="anyInvitations?.length">
175
+ <div class="bg-surface-0 dark:bg-surface-900 rounded-border shadow-sm p-4 mt-4">
176
+ <div class="text-2xl font-medium text-surface-800 dark:text-surface-50">
177
+ {{ t('event.invitations.title') }}
178
+ </div>
179
+ </div>
180
+ <range-viewer :pathFunction="myEventsInvitationsPathRange" key="invitations"
181
+ :canLoadTop="false" canDropBottom
182
+ loadBottomSensorSize="4000px" dropBottomSensorSize="3000px">
183
+ <template #empty>
184
+ <div class="text-xl text-surface-800 dark:text-surface-50 my-4 mx-4">
185
+ {{ t('event.invitations.noEventsFound') }}
186
+ </div>
187
+ </template>
188
+ <template #default="{ item: invitation }">
189
+ <EventCard :event="invitation.event" class="my-2" />
190
+ </template>
191
+ </range-viewer>
192
+ </div>
193
+
194
+ <div class="w-full">
195
+ <div class="bg-surface-0 dark:bg-surface-900 rounded-border shadow-sm p-4 mt-4">
196
+ <div class="text-2xl font-medium text-surface-800 dark:text-surface-50">
197
+ {{ t('event.yourEvents') }}
198
+ </div>
199
+ </div>
200
+ <range-viewer :pathFunction="myEventsAccessesPathRange" key="myEvents"
201
+ :canLoadTop="false" canDropBottom
202
+ loadBottomSensorSize="4000px" dropBottomSensorSize="3000px">
203
+ <template #empty>
204
+ <div class="text-xl text-surface-800 dark:text-surface-50 my-4 mx-4">
205
+ {{ t('event.invitations.noEventsFound') }}
206
+ </div>
207
+ </template>
208
+ <template #default="{ item: access }">
209
+ <EventCard :event="access.event" class="my-2" />
210
+ </template>
211
+ </range-viewer>
212
+ </div>
213
+ </div>
214
+ </template>
215
+
216
+ <script setup>
217
+ import EventCard from '../../components/events/EventCard.vue'
218
+ import { RangeViewer } from '@live-change/vue3-components'
219
+
220
+ import { usePath, live, useClient, reverseRange } from '@live-change/vue3-ssr'
221
+ import { useI18n } from 'vue-i18n'
222
+
223
+ const path = usePath()
224
+ const client = useClient()
225
+ const { t } = useI18n()
226
+
227
+ function myEventsAccessesPathRange(range) {
228
+ return path.accessControl.myAccessesByObjectType({
229
+ objectType: 'speedDating_Event',
230
+ ...reverseRange(range)
231
+ }).with(access =>
232
+ path.speedDating.event({ event: access.object }).bind('event')
233
+ )
234
+ }
235
+
236
+ function myEventsInvitationsPathRange(range) {
237
+ return path.accessControl.myAccessInvitationsByObjectType({
238
+ objectType: 'speedDating_Event',
239
+ ...reverseRange(range)
240
+ }).with(invitation =>
241
+ path.speedDating.event({ event: invitation.object }).bind('event')
242
+ )
243
+ }
244
+
245
+ const anyInvitationsPath = myEventsInvitationsPathRange({ limit: 1 })
246
+
247
+ const [anyInvitations] = await Promise.all([
248
+ live(anyInvitationsPath)
249
+ ])
250
+ </script>
251
+ ```
252
+
253
+ Notes:
254
+
255
+ - `myAccessesByObjectType` returns access entries with `objectType`, `object`, `role`, etc.
256
+ - You hydrate each access with the real entity using `.with(...)`.
257
+ - `<RangeViewer>` takes a `pathFunction(range)` that must handle `gt` / `lt` / `limit` (here we use `reverseRange(range)` from `@live-change/vue3-ssr`).
258
+
259
+ ---
260
+
261
+ ## Step 4 – Add invitations next to accepted accesses
262
+
263
+ Invitations are separate records that share the same `objectType` / `object` fields. To show invitations together with accepted accesses:
264
+
265
+ 1. Use `myAccessInvitationsByObjectType` (and optionally `...ByObjectTypeAndRole`) for pending invites.
266
+ 2. Use `myAccessesByObjectType` (and optionally `...ByObjectTypeAndRole`) for accepted access.
267
+ 3. Display them in separate sections or merge in the UI.
268
+
269
+ Example outline:
270
+
271
+ ```js
272
+ function myObjectsAccessesPathRange(range) {
273
+ return path.accessControl.myAccessesByObjectType({
274
+ objectType: 'myService_MyEntity',
275
+ ...reverseRange(range)
276
+ }).with(access =>
277
+ path.myService.myEntity({ entity: access.object }).bind('entity')
278
+ )
279
+ }
280
+
281
+ function myObjectsInvitationsPathRange(range) {
282
+ return path.accessControl.myAccessInvitationsByObjectType({
283
+ objectType: 'myService_MyEntity',
284
+ ...reverseRange(range)
285
+ }).with(invitation =>
286
+ path.myService.myEntity({ entity: invitation.object }).bind('entity')
287
+ )
288
+ }
289
+ ```
290
+
291
+ Use two `<RangeViewer>` blocks or one section with two lists, depending on UX.
292
+
293
+ ---
294
+
295
+ ## Step 5 – Filter by role with `myAccessibleObjectsByRole`
296
+
297
+ When indexes are enabled, you can query only objects where the current user has a specific role (for example `owner`, `admin`, `member`).
298
+
299
+ Example:
300
+
301
+ ```js
302
+ import { computed } from 'vue'
303
+ import { usePath, useClient, live } from '@live-change/vue3-ssr'
304
+
305
+ const path = usePath()
306
+ const client = useClient()
307
+
308
+ const ownerProjectsPath = computed(() =>
309
+ client.value.user
310
+ ? path.accessControl.myAccessibleObjectsByRole({
311
+ role: 'owner',
312
+ objectType: 'project_Project'
313
+ }).with(accessible =>
314
+ path.project.project({ project: accessible.object }).bind('project')
315
+ )
316
+ : null
317
+ )
318
+
319
+ const [ownerProjects] = await Promise.all([
320
+ live(ownerProjectsPath)
321
+ ])
322
+ ```
323
+
324
+ Use this pattern for:
325
+
326
+ - “Projects I own” vs. “Projects where I am a member”.
327
+ - “Companies where I am accountant” vs. general access.
328
+
329
+ ---
330
+
331
+ ## Step 6 – Admin tools with `accessibleObjects` and `objectAccesses`
332
+
333
+ For administrative UIs you may need to:
334
+
335
+ - list all objects accessible to a given user,
336
+ - list all users who have access to a specific object and their roles.
337
+
338
+ Use these views (available when `indexed: true`):
339
+
340
+ - `accessibleObjects({ sessionOrUserType, sessionOrUser, objectType?, ...range })`
341
+ - `accessibleObjectsByRole({ sessionOrUserType, sessionOrUser, role, objectType?, ...range })`
342
+ - `objectAccesses({ objectType, object, role?, ...range })`
343
+
344
+ Important:
345
+
346
+ - These views require **admin** role (checked in `indexes.js`).
347
+ - `sessionOrUserType` is `'user_User'` for users or `'session_Session'` for anonymous sessions.
348
+ - `sessionOrUser` is the user id or session id.
349
+
350
+ Example skeleton for an admin panel:
351
+
352
+ ```js
353
+ const usersProjectsPath = computed(() =>
354
+ client.value.roles.includes('admin')
355
+ ? path.accessControl.accessibleObjectsByRole({
356
+ sessionOrUserType: 'user_User',
357
+ sessionOrUser: inspectedUserId,
358
+ role: 'member',
359
+ objectType: 'project_Project'
360
+ }).with(accessible =>
361
+ path.project.project({ project: accessible.object }).bind('project')
362
+ )
363
+ : null
364
+ )
365
+ ```
366
+
367
+ ---
368
+
369
+ ## Summary
370
+
371
+ | Need | View | Notes |
372
+ |---|---|---|
373
+ | All entities of one type I can access | `myAccessibleObjects({ objectType })` | Requires `indexed: true`, simplest pattern |
374
+ | All entities where I have specific role | `myAccessibleObjectsByRole({ role, objectType? })` | Use for “owner”, “member”, etc. |
375
+ | Paginated “my X” list without indexes | `myAccessesByObjectType` + `<RangeViewer>` | Always available, hydrate with `.with()` |
376
+ | Pending invitations | `myAccessInvitationsByObjectType` | Often shown in a separate section |
377
+ | Admin: what objects a user can access | `accessibleObjects` / `accessibleObjectsByRole` | Requires admin role, explicit user id |
378
+ | Admin: who has access to object | `objectAccesses` | Good for audit / sharing dialogs |
379
+
380
+ Remember:
381
+
382
+ - `objectType` is always `service_Model`.
383
+ - For relations like `userItem`, use relation-based views instead; this skill is for **access-control based entities**.
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: live-change-frontend-action-buttons
3
+ description: Build async action buttons with workingZone, toast and confirm dialogs
4
+ ---
5
+
6
+ # Skill: live-change-frontend-action-buttons (Claude Code)
7
+
8
+ Use this skill when you build **buttons that trigger async actions** outside of forms, using `workingZone`, `toast`, and optionally `confirm`.
9
+
10
+ ## When to use
11
+
12
+ - A button triggers a backend action (delete, approve, toggle, etc.) — **no form fields**.
13
+ - You want the global loading spinner to appear while the action runs.
14
+ - Destructive actions need a confirmation dialog before executing.
15
+
16
+ **Need a form with fields?** Use `editorData` (model editing) or `actionData` (one-shot actions) instead.
17
+
18
+ ## Step 1 – Inject workingZone, set up toast/confirm
19
+
20
+ ```javascript
21
+ import { inject } from 'vue'
22
+ import { useToast } from 'primevue/usetoast'
23
+ import { useConfirm } from 'primevue/useconfirm'
24
+ import { useApi, useActions } from '@live-change/vue3-ssr'
25
+
26
+ const workingZone = inject('workingZone')
27
+ const toast = useToast()
28
+ const confirm = useConfirm()
29
+ const api = useApi()
30
+ const actions = useActions()
31
+ ```
32
+
33
+ `workingZone` is provided by `ViewRoot` (which wraps every page in `<WorkingZone>`). When you call `workingZone.addPromise(name, promise)`, the global spinner/blur activates until the promise resolves.
34
+
35
+ ## Step 2 – Simple action button
36
+
37
+ Wrap the async operation in `workingZone.addPromise()`:
38
+
39
+ ```javascript
40
+ function createItem() {
41
+ workingZone.addPromise('createItem', (async () => {
42
+ const result = await actions.blog.createArticle({})
43
+ toast.add({ severity: 'success', summary: 'Article created', life: 2000 })
44
+ router.push({ name: 'article:edit', params: { article: result } })
45
+ })())
46
+ }
47
+ ```
48
+
49
+ **Important:** Note the `(async () => { ... })()` pattern – you must invoke the async IIFE immediately so `addPromise` receives a Promise, not a function.
50
+
51
+ Template:
52
+
53
+ ```vue
54
+ <Button label="Create article" icon="pi pi-plus" @click="createItem" />
55
+ ```
56
+
57
+ ## Step 3 – Destructive action with confirm
58
+
59
+ Use `confirm.require()` before the action:
60
+
61
+ ```javascript
62
+ function deleteItem(item) {
63
+ confirm.require({
64
+ message: 'Are you sure you want to delete this article?',
65
+ header: 'Confirmation',
66
+ icon: 'pi pi-trash',
67
+ acceptClass: 'p-button-danger',
68
+ accept: async () => {
69
+ workingZone.addPromise('deleteArticle', (async () => {
70
+ await actions.blog.deleteArticle({ article: item.id })
71
+ toast.add({ severity: 'success', summary: 'Deleted', life: 2000 })
72
+ })())
73
+ },
74
+ reject: () => {
75
+ toast.add({ severity: 'info', summary: 'Cancelled', life: 1500 })
76
+ }
77
+ })
78
+ }
79
+ ```
80
+
81
+ Template:
82
+
83
+ ```vue
84
+ <Button label="Delete" icon="pi pi-trash" severity="danger" @click="deleteItem(article)" />
85
+ ```
86
+
87
+ ## Step 4 – Error handling
88
+
89
+ Add try/catch inside the async IIFE:
90
+
91
+ ```javascript
92
+ function toggleStatus(item) {
93
+ workingZone.addPromise('toggleStatus', (async () => {
94
+ try {
95
+ await actions.blog.toggleArticleStatus({ article: item.id })
96
+ toast.add({ severity: 'success', summary: 'Status updated', life: 2000 })
97
+ } catch(e) {
98
+ toast.add({ severity: 'error', summary: 'Error', detail: e?.message ?? e, life: 5000 })
99
+ }
100
+ })())
101
+ }
102
+ ```
103
+
104
+ ## Step 5 – Using api.command instead of actions
105
+
106
+ Both work. `actions` is shorthand for typed service actions:
107
+
108
+ ```javascript
109
+ // Using actions (preferred when available):
110
+ await actions.blog.deleteArticle({ article: id })
111
+
112
+ // Using api.command (always works):
113
+ await api.command(['blog', 'deleteArticle'], { article: id })
114
+ ```
115
+
116
+ ## Pattern summary
117
+
118
+ ```
119
+ Button click
120
+ → confirm.require() (if destructive)
121
+ → workingZone.addPromise('name', (async () => {
122
+ try {
123
+ await actions.service.action({ ... })
124
+ toast.add({ severity: 'success', ... })
125
+ } catch(e) {
126
+ toast.add({ severity: 'error', ... })
127
+ }
128
+ })())
129
+ ```
@@ -0,0 +1,149 @@
1
+ ---
2
+ name: live-change-frontend-action-form
3
+ description: Build one-shot action forms with actionData, AutoField and ActionButtons
4
+ ---
5
+
6
+ # Skill: live-change-frontend-action-form (Claude Code)
7
+
8
+ Use this skill when you build **one-shot action forms** with `actionData` and `AutoField` in a LiveChange frontend.
9
+
10
+ ## When to use
11
+
12
+ - You need a form for a command/action (not CRUD model editing).
13
+ - The form submits once, then shows a "done" state.
14
+ - You want draft auto-save while the user fills in the form.
15
+
16
+ **Editing a model record instead?** Use `editorData` (see `live-change-frontend-editor-form` skill).
17
+ **No form fields, just a button?** Use `api.command` (see `live-change-frontend-command-forms` skill).
18
+
19
+ ## Step 1 – Set up actionData
20
+
21
+ ```javascript
22
+ import { AutoField, actionData, ActionButtons } from '@live-change/frontend-auto-form'
23
+
24
+ const formData = await actionData({
25
+ service: 'blog',
26
+ action: 'publishArticle',
27
+ parameters: { article: props.articleId }, // fixed params (not shown as fields)
28
+ initialValue: { scheduleTime: null }, // initial values for editable fields
29
+ draft: true,
30
+ doneToast: 'Article published!',
31
+ onDone: (result) => {
32
+ router.push({ name: 'articles' })
33
+ },
34
+ })
35
+ ```
36
+
37
+ For reactive parameters, use `computedAsync`:
38
+
39
+ ```javascript
40
+ import { computedAsync } from '@vueuse/core'
41
+
42
+ const formData = computedAsync(() =>
43
+ actionData({
44
+ service: 'blog',
45
+ action: 'publishArticle',
46
+ parameters: { article: props.articleId },
47
+ })
48
+ )
49
+ ```
50
+
51
+ ## Step 2 – Build the template with AutoField
52
+
53
+ **Every field must show validation errors.** Always pass `:error="formData.propertiesErrors?.fieldName"` to AutoField — or use a manual `Message` if not using AutoField (see `live-change-frontend-editor-form` skill for all 3 approaches).
54
+
55
+ **Always wrap in `<form>`** with `@submit.prevent` and `@reset.prevent` handlers. `ActionButtons` uses `type="submit"` / `type="reset"` internally — without a parent `<form>`, the buttons do nothing.
56
+
57
+ Use `formData.action.properties.*` as definitions and `formData.value.*` as v-model:
58
+
59
+ ```vue
60
+ <template>
61
+ <form @submit.prevent="formData.submit()" @reset.prevent="formData.reset()">
62
+ <AutoField
63
+ :definition="formData.action.properties.scheduleTime"
64
+ v-model="formData.data.value.scheduleTime"
65
+ :error="formData.propertiesErrors?.scheduleTime"
66
+ label="Schedule time"
67
+ />
68
+ <AutoField
69
+ :definition="formData.action.properties.message"
70
+ v-model="formData.data.value.message"
71
+ :error="formData.propertiesErrors?.message"
72
+ label="Message"
73
+ />
74
+
75
+ <ActionButtons :actionFormData="formData" :resetButton="true" />
76
+ </form>
77
+ </template>
78
+ ```
79
+
80
+ ## Step 3 – Handle done state
81
+
82
+ After successful submission, `formData.done` becomes `true`:
83
+
84
+ ```vue
85
+ <template>
86
+ <div v-if="formData.done" class="text-center">
87
+ <i class="pi pi-check-circle text-4xl text-green-500 mb-2"></i>
88
+ <p>Article published successfully!</p>
89
+ </div>
90
+ <form v-else @submit.prevent="formData.submit()" @reset.prevent="formData.reset()">
91
+ <!-- fields + ActionButtons -->
92
+ </form>
93
+ </template>
94
+ ```
95
+
96
+ ## Step 4 – Manual buttons (alternative to ActionButtons)
97
+
98
+ ```vue
99
+ <div class="flex gap-2 justify-end">
100
+ <Button
101
+ type="submit"
102
+ :label="formData.submitting ? 'Executing...' : 'Execute'"
103
+ :icon="formData.submitting ? 'pi pi-spin pi-spinner' : 'pi pi-play'"
104
+ :disabled="formData.submitting"
105
+ />
106
+ <Button type="reset" label="Reset" :disabled="!formData.changed" icon="pi pi-eraser" />
107
+ </div>
108
+ ```
109
+
110
+ ## editorData vs actionData
111
+
112
+ | Aspect | `editorData` | `actionData` |
113
+ |---|---|---|
114
+ | Purpose | CRUD model editing | One-shot command form |
115
+ | Identifier | `model` + `identifiers` | `action` + `parameters` |
116
+ | Create/update | Detects automatically | Always "execute" |
117
+ | After submit | Record is saved | `done` becomes `true` |
118
+ | Definition source | `editor.model` | `formData.action` |
119
+ | `parameters` | Extra params on save | Fixed fields excluded from form |
120
+
121
+ ## Key options
122
+
123
+ | Option | Default | Description |
124
+ |---|---|---|
125
+ | `service` | required | Service name |
126
+ | `action` | required | Action name |
127
+ | `parameters` | `{}` | Fixed identifier fields (not editable) |
128
+ | `initialValue` | `{}` | Initial values for editable fields |
129
+ | `draft` | `true` | Auto-save draft while filling |
130
+ | `debounce` | `600` | Debounce delay in ms |
131
+ | `doneToast` | `"Action done"` | Toast after success |
132
+ | `onDone` | – | Callback after success |
133
+
134
+ ## Key returned properties
135
+
136
+ | Property | Description |
137
+ |---|---|
138
+ | `data` | Editable form data (recommended), it is a ref, it should be used with value - editor.data.value |
139
+ | `value` | Editable form data (obsolete), it is a ref, it should be used with value - editor.value.value |
140
+ | `action` | Action definition (`.properties.*` for `AutoField`) |
141
+ | `editableProperties` | Properties not fixed by `parameters` |
142
+ | `changed` | Form differs from initial value |
143
+ | `submit()` | Execute the action |
144
+ | `submitting` | Action call in progress |
145
+ | `done` | `true` after success |
146
+ | `reset()` | Discard draft, restore initial value |
147
+ | `propertiesErrors` | Server validation errors per property |
148
+ | `draftChanged` | Draft auto-saved but not submitted |
149
+ | `savingDraft` | Draft auto-save in progress |