@live-change/frontend-template 0.9.197 → 0.9.199
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/.claude/rules/live-change-backend-actions-views-triggers.md +184 -0
- package/.claude/rules/live-change-backend-architecture.md +126 -0
- package/.claude/rules/live-change-backend-models-and-relations.md +188 -0
- package/.claude/rules/live-change-frontend-vue-primevue.md +291 -0
- package/.claude/rules/live-change-service-structure.md +89 -0
- package/.claude/skills/create-skills-and-rules.md +196 -0
- package/.claude/skills/live-change-design-actions-views-triggers.md +190 -0
- package/.claude/skills/live-change-design-models-relations.md +173 -0
- package/.claude/skills/live-change-design-service.md +132 -0
- package/.claude/skills/live-change-frontend-action-buttons.md +128 -0
- package/.claude/skills/live-change-frontend-action-form.md +143 -0
- package/.claude/skills/live-change-frontend-analytics.md +146 -0
- package/.claude/skills/live-change-frontend-command-forms.md +215 -0
- package/.claude/skills/live-change-frontend-data-views.md +182 -0
- package/.claude/skills/live-change-frontend-editor-form.md +177 -0
- package/.claude/skills/live-change-frontend-locale-time.md +171 -0
- package/.claude/skills/live-change-frontend-page-list-detail.md +200 -0
- package/.claude/skills/live-change-frontend-range-list.md +128 -0
- package/.claude/skills/live-change-frontend-ssr-setup.md +118 -0
- package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +202 -0
- package/.cursor/rules/live-change-backend-architecture.mdc +131 -0
- package/.cursor/rules/live-change-backend-models-and-relations.mdc +194 -0
- package/.cursor/rules/live-change-frontend-vue-primevue.mdc +290 -0
- package/.cursor/rules/live-change-service-structure.mdc +107 -0
- package/.cursor/skills/live-change-design-actions-views-triggers.md +197 -0
- package/.cursor/skills/live-change-design-models-relations.md +168 -0
- package/.cursor/skills/live-change-design-service.md +75 -0
- package/.cursor/skills/live-change-frontend-action-buttons.md +128 -0
- package/.cursor/skills/live-change-frontend-action-form.md +143 -0
- package/.cursor/skills/live-change-frontend-analytics.md +146 -0
- package/.cursor/skills/live-change-frontend-command-forms.md +215 -0
- package/.cursor/skills/live-change-frontend-data-views.md +182 -0
- package/.cursor/skills/live-change-frontend-editor-form.md +177 -0
- package/.cursor/skills/live-change-frontend-locale-time.md +171 -0
- package/.cursor/skills/live-change-frontend-page-list-detail.md +200 -0
- package/.cursor/skills/live-change-frontend-range-list.md +128 -0
- package/.cursor/skills/live-change-frontend-ssr-setup.md +119 -0
- package/README.md +71 -0
- package/package.json +50 -50
- package/server/app.config.js +35 -0
- package/server/services.list.js +2 -0
- package/.nx/workspace-data/file-map.json +0 -195
- package/.nx/workspace-data/nx_files.nxt +0 -0
- package/.nx/workspace-data/project-graph.json +0 -8
- package/.nx/workspace-data/project-graph.lock +0 -0
- package/.nx/workspace-data/source-maps.json +0 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Design models with userItem, itemOf, propertyOf relations and access control
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Skill: live-change-design-models-relations
|
|
6
|
+
|
|
7
|
+
Ten skill opisuje **krok po kroku**, jak projektować modele i relacje (`userItem`, `itemOf`, `propertyOf`, `foreignModel`) w serwisach LiveChange.
|
|
8
|
+
|
|
9
|
+
## Kiedy używać
|
|
10
|
+
|
|
11
|
+
Użyj tego skilla, gdy:
|
|
12
|
+
|
|
13
|
+
- dodajesz nowy model do serwisu,
|
|
14
|
+
- przenosisz dane z ręcznego CRUD na relacje,
|
|
15
|
+
- potrzebujesz spójnego wzorca dla access control i indeksów.
|
|
16
|
+
|
|
17
|
+
## 1. Dobierz typ relacji
|
|
18
|
+
|
|
19
|
+
Zastanów się, jak model jest powiązany z resztą domeny:
|
|
20
|
+
|
|
21
|
+
- **`userItem`** – obiekt należy do zalogowanego użytkownika (np. konto, urządzenie użytkownika).
|
|
22
|
+
- **`itemOf`** – lista elementów należy do innego modelu (np. połączenia urządzenia, pozycje zamówienia).
|
|
23
|
+
- **`propertyOf`** – pojedyncza właściwość/model ze stanem o ID równym rodzicowi (np. stan kursora, ustawienia).
|
|
24
|
+
- **bez relacji** – gdy obiekt jest globalny lub ma inną strukturę (np. globalny config).
|
|
25
|
+
|
|
26
|
+
Wybierz jedną główną relację na model – dodatkowe powiązania możesz odzwierciedlić polami i indeksami.
|
|
27
|
+
|
|
28
|
+
## 2. Zdefiniuj `properties` w czytelny sposób
|
|
29
|
+
|
|
30
|
+
1. Utwórz sekcję `properties`:
|
|
31
|
+
- każdą właściwość zapisuj wieloliniowo,
|
|
32
|
+
- jasno określ typ, domyślne wartości, walidację.
|
|
33
|
+
|
|
34
|
+
2. Przykład:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
properties: {
|
|
38
|
+
name: {
|
|
39
|
+
type: String,
|
|
40
|
+
validation: ['nonEmpty']
|
|
41
|
+
},
|
|
42
|
+
status: {
|
|
43
|
+
type: String,
|
|
44
|
+
default: 'offline'
|
|
45
|
+
},
|
|
46
|
+
capabilities: {
|
|
47
|
+
type: Array,
|
|
48
|
+
of: {
|
|
49
|
+
type: String
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 3. Skonfiguruj relację
|
|
56
|
+
|
|
57
|
+
### `userItem`
|
|
58
|
+
|
|
59
|
+
1. Dodaj blok `userItem` w definicji modelu.
|
|
60
|
+
2. Ustaw role dla odczytu i zapisu.
|
|
61
|
+
3. Ogranicz `writeableProperties` do pól, które użytkownik może zmieniać.
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
userItem: {
|
|
65
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
66
|
+
writeAccessControl: { roles: ['owner', 'admin'] },
|
|
67
|
+
writeableProperties: ['name']
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `itemOf`
|
|
72
|
+
|
|
73
|
+
1. Ustal model rodzica (`what`).
|
|
74
|
+
2. W razie potrzeby użyj `definition.foreignModel`, jeśli rodzic jest w innym serwisie.
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
itemOf: {
|
|
78
|
+
what: Device,
|
|
79
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
80
|
+
writeAccessControl: { roles: ['owner', 'admin'] }
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### `propertyOf`
|
|
85
|
+
|
|
86
|
+
1. Użyj, gdy chcesz, żeby ID modelu = ID rodzica.
|
|
87
|
+
2. To ułatwia odczyt (`Model.get(parentId)`).
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
propertyOf: {
|
|
91
|
+
what: Device,
|
|
92
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
93
|
+
writeAccessControl: { roles: ['owner', 'admin'] }
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `propertyOf` z wieloma rodzicami (1:1 relacja do wielu encji)
|
|
98
|
+
|
|
99
|
+
Użyj, gdy model ma być „łącznikiem 1:1” pomiędzy kilkoma encjami (np. faktura ↔ kontrahent w konkretnej roli),
|
|
100
|
+
żeby generator relacji/CRUD rozumiał, że to jest relacja, a nie pole `someId` w `properties`.
|
|
101
|
+
|
|
102
|
+
Uwagi:
|
|
103
|
+
|
|
104
|
+
- Najczęściej jest to 1 lub 2 rodziców, ale lista `propertyOf` może zawierać **dowolnie wiele** modeli (np. relacja łącząca 3+ encje).
|
|
105
|
+
- Jeśli encja jest relacją, **nie dodawaj ręcznie** pól typu `...Id` w `properties` tylko po to, żeby “zrobić join” – generator CRUD nie potraktuje tego jako relacji.
|
|
106
|
+
|
|
107
|
+
Przykład:
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
const CostInvoice = definition.foreignModel('invoice', 'CostInvoice')
|
|
111
|
+
const Contractor = definition.foreignModel('company', 'Contractor')
|
|
112
|
+
|
|
113
|
+
definition.model({
|
|
114
|
+
name: 'Supplier',
|
|
115
|
+
properties: {
|
|
116
|
+
// opcjonalne pola dodatkowe
|
|
117
|
+
},
|
|
118
|
+
propertyOf: [
|
|
119
|
+
{ what: CostInvoice },
|
|
120
|
+
{ what: Contractor }
|
|
121
|
+
]
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### `foreignModel`
|
|
126
|
+
|
|
127
|
+
1. Na początku pliku z modelem zadeklaruj:
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
const Device = definition.foreignModel('deviceManager', 'Device')
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
2. Potem używaj `Device` w `itemOf`/`propertyOf`.
|
|
134
|
+
|
|
135
|
+
## 4. Dodaj indeksy
|
|
136
|
+
|
|
137
|
+
1. Zidentyfikuj typowe zapytania (np. po `sessionKey`, po `(device, status)`).
|
|
138
|
+
2. Dodaj sekcję `indexes`:
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
indexes: {
|
|
142
|
+
bySessionKey: {
|
|
143
|
+
property: ['sessionKey']
|
|
144
|
+
},
|
|
145
|
+
byDeviceAndStatus: {
|
|
146
|
+
property: ['device', 'status']
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
3. Używaj tych indeksów w widokach i akcjach (`indexObjectGet`, `indexRangeGet`).
|
|
152
|
+
|
|
153
|
+
## 5. Ustal access control na poziomie modelu/relacji
|
|
154
|
+
|
|
155
|
+
1. Dla `userItem` / `itemOf` / `propertyOf` ustaw zawsze:
|
|
156
|
+
- `readAccessControl`,
|
|
157
|
+
- `writeAccessControl`.
|
|
158
|
+
|
|
159
|
+
2. Nie zakładaj domyślnych reguł – wpisz je wprost w definicji modelu.
|
|
160
|
+
|
|
161
|
+
## 6. Sprawdź wygenerowane widoki/akcje
|
|
162
|
+
|
|
163
|
+
Po dodaniu relacji:
|
|
164
|
+
|
|
165
|
+
1. Sprawdź, jakie widoki/akcje generuje plugin (np. `myUserDevices`, `createMyUserDevice`, itp.).
|
|
166
|
+
2. Zastanów się, czy potrzebujesz dodatkowych, niestandardowych widoków/akcji:
|
|
167
|
+
- jeśli tak, dodaj je, ale **nie duplikuj** tego, co generuje relacja.
|
|
168
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Create or restructure a LiveChange backend service with proper directory layout
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Skill: live-change-design-service
|
|
6
|
+
|
|
7
|
+
Ten skill opisuje **krok po kroku**, jak zaprojektować nowy serwis w LiveChange / live-change-stack albo sensownie rozbudować istniejący.
|
|
8
|
+
|
|
9
|
+
## Kiedy używać
|
|
10
|
+
|
|
11
|
+
Użyj tego skilla, gdy:
|
|
12
|
+
|
|
13
|
+
- dodajesz **nowy serwis domenowy** do projektu,
|
|
14
|
+
- przenosisz większy kawałek logiki z innego serwisu,
|
|
15
|
+
- potrzebujesz upewnić się, że struktura plików i rejestracja serwisu są zgodne z konwencją.
|
|
16
|
+
|
|
17
|
+
## Kroki – nowy serwis
|
|
18
|
+
|
|
19
|
+
1. **Nazwij serwis**
|
|
20
|
+
- Wybierz zwięzłą, domenową nazwę, np. `payments`, `notifications`, `deviceManager`.
|
|
21
|
+
- Nazwa będzie używana jako `name` w `createServiceDefinition` oraz w `app.config.js`.
|
|
22
|
+
|
|
23
|
+
2. **Utwórz katalog serwisu**
|
|
24
|
+
- Ścieżka: `server/services/<serviceName>/`.
|
|
25
|
+
- W katalogu utwórz pliki:
|
|
26
|
+
- `definition.js`
|
|
27
|
+
- `index.js`
|
|
28
|
+
- opcjonalnie `config.js`
|
|
29
|
+
- pliki domenowe (np. `models.js`, `authenticator.js`, `actions.js`), jeśli potrzebne.
|
|
30
|
+
|
|
31
|
+
3. **Zaimplementuj `definition.js`**
|
|
32
|
+
- Importuj `app` z `@live-change/framework`.
|
|
33
|
+
- Jeśli serwis korzysta z relacji lub access control:
|
|
34
|
+
- importuj `relationsPlugin` z `@live-change/relations-plugin`,
|
|
35
|
+
- importuj `accessControlService` z `@live-change/access-control-service`.
|
|
36
|
+
- Wywołaj `app.createServiceDefinition({ name, use })`.
|
|
37
|
+
- **Nie** deklaruj tu modeli, akcji ani widoków.
|
|
38
|
+
|
|
39
|
+
4. **Zaimplementuj `index.js`**
|
|
40
|
+
- Importuj `definition` z `./definition.js`.
|
|
41
|
+
- Importuj wszystkie pliki domenowe (np. `./models.js`, `./authenticator.js`).
|
|
42
|
+
- Eksportuj `definition` jako `default`.
|
|
43
|
+
- Nie dodawaj innej logiki do `index.js`.
|
|
44
|
+
|
|
45
|
+
5. **(Opcjonalnie) utwórz `config.js`**
|
|
46
|
+
- Importuj `definition`.
|
|
47
|
+
- Odczytaj `definition.config` i rozwiąż wartości domyślne.
|
|
48
|
+
- Eksportuj plain object z konfiguracją serwisu.
|
|
49
|
+
|
|
50
|
+
6. **Dodaj serwis do `services.list.js`**
|
|
51
|
+
- Importuj z katalogu serwisu, nie z pojedynczego pliku.
|
|
52
|
+
- Dodaj serwis do eksportowanego obiektu.
|
|
53
|
+
|
|
54
|
+
7. **Dodaj serwis do `app.config.js`**
|
|
55
|
+
- W sekcji `services` dodaj `{ name: '<serviceName>' }`.
|
|
56
|
+
- Upewnij się, że kolejność jest sensowna:
|
|
57
|
+
- serwisy bazowe/plugins (user, session, accessControl) na początku,
|
|
58
|
+
- serwisy domenowe zależne od nich – dalej.
|
|
59
|
+
|
|
60
|
+
8. **Sprawdź zależności**
|
|
61
|
+
- Jeśli serwis korzysta z modeli w innych serwisach:
|
|
62
|
+
- użyj `definition.foreignModel` wewnątrz domenowych plików serwisu,
|
|
63
|
+
- nie importuj bezpośrednio ich plików modeli.
|
|
64
|
+
- Upewnij się, że serwisy, od których zależysz, są wcześniejsze w `app.config.js`.
|
|
65
|
+
|
|
66
|
+
## Kroki – rozbudowa istniejącego serwisu
|
|
67
|
+
|
|
68
|
+
1. Przejrzyj istniejący katalog `server/services/<serviceName>/`.
|
|
69
|
+
2. Sprawdź, czy `definition.js` ma poprawne `use` (relacje, accessControl).
|
|
70
|
+
3. Nowe modele/akcje/widoki/triggery dodaj do **osobnych plików domenowych**:
|
|
71
|
+
- jeśli logika jest powiązana z istniejącym modelem – do jego pliku,
|
|
72
|
+
- jeśli tworzysz większy nowy obszar – do nowego pliku (np. `notifications.js`).
|
|
73
|
+
4. Upewnij się, że nowy plik jest importowany w `index.js`.
|
|
74
|
+
5. Nie dodawaj ciężkiej logiki do `definition.js` ani `index.js`.
|
|
75
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Build async action buttons with workingZone, toast and confirm dialogs
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Skill: live-change-frontend-action-buttons (Claude Code)
|
|
6
|
+
|
|
7
|
+
Use this skill when you build **buttons that trigger async actions** outside of forms, using `workingZone`, `toast`, and optionally `confirm`.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- A button triggers a backend action (delete, approve, toggle, etc.) — **no form fields**.
|
|
12
|
+
- You want the global loading spinner to appear while the action runs.
|
|
13
|
+
- Destructive actions need a confirmation dialog before executing.
|
|
14
|
+
|
|
15
|
+
**Need a form with fields?** Use `editorData` (model editing) or `actionData` (one-shot actions) instead.
|
|
16
|
+
|
|
17
|
+
## Step 1 – Inject workingZone, set up toast/confirm
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
import { inject } from 'vue'
|
|
21
|
+
import { useToast } from 'primevue/usetoast'
|
|
22
|
+
import { useConfirm } from 'primevue/useconfirm'
|
|
23
|
+
import { useApi, useActions } from '@live-change/vue3-ssr'
|
|
24
|
+
|
|
25
|
+
const workingZone = inject('workingZone')
|
|
26
|
+
const toast = useToast()
|
|
27
|
+
const confirm = useConfirm()
|
|
28
|
+
const api = useApi()
|
|
29
|
+
const actions = useActions()
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`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.
|
|
33
|
+
|
|
34
|
+
## Step 2 – Simple action button
|
|
35
|
+
|
|
36
|
+
Wrap the async operation in `workingZone.addPromise()`:
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
function createItem() {
|
|
40
|
+
workingZone.addPromise('createItem', (async () => {
|
|
41
|
+
const result = await actions.blog.createArticle({})
|
|
42
|
+
toast.add({ severity: 'success', summary: 'Article created', life: 2000 })
|
|
43
|
+
router.push({ name: 'article:edit', params: { article: result } })
|
|
44
|
+
})())
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Important:** Note the `(async () => { ... })()` pattern – you must invoke the async IIFE immediately so `addPromise` receives a Promise, not a function.
|
|
49
|
+
|
|
50
|
+
Template:
|
|
51
|
+
|
|
52
|
+
```vue
|
|
53
|
+
<Button label="Create article" icon="pi pi-plus" @click="createItem" />
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Step 3 – Destructive action with confirm
|
|
57
|
+
|
|
58
|
+
Use `confirm.require()` before the action:
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
function deleteItem(item) {
|
|
62
|
+
confirm.require({
|
|
63
|
+
message: 'Are you sure you want to delete this article?',
|
|
64
|
+
header: 'Confirmation',
|
|
65
|
+
icon: 'pi pi-trash',
|
|
66
|
+
acceptClass: 'p-button-danger',
|
|
67
|
+
accept: async () => {
|
|
68
|
+
workingZone.addPromise('deleteArticle', (async () => {
|
|
69
|
+
await actions.blog.deleteArticle({ article: item.id })
|
|
70
|
+
toast.add({ severity: 'success', summary: 'Deleted', life: 2000 })
|
|
71
|
+
})())
|
|
72
|
+
},
|
|
73
|
+
reject: () => {
|
|
74
|
+
toast.add({ severity: 'info', summary: 'Cancelled', life: 1500 })
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Template:
|
|
81
|
+
|
|
82
|
+
```vue
|
|
83
|
+
<Button label="Delete" icon="pi pi-trash" severity="danger" @click="deleteItem(article)" />
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Step 4 – Error handling
|
|
87
|
+
|
|
88
|
+
Add try/catch inside the async IIFE:
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
function toggleStatus(item) {
|
|
92
|
+
workingZone.addPromise('toggleStatus', (async () => {
|
|
93
|
+
try {
|
|
94
|
+
await actions.blog.toggleArticleStatus({ article: item.id })
|
|
95
|
+
toast.add({ severity: 'success', summary: 'Status updated', life: 2000 })
|
|
96
|
+
} catch(e) {
|
|
97
|
+
toast.add({ severity: 'error', summary: 'Error', detail: e?.message ?? e, life: 5000 })
|
|
98
|
+
}
|
|
99
|
+
})())
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Step 5 – Using api.command instead of actions
|
|
104
|
+
|
|
105
|
+
Both work. `actions` is shorthand for typed service actions:
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
// Using actions (preferred when available):
|
|
109
|
+
await actions.blog.deleteArticle({ article: id })
|
|
110
|
+
|
|
111
|
+
// Using api.command (always works):
|
|
112
|
+
await api.command(['blog', 'deleteArticle'], { article: id })
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Pattern summary
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
Button click
|
|
119
|
+
→ confirm.require() (if destructive)
|
|
120
|
+
→ workingZone.addPromise('name', (async () => {
|
|
121
|
+
try {
|
|
122
|
+
await actions.service.action({ ... })
|
|
123
|
+
toast.add({ severity: 'success', ... })
|
|
124
|
+
} catch(e) {
|
|
125
|
+
toast.add({ severity: 'error', ... })
|
|
126
|
+
}
|
|
127
|
+
})())
|
|
128
|
+
```
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Build one-shot action forms with actionData, AutoField and ActionButtons
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Skill: live-change-frontend-action-form (Claude Code)
|
|
6
|
+
|
|
7
|
+
Use this skill when you build **one-shot action forms** with `actionData` and `AutoField` in a LiveChange frontend.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- You need a form for a command/action (not CRUD model editing).
|
|
12
|
+
- The form submits once, then shows a "done" state.
|
|
13
|
+
- You want draft auto-save while the user fills in the form.
|
|
14
|
+
|
|
15
|
+
**Editing a model record instead?** Use `editorData` (see `live-change-frontend-editor-form` skill).
|
|
16
|
+
**No form fields, just a button?** Use `api.command` (see `live-change-frontend-command-forms` skill).
|
|
17
|
+
|
|
18
|
+
## Step 1 – Set up actionData
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
import { AutoField, actionData, ActionButtons } from '@live-change/frontend-auto-form'
|
|
22
|
+
|
|
23
|
+
const formData = await actionData({
|
|
24
|
+
service: 'blog',
|
|
25
|
+
action: 'publishArticle',
|
|
26
|
+
parameters: { article: props.articleId }, // fixed params (not shown as fields)
|
|
27
|
+
initialValue: { scheduleTime: null }, // initial values for editable fields
|
|
28
|
+
draft: true,
|
|
29
|
+
doneToast: 'Article published!',
|
|
30
|
+
onDone: (result) => {
|
|
31
|
+
router.push({ name: 'articles' })
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For reactive parameters, use `computedAsync`:
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
import { computedAsync } from '@vueuse/core'
|
|
40
|
+
|
|
41
|
+
const formData = computedAsync(() =>
|
|
42
|
+
actionData({
|
|
43
|
+
service: 'blog',
|
|
44
|
+
action: 'publishArticle',
|
|
45
|
+
parameters: { article: props.articleId },
|
|
46
|
+
})
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Step 2 – Build the template with AutoField
|
|
51
|
+
|
|
52
|
+
Use `formData.action.properties.*` as definitions and `formData.value.*` as v-model:
|
|
53
|
+
|
|
54
|
+
```vue
|
|
55
|
+
<template>
|
|
56
|
+
<form @submit.prevent="formData.submit()" @reset.prevent="formData.reset()">
|
|
57
|
+
<AutoField
|
|
58
|
+
:definition="formData.action.properties.scheduleTime"
|
|
59
|
+
v-model="formData.value.scheduleTime"
|
|
60
|
+
:error="formData.propertiesErrors?.scheduleTime"
|
|
61
|
+
label="Schedule time"
|
|
62
|
+
/>
|
|
63
|
+
<AutoField
|
|
64
|
+
:definition="formData.action.properties.message"
|
|
65
|
+
v-model="formData.value.message"
|
|
66
|
+
:error="formData.propertiesErrors?.message"
|
|
67
|
+
label="Message"
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
<ActionButtons :actionFormData="formData" :resetButton="true" />
|
|
71
|
+
</form>
|
|
72
|
+
</template>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Step 3 – Handle done state
|
|
76
|
+
|
|
77
|
+
After successful submission, `formData.done` becomes `true`:
|
|
78
|
+
|
|
79
|
+
```vue
|
|
80
|
+
<template>
|
|
81
|
+
<div v-if="formData.done" class="text-center">
|
|
82
|
+
<i class="pi pi-check-circle text-4xl text-green-500 mb-2"></i>
|
|
83
|
+
<p>Article published successfully!</p>
|
|
84
|
+
</div>
|
|
85
|
+
<form v-else @submit.prevent="formData.submit()" @reset.prevent="formData.reset()">
|
|
86
|
+
<!-- fields + ActionButtons -->
|
|
87
|
+
</form>
|
|
88
|
+
</template>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Step 4 – Manual buttons (alternative to ActionButtons)
|
|
92
|
+
|
|
93
|
+
```vue
|
|
94
|
+
<div class="flex gap-2 justify-end">
|
|
95
|
+
<Button
|
|
96
|
+
type="submit"
|
|
97
|
+
:label="formData.submitting ? 'Executing...' : 'Execute'"
|
|
98
|
+
:icon="formData.submitting ? 'pi pi-spin pi-spinner' : 'pi pi-play'"
|
|
99
|
+
:disabled="formData.submitting"
|
|
100
|
+
/>
|
|
101
|
+
<Button type="reset" label="Reset" :disabled="!formData.changed" icon="pi pi-eraser" />
|
|
102
|
+
</div>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## editorData vs actionData
|
|
106
|
+
|
|
107
|
+
| Aspect | `editorData` | `actionData` |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| Purpose | CRUD model editing | One-shot command form |
|
|
110
|
+
| Identifier | `model` + `identifiers` | `action` + `parameters` |
|
|
111
|
+
| Create/update | Detects automatically | Always "execute" |
|
|
112
|
+
| After submit | Record is saved | `done` becomes `true` |
|
|
113
|
+
| Definition source | `editor.model` | `formData.action` |
|
|
114
|
+
| `parameters` | Extra params on save | Fixed fields excluded from form |
|
|
115
|
+
|
|
116
|
+
## Key options
|
|
117
|
+
|
|
118
|
+
| Option | Default | Description |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `service` | required | Service name |
|
|
121
|
+
| `action` | required | Action name |
|
|
122
|
+
| `parameters` | `{}` | Fixed identifier fields (not editable) |
|
|
123
|
+
| `initialValue` | `{}` | Initial values for editable fields |
|
|
124
|
+
| `draft` | `true` | Auto-save draft while filling |
|
|
125
|
+
| `debounce` | `600` | Debounce delay in ms |
|
|
126
|
+
| `doneToast` | `"Action done"` | Toast after success |
|
|
127
|
+
| `onDone` | – | Callback after success |
|
|
128
|
+
|
|
129
|
+
## Key returned properties
|
|
130
|
+
|
|
131
|
+
| Property | Description |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `value` | Editable form data |
|
|
134
|
+
| `action` | Action definition (`.properties.*` for `AutoField`) |
|
|
135
|
+
| `editableProperties` | Properties not fixed by `parameters` |
|
|
136
|
+
| `changed` | Form differs from initial value |
|
|
137
|
+
| `submit()` | Execute the action |
|
|
138
|
+
| `submitting` | Action call in progress |
|
|
139
|
+
| `done` | `true` after success |
|
|
140
|
+
| `reset()` | Discard draft, restore initial value |
|
|
141
|
+
| `propertiesErrors` | Server validation errors per property |
|
|
142
|
+
| `draftChanged` | Draft auto-saved but not submitted |
|
|
143
|
+
| `savingDraft` | Draft auto-save in progress |
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Integrate analytics tracking with analytics.emit and provider wiring
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Skill: live-change-frontend-analytics (Claude Code)
|
|
6
|
+
|
|
7
|
+
Use this skill when you add **analytics tracking** to a LiveChange frontend.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- You need to track user actions, page views, or custom events.
|
|
12
|
+
- You are integrating PostHog, GA4, or another analytics provider.
|
|
13
|
+
- You need consent-aware analytics.
|
|
14
|
+
|
|
15
|
+
## Step 1 – Emit events with `analytics`
|
|
16
|
+
|
|
17
|
+
Import `analytics` from `@live-change/vue3-components` and emit events:
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
import { analytics } from '@live-change/vue3-components'
|
|
21
|
+
|
|
22
|
+
// In a component or handler:
|
|
23
|
+
analytics.emit('article:published', { articleId: article.id })
|
|
24
|
+
analytics.emit('button:clicked', { action: 'delete', target: 'article' })
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Standard built-in events:
|
|
28
|
+
|
|
29
|
+
| Event | Payload | When emitted |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `pageView` | route `to` object | On route change (automatic in App.vue) |
|
|
32
|
+
| `user:identification` | `{ user, session, identification, contacts }` | When user identity is known |
|
|
33
|
+
| `consent` | `{ analytics: boolean }` | When user grants/denies tracking consent |
|
|
34
|
+
| `locale:change` | locale settings object | When language/locale changes |
|
|
35
|
+
|
|
36
|
+
## Step 2 – Wire user identification in App.vue
|
|
37
|
+
|
|
38
|
+
Emit `user:identification` when the user profile is loaded:
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
import { analytics } from '@live-change/vue3-components'
|
|
42
|
+
import { usePath, live, useClient } from '@live-change/vue3-ssr'
|
|
43
|
+
import { computed, watch } from 'vue'
|
|
44
|
+
|
|
45
|
+
const client = useClient()
|
|
46
|
+
const path = usePath()
|
|
47
|
+
|
|
48
|
+
if(typeof window !== 'undefined') {
|
|
49
|
+
Promise.all([
|
|
50
|
+
live(path.userIdentification.myIdentification()),
|
|
51
|
+
]).then(([identification]) => {
|
|
52
|
+
const fullIdentification = computed(() => ({
|
|
53
|
+
user: client.value.user,
|
|
54
|
+
session: client.value.session,
|
|
55
|
+
identification: identification.value,
|
|
56
|
+
}))
|
|
57
|
+
watch(fullIdentification, (newId) => {
|
|
58
|
+
analytics.emit('user:identification', newId)
|
|
59
|
+
}, { immediate: true })
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Step 3 – Create a provider file (e.g. PostHog)
|
|
65
|
+
|
|
66
|
+
Create a file like `src/analytics/posthog.js`:
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
import { analytics } from '@live-change/vue3-components'
|
|
70
|
+
import posthog from 'posthog-js'
|
|
71
|
+
|
|
72
|
+
posthog.init('phc_YOUR_KEY', {
|
|
73
|
+
api_host: 'https://eu.i.posthog.com',
|
|
74
|
+
person_profiles: 'always'
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Page views
|
|
78
|
+
analytics.on('pageView', (to) => {
|
|
79
|
+
posthog.register({
|
|
80
|
+
route: { name: to.name, params: to.params }
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// User identification
|
|
85
|
+
analytics.on('user:identification', (identification) => {
|
|
86
|
+
if (!identification.user) {
|
|
87
|
+
posthog.reset()
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
posthog.identify(identification.user, {
|
|
91
|
+
firstName: identification.identification?.firstName,
|
|
92
|
+
lastName: identification.identification?.lastName,
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// Consent
|
|
97
|
+
analytics.on('consent', (payload) => {
|
|
98
|
+
if (payload?.analytics === true) {
|
|
99
|
+
posthog.opt_in_capturing()
|
|
100
|
+
} else {
|
|
101
|
+
posthog.opt_out_capturing()
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Catch-all for custom events
|
|
106
|
+
const ignored = ['user:identification', 'pageView', 'consent']
|
|
107
|
+
analytics.on('*', (type, event) => {
|
|
108
|
+
if (ignored.includes(type)) return
|
|
109
|
+
posthog.capture(type, event)
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Import it in `App.vue`:
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
import './analytics/posthog'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Step 4 – Emit custom events in components
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
// Track a form submission
|
|
123
|
+
analytics.emit('form:submitted', { formName: 'contactForm' })
|
|
124
|
+
|
|
125
|
+
// Track a feature usage
|
|
126
|
+
analytics.emit('feature:used', { feature: 'darkMode', enabled: true })
|
|
127
|
+
|
|
128
|
+
// Track navigation
|
|
129
|
+
analytics.emit('navigation:click', { target: 'pricing', source: 'navbar' })
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Step 5 – Consent handling
|
|
133
|
+
|
|
134
|
+
Emit consent events from your consent banner:
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
function acceptAnalytics() {
|
|
138
|
+
analytics.emit('consent', { analytics: true })
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function rejectAnalytics() {
|
|
142
|
+
analytics.emit('consent', { analytics: false })
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Provider files listen for `consent` and enable/disable tracking accordingly.
|