@live-change/frontend-template 0.9.198 → 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.
- package/.claude/rules/live-change-backend-actions-views-triggers.md +246 -0
- package/.claude/rules/live-change-backend-architecture.md +126 -0
- package/.claude/rules/live-change-backend-event-sourcing.md +186 -0
- package/.claude/rules/live-change-backend-models-and-relations.md +260 -0
- package/.claude/rules/live-change-frontend-vue-primevue.md +317 -0
- package/.claude/rules/live-change-service-structure.md +89 -0
- package/.claude/settings.json +32 -0
- package/.claude/skills/create-skills-and-rules/SKILL.md +248 -0
- package/.claude/skills/create-skills-and-rules.md +196 -0
- package/.claude/skills/live-change-backend-change-triggers/SKILL.md +186 -0
- package/.claude/skills/live-change-design-actions-views-triggers/SKILL.md +462 -0
- package/.claude/skills/live-change-design-actions-views-triggers.md +190 -0
- package/.claude/skills/live-change-design-models-relations/SKILL.md +230 -0
- package/.claude/skills/live-change-design-models-relations.md +173 -0
- package/.claude/skills/live-change-design-service/SKILL.md +133 -0
- package/.claude/skills/live-change-design-service.md +132 -0
- package/.claude/skills/live-change-frontend-accessible-objects/SKILL.md +384 -0
- package/.claude/skills/live-change-frontend-accessible-objects.md +383 -0
- package/.claude/skills/live-change-frontend-action-buttons/SKILL.md +129 -0
- package/.claude/skills/live-change-frontend-action-buttons.md +128 -0
- package/.claude/skills/live-change-frontend-action-form/SKILL.md +149 -0
- package/.claude/skills/live-change-frontend-action-form.md +143 -0
- package/.claude/skills/live-change-frontend-analytics/SKILL.md +147 -0
- package/.claude/skills/live-change-frontend-analytics.md +146 -0
- package/.claude/skills/live-change-frontend-command-forms/SKILL.md +216 -0
- package/.claude/skills/live-change-frontend-command-forms.md +215 -0
- package/.claude/skills/live-change-frontend-data-views/SKILL.md +183 -0
- package/.claude/skills/live-change-frontend-data-views.md +182 -0
- package/.claude/skills/live-change-frontend-editor-form/SKILL.md +240 -0
- package/.claude/skills/live-change-frontend-editor-form.md +177 -0
- package/.claude/skills/live-change-frontend-locale-time/SKILL.md +172 -0
- package/.claude/skills/live-change-frontend-locale-time.md +171 -0
- package/.claude/skills/live-change-frontend-page-list-detail/SKILL.md +201 -0
- package/.claude/skills/live-change-frontend-page-list-detail.md +200 -0
- package/.claude/skills/live-change-frontend-range-list/SKILL.md +129 -0
- package/.claude/skills/live-change-frontend-range-list.md +128 -0
- package/.claude/skills/live-change-frontend-ssr-setup/SKILL.md +119 -0
- package/.claude/skills/live-change-frontend-ssr-setup.md +118 -0
- package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +290 -0
- package/.cursor/rules/live-change-backend-architecture.mdc +131 -0
- package/.cursor/rules/live-change-backend-event-sourcing.mdc +185 -0
- package/.cursor/rules/live-change-backend-models-and-relations.mdc +256 -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/create-skills-and-rules.md +248 -0
- package/.cursor/skills/live-change-backend-change-triggers.md +186 -0
- package/.cursor/skills/live-change-design-actions-views-triggers.md +296 -0
- package/.cursor/skills/live-change-design-models-relations.md +230 -0
- package/.cursor/skills/live-change-design-service.md +76 -0
- package/.cursor/skills/live-change-frontend-accessible-objects.md +384 -0
- package/.cursor/skills/live-change-frontend-action-buttons.md +129 -0
- package/.cursor/skills/live-change-frontend-action-form.md +149 -0
- package/.cursor/skills/live-change-frontend-analytics.md +147 -0
- package/.cursor/skills/live-change-frontend-command-forms.md +216 -0
- package/.cursor/skills/live-change-frontend-data-views.md +183 -0
- package/.cursor/skills/live-change-frontend-editor-form.md +240 -0
- package/.cursor/skills/live-change-frontend-locale-time.md +172 -0
- package/.cursor/skills/live-change-frontend-page-list-detail.md +201 -0
- package/.cursor/skills/live-change-frontend-range-list.md +129 -0
- package/.cursor/skills/live-change-frontend-ssr-setup.md +120 -0
- package/README.md +71 -0
- package/front/src/router.js +2 -1
- package/opencode.json +10 -0
- package/package.json +52 -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,256 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rules for defining models, relations, indexes and access control in LiveChange
|
|
3
|
+
globs: **/services/**/*.js
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# LiveChange backend – modele i relacje
|
|
8
|
+
|
|
9
|
+
## Ogólne zasady
|
|
10
|
+
|
|
11
|
+
- Modele zapisuj w plikach domenowych (`<domain>.js`) importowanych w `index.js` serwisu.
|
|
12
|
+
- Dbaj o czytelność definicji – **właściwości wieloliniowo**, z jasnymi polami `type`, `default`, `validation` itd.
|
|
13
|
+
- Tam, gdzie to możliwe, używaj relacji (`userItem`, `itemOf`, `propertyOf`, `foreignModel`) zamiast ręcznego klepania CRUD i widoków.
|
|
14
|
+
|
|
15
|
+
## Styl definicji `properties`
|
|
16
|
+
|
|
17
|
+
- Każde pole opisane wieloliniowo, jedna rzecz na linię.
|
|
18
|
+
- Unikaj upychania typu i walidacji w jednej linii, jeśli zmniejsza to czytelność.
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
properties: {
|
|
22
|
+
name: {
|
|
23
|
+
type: String,
|
|
24
|
+
validation: ['nonEmpty']
|
|
25
|
+
},
|
|
26
|
+
status: {
|
|
27
|
+
type: String,
|
|
28
|
+
default: 'offline'
|
|
29
|
+
},
|
|
30
|
+
capabilities: {
|
|
31
|
+
type: Array,
|
|
32
|
+
of: {
|
|
33
|
+
type: String
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## `userItem` – zasób należący do użytkownika
|
|
40
|
+
|
|
41
|
+
Używaj, gdy model należy do zalogowanego użytkownika.
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
definition.model({
|
|
45
|
+
name: 'Device',
|
|
46
|
+
properties: {
|
|
47
|
+
name: {
|
|
48
|
+
type: String
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
userItem: {
|
|
52
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
53
|
+
writeAccessControl: { roles: ['owner', 'admin'] },
|
|
54
|
+
writeableProperties: ['name']
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Skutek:
|
|
60
|
+
|
|
61
|
+
- automatyczne widoki typu „moje X”,
|
|
62
|
+
- automatyczne akcje create/update/delete dla właściciela.
|
|
63
|
+
|
|
64
|
+
## `itemOf` – dziecko należy do rodzica
|
|
65
|
+
|
|
66
|
+
Używaj, gdy model jest listą elementów powiązanych z innym modelem.
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
definition.model({
|
|
70
|
+
name: 'DeviceConnection',
|
|
71
|
+
properties: {
|
|
72
|
+
connectionType: {
|
|
73
|
+
type: String
|
|
74
|
+
},
|
|
75
|
+
status: {
|
|
76
|
+
type: String,
|
|
77
|
+
default: 'offline'
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
itemOf: {
|
|
81
|
+
what: Device,
|
|
82
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
83
|
+
writeAccessControl: { roles: ['owner', 'admin'] }
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Zasady:
|
|
89
|
+
|
|
90
|
+
- rodzic `what` to model zadeklarowany wcześniej w tym lub innym serwisie,
|
|
91
|
+
- relacja generuje standardowe widoki/akcje dla listy i elementów.
|
|
92
|
+
|
|
93
|
+
## `propertyOf` – właściwość z ID równym rodzicowi
|
|
94
|
+
|
|
95
|
+
Używaj, gdy model przechowuje „stan” jednego obiektu (ID dziecka = ID rodzica).
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
definition.model({
|
|
99
|
+
name: 'DeviceCursorState',
|
|
100
|
+
properties: {
|
|
101
|
+
x: { type: Number },
|
|
102
|
+
y: { type: Number }
|
|
103
|
+
},
|
|
104
|
+
propertyOf: {
|
|
105
|
+
what: Device,
|
|
106
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
107
|
+
writeAccessControl: { roles: ['owner', 'admin'] }
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Skutek:
|
|
113
|
+
|
|
114
|
+
- prosty dostęp: `DeviceCursorState.get(deviceId)`,
|
|
115
|
+
- bez potrzeby dodatkowych indeksów po polu `device`.
|
|
116
|
+
|
|
117
|
+
## `propertyOf` dla wielu modeli (relacja 1:1 do każdego z nich)
|
|
118
|
+
|
|
119
|
+
W niektórych domenach model jest „łącznikiem 1:1” pomiędzy rekordami (np. faktura ↔ kontrahent w roli dostawcy/klienta).
|
|
120
|
+
Najczęściej są to 2 modele, ale `propertyOf` może wskazywać na **dowolnie wiele** modeli (np. relacja łącząca 3+ encje), jeśli taka jest semantyka domeny.
|
|
121
|
+
|
|
122
|
+
Wtedy:
|
|
123
|
+
|
|
124
|
+
- **nie** przechowuj „drugiej strony” jako zwykłego `contractorId` (albo ogólnie `someId`) w `properties`
|
|
125
|
+
- **nie** dodawaj ręcznie pól `...Id` w modelu relacyjnym, jeśli to ma być relacja – to utrudnia generatorowi CRUD/relacji poprawne wnioskowanie o powiązaniach
|
|
126
|
+
- zamiast tego zdefiniuj relacje jako `propertyOf` do **każdego** z modeli, które mają być rodzicami relacji
|
|
127
|
+
|
|
128
|
+
To jest istotne, bo generator CRUD/relacji rozumie wtedy, że encja jest powiązaniem pomiędzy dwiema encjami, a nie „zwykłym obiektem z polem id”.
|
|
129
|
+
|
|
130
|
+
Przykład (schematycznie):
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
const CostInvoice = definition.foreignModel('invoice', 'CostInvoice')
|
|
134
|
+
const Contractor = definition.foreignModel('company', 'Contractor')
|
|
135
|
+
|
|
136
|
+
definition.model({
|
|
137
|
+
name: 'Supplier',
|
|
138
|
+
properties: {
|
|
139
|
+
// dodatkowe pola relacji (opcjonalnie)
|
|
140
|
+
},
|
|
141
|
+
propertyOf: [
|
|
142
|
+
{ what: CostInvoice },
|
|
143
|
+
{ what: Contractor }
|
|
144
|
+
]
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## `foreignModel` – relacja do modelu z innego serwisu
|
|
149
|
+
|
|
150
|
+
Używaj, gdy `itemOf`/`propertyOf` ma wskazywać na model spoza aktualnego serwisu.
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
const Device = definition.foreignModel('deviceManager', 'Device')
|
|
154
|
+
|
|
155
|
+
definition.model({
|
|
156
|
+
name: 'BotSession',
|
|
157
|
+
properties: {
|
|
158
|
+
// ...
|
|
159
|
+
},
|
|
160
|
+
itemOf: {
|
|
161
|
+
what: Device,
|
|
162
|
+
readAccessControl: { roles: ['owner', 'admin'] }
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Zasady:
|
|
168
|
+
|
|
169
|
+
- pierwszy argument to nazwa serwisu,
|
|
170
|
+
- drugi to nazwa modelu w tamtym serwisie.
|
|
171
|
+
|
|
172
|
+
## Automatycznie dodawane pola z relacji
|
|
173
|
+
|
|
174
|
+
Relacje automatycznie dodają **pola identyfikatorów** i **indeksy** do modelu. **Nie definiuj ich ponownie** w `properties`.
|
|
175
|
+
|
|
176
|
+
**Konwencja nazw:** nazwa pola = nazwa modelu rodzica z małą pierwszą literą (`Device` → `device`, `CostInvoice` → `costInvoice`).
|
|
177
|
+
|
|
178
|
+
| Relacja | Dodane pole/pola | Dodane indeksy |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `itemOf: { what: Device }` | `device` | `byDevice` |
|
|
181
|
+
| `propertyOf: { what: Device }` | `device` | `byDevice` |
|
|
182
|
+
| `userItem` | `user` | `byUser` |
|
|
183
|
+
| `userProperty` | `user` | `byUser` |
|
|
184
|
+
| `sessionOrUserProperty` | `sessionOrUserType`, `sessionOrUser` | `bySessionOrUser` (hash) |
|
|
185
|
+
| `sessionOrUserProperty: { extendedWith: ['object'] }` | + `objectType`, `object` | indeksy złożone |
|
|
186
|
+
| `propertyOfAny: { ownerTypes: [...] }` | `ownerType`, `owner` | `byOwner` (hash) |
|
|
187
|
+
| `boundTo: { what: Device }` | `device` | `byDevice` (hash) |
|
|
188
|
+
|
|
189
|
+
Dla relacji z wieloma rodzicami (np. `propertyOf: [{ what: A }, { what: B }]`) tworzone są wszystkie kombinacje indeksów (`byA`, `byB`, `byAAndB`).
|
|
190
|
+
|
|
191
|
+
## `propertyOfAny` — typy rodzica (`{name}Types`)
|
|
192
|
+
|
|
193
|
+
`propertyOfAny` jest relacją polimorficzną. Lista dozwolonych typów jest podawana w polach `{name}Types`, gdzie `{name}` pochodzi z `to`.
|
|
194
|
+
|
|
195
|
+
- Gdy `to` nie jest podane, domyślnie jest `['owner']`, więc użyj `ownerTypes`.
|
|
196
|
+
- Gdy `to: ['invoice']`, użyj `invoiceTypes`.
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
// domyślnie to: ['owner']
|
|
200
|
+
propertyOfAny: {
|
|
201
|
+
ownerTypes: ['invoice_CostInvoice', 'invoice_IncomeInvoice']
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// jawnie
|
|
205
|
+
propertyOfAny: {
|
|
206
|
+
to: ['invoice'],
|
|
207
|
+
invoiceTypes: ['invoice_CostInvoice', 'invoice_IncomeInvoice']
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
// ✅ Poprawnie — definiuj tylko SWOJE pola
|
|
213
|
+
definition.model({
|
|
214
|
+
name: 'Connection',
|
|
215
|
+
properties: {
|
|
216
|
+
status: { type: String } // 'device' NIE jest tutaj — dodaje go itemOf
|
|
217
|
+
},
|
|
218
|
+
itemOf: { what: Device } // automatycznie dodaje pole 'device' + indeks 'byDevice'
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// ❌ Źle — redundantne pole
|
|
222
|
+
definition.model({
|
|
223
|
+
name: 'Connection',
|
|
224
|
+
properties: {
|
|
225
|
+
device: { type: String }, // ❌ już dodane przez itemOf
|
|
226
|
+
status: { type: String }
|
|
227
|
+
},
|
|
228
|
+
itemOf: { what: Device }
|
|
229
|
+
})
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Użyj `node server/start.js describe --service myService --model MyModel --output yaml` żeby zobaczyć wszystkie pola łącznie z automatycznie dodanymi.
|
|
233
|
+
|
|
234
|
+
## Indeksy
|
|
235
|
+
|
|
236
|
+
- Definiuj indeksy jawnie w modelu, gdy będziesz często wyszukiwać po danym polu lub kombinacji pól.
|
|
237
|
+
- Nazwy indeksów powinny być opisowe, bez skrótów trudnych do odczytania.
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
indexes: {
|
|
241
|
+
bySessionKey: {
|
|
242
|
+
property: ['sessionKey']
|
|
243
|
+
},
|
|
244
|
+
byDeviceAndStatus: {
|
|
245
|
+
property: ['device', 'status']
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Poza serwisem indeks bywa widoczny pod nazwą z prefiksem serwisu, np. `myService_Model_byDeviceAndStatus`.
|
|
251
|
+
|
|
252
|
+
## Access control na relacjach
|
|
253
|
+
|
|
254
|
+
- Zawsze ustawiaj `readAccessControl` i `writeAccessControl` na relacjach (`userItem`, `itemOf`, `propertyOf`), zamiast polegać na domyślnym zachowaniu.
|
|
255
|
+
- Traktuj to jako część modelu, nie dodatek na końcu.
|
|
256
|
+
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rules for Vue 3, PrimeVue, Tailwind frontend development on LiveChange
|
|
3
|
+
globs: **/front/src/**/*.{vue,js,ts}
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Frontend na live-change-stack – Vue 3 + PrimeVue + Tailwind
|
|
8
|
+
|
|
9
|
+
## Stack
|
|
10
|
+
|
|
11
|
+
- Vue 3 + TypeScript
|
|
12
|
+
- PrimeVue 4 – główna biblioteka komponentów UI
|
|
13
|
+
- Tailwind CSS – podstawowe stylowanie (unikaj zbędnego custom CSS)
|
|
14
|
+
- vite-plugin-pages – file-based routing w `src/pages/`
|
|
15
|
+
- @live-change/vue3-ssr – integracja z backendem LiveChange
|
|
16
|
+
|
|
17
|
+
## Pobieranie danych – `live` + `Promise.all`
|
|
18
|
+
|
|
19
|
+
- **Nie** używaj `ref(null)` + `onMounted` do ładowania danych.
|
|
20
|
+
- Dane ładuj w `setup`/`script setup` przez `await Promise.all(...)` na `live(path()....)`.
|
|
21
|
+
- Rodzic powinien owijać stronę w `<Suspense>` (w projektach live-change robi to zazwyczaj `ViewRoot` globalnie).
|
|
22
|
+
|
|
23
|
+
Przykład:
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { path, live, api as useApi } from '@live-change/vue3-ssr'
|
|
27
|
+
|
|
28
|
+
const api = useApi()
|
|
29
|
+
|
|
30
|
+
const [devices] = await Promise.all([
|
|
31
|
+
live(path().deviceManager.myUserDevices({}))
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
const [device, connections] = await Promise.all([
|
|
35
|
+
live(path().deviceManager.myUserDevice({ device: deviceId })),
|
|
36
|
+
live(path().deviceManager.deviceOwnedDeviceConnections({ device: deviceId }))
|
|
37
|
+
])
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Dostęp w template:
|
|
41
|
+
|
|
42
|
+
```vue
|
|
43
|
+
<template>
|
|
44
|
+
<div v-if="device.value">
|
|
45
|
+
{{ device.value.name }}
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Komendy i formularze – wybór wzorca
|
|
51
|
+
|
|
52
|
+
Są 4 sposoby wywoływania akcji backendowych. Używaj właściwego:
|
|
53
|
+
|
|
54
|
+
| Wzorzec | Kiedy używać |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `editorData` | **Edycja rekordów modelu** (create/update). Drafty, walidacja, `AutoField`. Ustawienia, edytory, profile. |
|
|
57
|
+
| `actionData` | **Jednorazowe formularze akcji** (nie CRUD). Submit → done. Publikacja, zaproszenie, import. |
|
|
58
|
+
| `api.command` | **Pojedynczy przycisk lub wywołanie z kodu** (bez pól formularza). Usuwanie, toggle, akcje z kodu. |
|
|
59
|
+
| `<command-form>` | **Unikaj.** Legacy, tylko trywialne prototypy. Preferuj `editorData` lub `actionData`. |
|
|
60
|
+
|
|
61
|
+
Schemat decyzji:
|
|
62
|
+
|
|
63
|
+
1. Użytkownik wypełnia pola formularza? → **Nie**: użyj `api.command` (owijaj w `workingZone.addPromise` dla przycisków).
|
|
64
|
+
2. Edytuje rekord modelu (create/update)? → **Tak**: użyj `editorData`. **Nie**: użyj `actionData`.
|
|
65
|
+
3. `<command-form>` tylko do najprostszych jednorazowych przypadków.
|
|
66
|
+
|
|
67
|
+
### `api.command`
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
await api.command(['deviceManager', 'createMyUserDevice'], {
|
|
71
|
+
name: 'Moje urządzenie'
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
await api.command(['deviceManager', 'deleteMyUserDevice'], {
|
|
75
|
+
device: id
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Format komendy:
|
|
80
|
+
|
|
81
|
+
- tablica `[serviceName, actionName]`,
|
|
82
|
+
- payload jako zwykły obiekt.
|
|
83
|
+
|
|
84
|
+
## Routing – blok `<route>` i meta `signedIn`
|
|
85
|
+
|
|
86
|
+
- Każda strona w `src/pages/` może mieć blok `<route>` z metadanymi.
|
|
87
|
+
- Używaj `meta.signedIn` do oznaczania stron wymagających logowania.
|
|
88
|
+
|
|
89
|
+
```vue
|
|
90
|
+
<route>
|
|
91
|
+
{ "name": "devices", "meta": { "signedIn": true } }
|
|
92
|
+
</route>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Dynamiczne ścieżki:
|
|
96
|
+
|
|
97
|
+
- plik `[id].vue` odpowiada ścieżce `/devices/:id`.
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
import { useRoute } from 'vue-router'
|
|
101
|
+
|
|
102
|
+
const route = useRoute()
|
|
103
|
+
const deviceId = route.params.id
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Confirm + Toast – wzorzec dla akcji destrukcyjnych
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import { useConfirm } from 'primevue/useconfirm'
|
|
110
|
+
import { useToast } from 'primevue/usetoast'
|
|
111
|
+
|
|
112
|
+
const confirm = useConfirm()
|
|
113
|
+
const toast = useToast()
|
|
114
|
+
|
|
115
|
+
function deleteDevice(id) {
|
|
116
|
+
confirm.require({
|
|
117
|
+
message: 'Czy na pewno chcesz usunąć to urządzenie?',
|
|
118
|
+
header: 'Potwierdzenie',
|
|
119
|
+
icon: 'pi pi-exclamation-triangle',
|
|
120
|
+
accept: async () => {
|
|
121
|
+
await api.command(['deviceManager', 'deleteMyUserDevice'], { device: id })
|
|
122
|
+
toast.add({
|
|
123
|
+
severity: 'success',
|
|
124
|
+
summary: 'Usunięto',
|
|
125
|
+
life: 2000
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Wzorce UI – lista i szczegóły
|
|
133
|
+
|
|
134
|
+
### Lista
|
|
135
|
+
|
|
136
|
+
```vue
|
|
137
|
+
<template>
|
|
138
|
+
<div class="container mx-auto p-4">
|
|
139
|
+
<div class="flex items-center justify-between mb-6">
|
|
140
|
+
<h1 class="text-2xl font-bold">Urządzenia</h1>
|
|
141
|
+
<Button label="Dodaj" icon="pi pi-plus" @click="openDialog" />
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<Card v-if="devices.value?.length === 0">
|
|
145
|
+
<template #content>
|
|
146
|
+
<p class="text-center text-gray-500">
|
|
147
|
+
Brak urządzeń
|
|
148
|
+
</p>
|
|
149
|
+
</template>
|
|
150
|
+
</Card>
|
|
151
|
+
|
|
152
|
+
<div class="grid gap-4">
|
|
153
|
+
<Card v-for="device in devices.value" :key="device.id">
|
|
154
|
+
<template #content>
|
|
155
|
+
<!-- zawartość -->
|
|
156
|
+
</template>
|
|
157
|
+
</Card>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</template>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Tag statusu
|
|
164
|
+
|
|
165
|
+
```vue
|
|
166
|
+
<Tag
|
|
167
|
+
:value="conn.status"
|
|
168
|
+
:severity="conn.status === 'online' ? 'success' : 'secondary'"
|
|
169
|
+
/>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Computed paths – reaktywne parametry
|
|
173
|
+
|
|
174
|
+
Gdy ścieżki zależą od reaktywnych wartości (route params, props), owijaj je w `computed()`:
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
import { computed, unref } from 'vue'
|
|
178
|
+
import { usePath, live } from '@live-change/vue3-ssr'
|
|
179
|
+
|
|
180
|
+
const path = usePath()
|
|
181
|
+
|
|
182
|
+
const articlePath = computed(() => path.blog.article({ article: unref(articleId) }))
|
|
183
|
+
const [article] = await Promise.all([live(articlePath)])
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Dla warunkowego ładowania (np. tylko gdy zalogowany), zwróć wartość falsy:
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
import { useClient } from '@live-change/vue3-ssr'
|
|
190
|
+
const client = useClient()
|
|
191
|
+
|
|
192
|
+
const myDataPath = computed(() => client.value.user && path.blog.myArticles({}))
|
|
193
|
+
const [myData] = await Promise.all([live(myDataPath)])
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Powiązane dane – `.with()`
|
|
197
|
+
|
|
198
|
+
Dołączaj powiązane obiekty do elementów w jednym reaktywnym zapytaniu:
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
path.blog.articles({})
|
|
202
|
+
.with(article => path.userIdentification.identification({
|
|
203
|
+
sessionOrUserType: article.authorType,
|
|
204
|
+
sessionOrUser: article.author
|
|
205
|
+
}).bind('authorProfile'))
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Dostęp: `article.authorProfile?.firstName`. Działa zarówno z `live()` jak i `RangeViewer`.
|
|
209
|
+
|
|
210
|
+
## WorkingZone dla akcji asynchronicznych
|
|
211
|
+
|
|
212
|
+
`ViewRoot` opakowuje każdą stronę w `<WorkingZone>`. Używaj `inject('workingZone')` dla akcji przycisków poza formularzami:
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
import { inject } from 'vue'
|
|
216
|
+
const workingZone = inject('workingZone')
|
|
217
|
+
|
|
218
|
+
function doAction() {
|
|
219
|
+
workingZone.addPromise('actionName', (async () => {
|
|
220
|
+
await actions.blog.publishArticle({ article: id })
|
|
221
|
+
toast.add({ severity: 'success', summary: 'Opublikowano', life: 2000 })
|
|
222
|
+
})())
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Aktywuje globalny spinner/blur na czas trwania promisy.
|
|
227
|
+
|
|
228
|
+
## Kontrola dostępu z `useClient`
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
import { useClient } from '@live-change/vue3-ssr'
|
|
232
|
+
const client = useClient()
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
- `client.value.user` – truthy gdy zalogowany
|
|
236
|
+
- `client.value.roles` – tablica ról (np. `['admin', 'owner']`)
|
|
237
|
+
|
|
238
|
+
W template:
|
|
239
|
+
|
|
240
|
+
```vue
|
|
241
|
+
<Button v-if="client.roles.includes('admin')" label="Admin" />
|
|
242
|
+
<div v-if="!client.user">Zaloguj się</div>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Locale i czas
|
|
246
|
+
|
|
247
|
+
- Wywołaj `useLocale().captureLocale()` w `App.vue` żeby zapisać locale przeglądarki do backendu.
|
|
248
|
+
- Używaj `locale.localTime(date)` z `d()` z vue-i18n do wyświetlania dat bezpiecznego dla SSR.
|
|
249
|
+
- `currentTime` z `@live-change/frontend-base` to reaktywny ref który tykuje co 500ms.
|
|
250
|
+
- `useTimeSynchronization()` z `@live-change/vue3-ssr` koryguje różnicę zegarów – używaj gdy timing jest krytyczny (odliczanie, eventy real-time).
|
|
251
|
+
|
|
252
|
+
## Analityka
|
|
253
|
+
|
|
254
|
+
Używaj `analytics` z `@live-change/vue3-components`:
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
import { analytics } from '@live-change/vue3-components'
|
|
258
|
+
analytics.emit('article:published', { articleId: id })
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Podpinaj providery (PostHog, GA4) w osobnym pliku importowanym z `App.vue`.
|
|
262
|
+
|
|
263
|
+
## SSR i konfiguracja frontendu
|
|
264
|
+
|
|
265
|
+
- Utrzymuj standardowy układ entry points:
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
// entry-client.js
|
|
269
|
+
import { clientEntry } from '@live-change/frontend-base/client-entry.js'
|
|
270
|
+
export default clientEntry(App, createRouter, config)
|
|
271
|
+
|
|
272
|
+
// entry-server.js
|
|
273
|
+
import { serverEntry, sitemapEntry } from '@live-change/frontend-base/server-entry.js'
|
|
274
|
+
export const render = serverEntry(App, createRouter, config)
|
|
275
|
+
export const sitemap = sitemapEntry(App, createRouter, routerSitemap, config)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
- Konfigurację PrimeVue trzymaj w jednym miejscu (`config.js`) i używaj `definePreset` do nadpisania motywu.
|
|
279
|
+
|
|
280
|
+
## Odkrywanie widoków i akcji z `describe`
|
|
281
|
+
|
|
282
|
+
Komenda CLI `describe` pozwala znaleźć dostępne widoki (do `live()`) i akcje (do `api.command` / `editorData` / `actionData`):
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
node server/start.js describe --service blog
|
|
286
|
+
node server/start.js describe --service blog --view articlesByCreatedAt --output yaml
|
|
287
|
+
node server/start.js describe --service blog --action createMyUserArticle --output yaml
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
To najszybszy sposób na odkrycie jakie ścieżki i akcje są dostępne, w tym te wygenerowane automatycznie przez relacje.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: LiveChange service file structure — every service must be a directory with separate definition.js, index.js, etc.
|
|
3
|
+
globs: server/**/*.js
|
|
4
|
+
alwaysApply: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# LiveChange Service Structure
|
|
8
|
+
|
|
9
|
+
Every LiveChange service **must** be a directory, not a single file.
|
|
10
|
+
|
|
11
|
+
## Required structure
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
server/<serviceName>/
|
|
15
|
+
definition.js # creates app.createServiceDefinition({ name }) – nothing else
|
|
16
|
+
index.js # imports definition, imports all domain files, exports definition
|
|
17
|
+
config.js # required – resolves definition.config, sets definition.clientConfig, exports plain config object
|
|
18
|
+
<domain>.js # one file per domain area (models, views, actions, triggers)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## definition.js
|
|
22
|
+
|
|
23
|
+
Only creates and exports the definition. No models, no actions here.
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import App from '@live-change/framework'
|
|
27
|
+
const app = App.app()
|
|
28
|
+
|
|
29
|
+
const definition = app.createServiceDefinition({ name: 'myService' })
|
|
30
|
+
|
|
31
|
+
export default definition
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## index.js
|
|
35
|
+
|
|
36
|
+
Imports definition and all domain files (side-effect imports), then re-exports definition.
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
import App from '@live-change/framework'
|
|
40
|
+
const app = App.app()
|
|
41
|
+
|
|
42
|
+
import definition from './definition.js'
|
|
43
|
+
|
|
44
|
+
import './authenticator.js'
|
|
45
|
+
import './myModel.js'
|
|
46
|
+
import './otherModel.js'
|
|
47
|
+
|
|
48
|
+
export default definition
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## config.js (required)
|
|
52
|
+
|
|
53
|
+
Reads `definition.config` (passed from `app.config.js`), resolves defaults, sets `definition.clientConfig` (for frontend),
|
|
54
|
+
and exports a plain config object used by other domain files.
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
import definition from './definition.js'
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
defaultAccess = {
|
|
61
|
+
writeAccessControl: ['owner', 'admin'],
|
|
62
|
+
readAllAccess: ['admin']
|
|
63
|
+
}
|
|
64
|
+
} = definition.config || {}
|
|
65
|
+
|
|
66
|
+
definition.clientConfig = {
|
|
67
|
+
// values exposed to frontend, if needed
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default { defaultAccess }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Domain files (e.g. myModel.js)
|
|
74
|
+
|
|
75
|
+
Each file imports `definition` (and `config` if needed) and registers models/views/actions/triggers.
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
import App from '@live-change/framework'
|
|
79
|
+
const app = App.app()
|
|
80
|
+
|
|
81
|
+
import definition from './definition.js'
|
|
82
|
+
import config from './config.js'
|
|
83
|
+
|
|
84
|
+
export const MyModel = definition.model({ name: 'MyModel', ... })
|
|
85
|
+
|
|
86
|
+
definition.view({ name: 'myList', ... })
|
|
87
|
+
definition.action({ name: 'doThing', ... })
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Rule of thumb:
|
|
91
|
+
|
|
92
|
+
- Domain files should read configuration from `config` (exported from `config.js`), not directly from `definition.config`.
|
|
93
|
+
|
|
94
|
+
## services.list.js import
|
|
95
|
+
|
|
96
|
+
Always import from the directory index, not a flat file:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
import myService from './myService/index.js' // ✓
|
|
100
|
+
import myService from './myService.js' // ✗
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Why
|
|
104
|
+
|
|
105
|
+
A flat single-file service makes it hard to add new domain files (auth, models, triggers, etc.)
|
|
106
|
+
without the file growing unmanageably. The directory structure mirrors the pattern used in
|
|
107
|
+
`@live-change/live-change-stack/services/*`.
|