@live-change/frontend-template 0.9.203 → 0.9.205
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 +49 -1
- package/.claude/rules/live-change-backend-models-and-relations.md +24 -6
- package/.claude/rules/live-change-service-structure.md +2 -2
- package/.claude/settings.json +3 -1
- package/.claude/skills/live-change-backend-change-triggers/SKILL.md +15 -0
- package/.claude/skills/live-change-dao-protocol/SKILL.md +46 -0
- package/.claude/skills/live-change-design-actions-views-triggers/SKILL.md +110 -0
- package/.claude/skills/live-change-design-models-relations/SKILL.md +63 -5
- package/.claude/skills/live-change-frontend-data-views/SKILL.md +73 -4
- package/.claude/skills/live-change-frontend-range-list/SKILL.md +90 -0
- package/.claude/skills/live-change-frontend-synchronized/SKILL.md +101 -0
- package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +23 -4
- package/.cursor/rules/live-change-backend-architecture.mdc +1 -1
- package/.cursor/rules/live-change-backend-event-sourcing.mdc +1 -1
- package/.cursor/rules/live-change-backend-models-and-relations.mdc +36 -7
- package/.cursor/rules/live-change-backend-views-vs-triggers-for-reads-writes.mdc +28 -0
- package/.cursor/rules/live-change-dao-protocol.mdc +47 -0
- package/.cursor/rules/live-change-frontend-views-not-commands-for-reads.mdc +30 -0
- package/.cursor/rules/live-change-frontend-vue-primevue.mdc +70 -4
- package/.cursor/rules/live-change-service-structure.mdc +1 -1
- package/.cursor/skills/live-change-backend-change-triggers.md +15 -0
- package/.cursor/skills/live-change-design-actions-views-triggers.md +51 -0
- package/.cursor/skills/live-change-design-models-relations.md +23 -5
- package/.cursor/skills/live-change-frontend-data-views.md +15 -0
- package/.cursor/skills/live-change-frontend-range-list.md +21 -0
- package/.cursor/skills/live-change-frontend-synchronized.md +101 -0
- package/.node-version +1 -1
- package/.nvmrc +1 -1
- package/front/src/pages/index.vue +1 -1
- package/package.json +55 -55
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: live-change-frontend-synchronized
|
|
3
|
+
description: Use synchronized and synchronizedList for autosave editing in LiveChange Vue frontends, including object vs list decision flow, identifiers mapping, and save/delete patterns
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: live-change-frontend-synchronized (Claude Code)
|
|
7
|
+
|
|
8
|
+
Use this skill when implementing or refactoring frontend editing flows that should keep local form state synchronized with backend data.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
- Editing one object loaded from `live(...)` with autosave or manual save.
|
|
13
|
+
- Editing list items inline (for example access roles) with per-row identifiers.
|
|
14
|
+
- Replacing custom `watch + api.command` autosave logic with a standard helper.
|
|
15
|
+
- Choosing between `synchronized`, `synchronizedList`, and `editorData`.
|
|
16
|
+
|
|
17
|
+
## Decision flow
|
|
18
|
+
|
|
19
|
+
1. **One editable object** (`profile`, `note`, `settings`) -> use `synchronized`.
|
|
20
|
+
2. **Editable list rows** (`accesses`, `invitations`, `requests`) -> use `synchronizedList`.
|
|
21
|
+
3. **Definition-driven CRUD form with validation UI** -> use `editorData` from auto-form stack.
|
|
22
|
+
|
|
23
|
+
## What counts as "editable list rows"
|
|
24
|
+
|
|
25
|
+
Treat the feature as a list flow (`synchronizedList`) when most of these are true:
|
|
26
|
+
|
|
27
|
+
- The UI renders rows with `v-for` and each row has editable fields.
|
|
28
|
+
- Users can edit many rows inline in one screen (table/config/admin list).
|
|
29
|
+
- Autosave should happen per row while the list stays reactive.
|
|
30
|
+
- Each row needs its own action payload keys in addition to shared list context.
|
|
31
|
+
|
|
32
|
+
In this case, wire one `synchronizedList(...)` and edit fields directly on `syncList.value`.
|
|
33
|
+
Do not build a parallel `id -> synchronized(...)` map for rows.
|
|
34
|
+
|
|
35
|
+
## `synchronized` pattern (object)
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
import { synchronized } from '@live-change/vue3-components'
|
|
39
|
+
|
|
40
|
+
const sync = synchronized({
|
|
41
|
+
source: sourceRef,
|
|
42
|
+
update: actions.service.updateThing,
|
|
43
|
+
identifiers: computed(() => ({ thing: thingId.value })),
|
|
44
|
+
recursive: true,
|
|
45
|
+
autoSave: true,
|
|
46
|
+
debounce: 600
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const { value: editable, changed, saving, save } = sync
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Rules
|
|
53
|
+
|
|
54
|
+
- `source` should come from `live(...)` or a computed source.
|
|
55
|
+
- Keep identifiers stable and pass them through `identifiers` (plain object or `computed`).
|
|
56
|
+
- Use `updateDataProperty: 'data'` for draft-like payloads where backend expects nested data.
|
|
57
|
+
- Use `autoSave: false` when save must happen only after explicit confirmation.
|
|
58
|
+
|
|
59
|
+
## `synchronizedList` pattern (list)
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
import { synchronizedList } from '@live-change/vue3-components'
|
|
63
|
+
|
|
64
|
+
const syncList = synchronizedList({
|
|
65
|
+
source: accesses,
|
|
66
|
+
update: actions.accessControl.updateSessionOrUserAndObjectOwnedAccess,
|
|
67
|
+
delete: actions.accessControl.resetSessionOrUserAndObjectOwnedAccess,
|
|
68
|
+
identifiers: { object, objectType },
|
|
69
|
+
objectIdentifiers: ({ to, sessionOrUser, sessionOrUserType }) => ({
|
|
70
|
+
access: to, sessionOrUser, sessionOrUserType, object, objectType
|
|
71
|
+
}),
|
|
72
|
+
recursive: true
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const editableRows = syncList.value
|
|
76
|
+
await syncList.delete(editableRows.value[0])
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Rules
|
|
80
|
+
|
|
81
|
+
- `source` should be a list from `live(...)`.
|
|
82
|
+
- Every item should have stable `id`.
|
|
83
|
+
- Put shared context in `identifiers`, and row-specific keys in `objectIdentifiers`.
|
|
84
|
+
- Edit rows through `syncList.value`; call `syncList.delete(...)` / `syncList.insert(...)` on the helper object.
|
|
85
|
+
- Prefer this pattern for configuration lists, permission tables, dictionaries, and other multi-row settings editors.
|
|
86
|
+
|
|
87
|
+
## Project examples to follow
|
|
88
|
+
|
|
89
|
+
- `speed-dating/front/src/components/notes/NoteEditor.vue` (`synchronized` with autosave)
|
|
90
|
+
- `speed-dating/front/src/components/profile/ProfileSettings.vue` (`synchronized` + `updateDataProperty: 'data'`)
|
|
91
|
+
- `family-tree/front/src/components/AgreementDialog.vue` (`synchronized` with manual save)
|
|
92
|
+
- `rcstreamer/front/src/configuration/AccessRequests.vue` (`synchronizedList`)
|
|
93
|
+
- `rcstreamer/front/src/configuration/AccessInvitations.vue` (`synchronizedList`)
|
|
94
|
+
- `rcstreamer/front/src/configuration/AccessList.vue` (`synchronizedList`)
|
|
95
|
+
|
|
96
|
+
## Common mistakes
|
|
97
|
+
|
|
98
|
+
- Using `synchronized` for a list of rows instead of `synchronizedList`.
|
|
99
|
+
- Forgetting `objectIdentifiers` when backend update/delete actions need per-row keys.
|
|
100
|
+
- Mutating raw `live(...)` list data instead of `synchronizedList(...).value`.
|
|
101
|
+
- Mixing autosave helper patterns with unrelated one-shot action form patterns.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Rules for implementing actions, views, and triggers in LiveChange services
|
|
3
|
-
globs: **/services/**/*.js
|
|
3
|
+
globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
|
|
4
4
|
alwaysApply: false
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -54,9 +54,9 @@ definition.action({
|
|
|
54
54
|
|
|
55
55
|
## Widoki – ogólne zasady
|
|
56
56
|
|
|
57
|
-
- Widok ma być
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
- Widok ma być **kanałem odczytu** (DAO, indeksy, agregacje, wyliczenia). Logika odczytu należy do widoku (ew. funkcji pomocniczej wywołanej z `daoPath` / `get`), a nie do akcji „tylko po to, żeby zwrócić dane bez zmiany stanu”.
|
|
58
|
+
- W implementacji ścieżek korzystaj z indeksów (`indexObjectGet`, `indexRangeGet`) zamiast pełnych skanów.
|
|
59
|
+
- Odczyt vs zapis (CQRS-like): [live-change-backend-views-vs-triggers-for-reads-writes.mdc](./live-change-backend-views-vs-triggers-for-reads-writes.mdc).
|
|
60
60
|
|
|
61
61
|
## Widoki – reguła: `get` + `observable` zawsze razem
|
|
62
62
|
|
|
@@ -173,6 +173,19 @@ Jeśli filtr po miesiącu jest częsty, lepiej dodać indeks z bucketem miesiąc
|
|
|
173
173
|
BankTransaction.sortedIndexRangePath('byBankAccountAndMonthAndDate', [bankAccount, month], range)
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
### Guardrails dla widoków pod RangeViewer/rangeBuckets
|
|
177
|
+
|
|
178
|
+
- Dla list paginowanych po indeksie preferuj `Model.sortedIndexRangePath(indexName, keyPrefix, App.extractRange(props))`.
|
|
179
|
+
- Nie stosuj semantyki `indexRangePath` dla widoków konsumowanych przez bucketowe UI zakresowe.
|
|
180
|
+
- `gt/gte/lt/lte` zostaw wyłącznie jako kursor paginacji, nie jako filtry domenowe.
|
|
181
|
+
- Dla filtrów typu miesiąc/rok/status najpierw projektuj indeks z odpowiednim prefiksem.
|
|
182
|
+
- `App.utils.prefixRange` traktuj jako fallback backendowy, gdy zmiana indeksu nie jest możliwa.
|
|
183
|
+
|
|
184
|
+
### Guardrail dla indeksów standalone
|
|
185
|
+
|
|
186
|
+
- Gdy indeks łączy równorzędne strumienie danych (union wielu tabel), definiuj go jako serwisowy `definition.index(...)` (najlepiej w osobnym `indexes.js`), a nie jako `model.indexes` przypisany do przypadkowego modelu.
|
|
187
|
+
- `model.indexes` stosuj tylko wtedy, gdy semantycznym właścicielem indeksu jest jeden model.
|
|
188
|
+
|
|
176
189
|
## Triggery – online/offline i batchowanie
|
|
177
190
|
|
|
178
191
|
- Triggery są do reakcji na zdarzenia (np. zmiana stanu sesji, start serwera).
|
|
@@ -271,6 +284,12 @@ definition.trigger({
|
|
|
271
284
|
|
|
272
285
|
Sprawdzaj `data`/`oldData`: oba obecne = update, tylko `data` = create, tylko `oldData` = delete.
|
|
273
286
|
|
|
287
|
+
## Cron-service — harmonogramy, interwały i UI admina
|
|
288
|
+
|
|
289
|
+
- Dla wykonywania **triggerów** wg **czasu ściennego** lub **stałego interwału** używaj **`@live-change/cron-service`** (**Schedule** / **Interval**) wraz z **task-service** — nie planuj „tylko timera” bez modeli cron i cyklu życia **`changeCron_Schedule`** / **`changeCron_Interval`**.
|
|
290
|
+
- Wzorzec admina (jak **task-frontend**): **`setSchedule`** / **`setInterval`** przez **`ActionForm`**, listy przez **`path.cron.schedules`** / **`path.cron.intervals`**, wzbogacanie wierszy **`.with()`** o **`scheduleInfo`** / **`intervalInfo`**, **`runState`** (`jobType` **`cron_Schedule`** lub **`cron_Interval`**), **`task.tasksByCauseAndCreatedAt`**; usuwanie przez **`deleteSchedule`** / **`deleteInterval`**.
|
|
291
|
+
- Pola czasu **Schedule** (**minute**, **hour**, **day**, **dayOfWeek**, **month**): **`NaN`** = „każdy” na danym poziomie — szczegóły w **`15-cron-and-intervals.md`** (sekcja **API used by task-frontend**).
|
|
292
|
+
|
|
274
293
|
## Wzorzec „pending + resolve” (asynchroniczny wynik)
|
|
275
294
|
|
|
276
295
|
- Używaj, gdy akcja w serwisie musi poczekać na wynik z zewnętrznego procesu (np. urządzenie, worker).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Rules for defining models, relations, indexes and access control in LiveChange
|
|
3
|
-
globs: **/services/**/*.js
|
|
3
|
+
globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
|
|
4
4
|
alwaysApply: false
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -36,6 +36,29 @@ properties: {
|
|
|
36
36
|
}
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## Arity relacji (bardzo ważne)
|
|
40
|
+
|
|
41
|
+
Rozróżniaj dwa poziomy:
|
|
42
|
+
|
|
43
|
+
- **arity adnotacji** — czy można podać listę konfiguracji
|
|
44
|
+
- **arity rodziców w konfiguracji** — `what: [A, B]` lub `to: ['owner', 'topic']`
|
|
45
|
+
|
|
46
|
+
| Relacja | Arity adnotacji | Arity rodziców |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `propertyOf` | jedna konfiguracja | `what` może być pojedyncze lub tablica modeli |
|
|
49
|
+
| `itemOf` | jedna konfiguracja | `what` może być pojedyncze lub tablica modeli |
|
|
50
|
+
| `boundTo` | jedna konfiguracja | `what` może być pojedyncze lub tablica modeli |
|
|
51
|
+
| `relatedTo` | jedna lub wiele konfiguracji | w każdej konfiguracji `what` może być pojedyncze lub tablica modeli |
|
|
52
|
+
| `propertyOfAny` | jedna konfiguracja | `to` może mieć jedną lub wiele nazw |
|
|
53
|
+
| `itemOfAny` | jedna konfiguracja | `to` może mieć jedną lub wiele nazw |
|
|
54
|
+
| `boundToAny` | jedna konfiguracja | `to` może mieć jedną lub wiele nazw |
|
|
55
|
+
| `relatedToAny` | jedna lub wiele konfiguracji | w każdej konfiguracji `to` może mieć jedną lub wiele nazw |
|
|
56
|
+
|
|
57
|
+
Przykłady:
|
|
58
|
+
|
|
59
|
+
- poprawnie: `propertyOf: { what: [A, B] }`
|
|
60
|
+
- niepoprawnie: `propertyOf: [configA, configB]`
|
|
61
|
+
|
|
39
62
|
## `userItem` – zasób należący do użytkownika
|
|
40
63
|
|
|
41
64
|
Używaj, gdy model należy do zalogowanego użytkownika.
|
|
@@ -123,7 +146,7 @@ Wtedy:
|
|
|
123
146
|
|
|
124
147
|
- **nie** przechowuj „drugiej strony” jako zwykłego `contractorId` (albo ogólnie `someId`) w `properties`
|
|
125
148
|
- **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
|
|
149
|
+
- zamiast tego zdefiniuj jedną relację `propertyOf` z `what: [ModelA, ModelB, ...]`
|
|
127
150
|
|
|
128
151
|
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
152
|
|
|
@@ -138,10 +161,9 @@ definition.model({
|
|
|
138
161
|
properties: {
|
|
139
162
|
// dodatkowe pola relacji (opcjonalnie)
|
|
140
163
|
},
|
|
141
|
-
propertyOf:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
]
|
|
164
|
+
propertyOf: {
|
|
165
|
+
what: [CostInvoice, Contractor]
|
|
166
|
+
}
|
|
145
167
|
})
|
|
146
168
|
```
|
|
147
169
|
|
|
@@ -186,7 +208,7 @@ Relacje automatycznie dodają **pola identyfikatorów** i **indeksy** do modelu.
|
|
|
186
208
|
| `propertyOfAny: { ownerTypes: [...] }` | `ownerType`, `owner` | `byOwner` (hash) |
|
|
187
209
|
| `boundTo: { what: Device }` | `device` | `byDevice` (hash) |
|
|
188
210
|
|
|
189
|
-
Dla relacji z wieloma rodzicami (np. `propertyOf:
|
|
211
|
+
Dla relacji z wieloma rodzicami (np. `propertyOf: { what: [A, B] }`) tworzone są wszystkie kombinacje indeksów (`byA`, `byB`, `byAAndB`).
|
|
190
212
|
|
|
191
213
|
## `propertyOfAny` — typy rodzica (`{name}Types`)
|
|
192
214
|
|
|
@@ -249,6 +271,13 @@ indexes: {
|
|
|
249
271
|
|
|
250
272
|
Poza serwisem indeks bywa widoczny pod nazwą z prefiksem serwisu, np. `myService_Model_byDeviceAndStatus`.
|
|
251
273
|
|
|
274
|
+
### Kiedy indeks ma być poza modelem
|
|
275
|
+
|
|
276
|
+
Jeśli indeks jest unią/projekcją danych z kilku równorzędnych tabel (bez jednego naturalnego właściciela), nie wciskaj go do `model.indexes`.
|
|
277
|
+
|
|
278
|
+
- Użyj serwisowego `definition.index(...)`, najlepiej w osobnym `indexes.js`.
|
|
279
|
+
- To dotyczy szczególnie indeksów łączących różne typy encji w jeden strumień odczytu.
|
|
280
|
+
|
|
252
281
|
### Indeksy `function` dla pól wyliczanych
|
|
253
282
|
|
|
254
283
|
Gdy część klucza indeksu nie istnieje jako zwykłe pole modelu (np. `month` wyliczany z `date`), użyj indeksu `function` zamiast `property`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: LiveChange backend — views/viewGet for reads; actions/triggers + emit for writes (CQRS-like)
|
|
3
|
+
globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# LiveChange backend — widoki do odczytu, akcja/trigger do zapisu
|
|
8
|
+
|
|
9
|
+
Na serwerze **inne ścieżki niż na froncie**: nie ma `live` / `useFetch` — odczyt definiujesz jako **`definition.view`** i ewentualnie wywołujesz go z kodu przez **`app.viewGet`** / **`app.serviceViewGet`**.
|
|
10
|
+
|
|
11
|
+
## Odczyt
|
|
12
|
+
|
|
13
|
+
- **Widok** (`definition.view`) — jedyny kanał wystawienia danych do klienta (DAO, indeksy, wyliczenia, `fetch` zewnętrzny). Może zwracać dane złożone lub wyliczone (np. podgląd „następnego numeru”).
|
|
14
|
+
- **Z kodu serwera** (trigger, akcja, batch):
|
|
15
|
+
- `await app.viewGet('viewName', { ...properties })` — widok **w tym samym** serwisie co `app`.
|
|
16
|
+
- `await app.serviceViewGet('otherService', 'viewName', { ...properties })` — widok w **innym** serwisie.
|
|
17
|
+
- Bezpośredni odczyt modelu (`Model.get`, `indexObjectGet`, …) jest OK tam, gdzie nie potrzebujesz warstwy widoku/access control widoku — ale **nie** zamieniaj tego na „akcję tylko po to, żeby coś zwrócić”.
|
|
18
|
+
|
|
19
|
+
## Zapis / zmiana stanu
|
|
20
|
+
|
|
21
|
+
- **Akcja** (`definition.action`) i **trigger** (`definition.trigger`) — walidacja, orchestracja, **`emit`** do eventów, **`trigger`** / **`triggerService`** do zapisu zgodnie z [live-change-backend-event-sourcing.mdc](./live-change-backend-event-sourcing.mdc).
|
|
22
|
+
- Nie dodawaj `definition.action`, której jedynym celem jest zwrócenie danych **bez** trwałej zmiany stanu — użyj **widoku**.
|
|
23
|
+
|
|
24
|
+
## Antywzorzec
|
|
25
|
+
|
|
26
|
+
- Akcja/command po stronie klienta tylko po to, żeby pobrać podgląd — błąd projektu po obu stronach: na backendzie powinien być **widok**, na froncie `live`/`useFetch` (reguła frontendowa).
|
|
27
|
+
|
|
28
|
+
Szczegóły: `live-change-stack/docs/docs/server/07-views.md`, `06-actions.md`, `08-triggers.md`. API klienta (Vue) — `live-change-stack/docs/docs/frontend/04-logic-and-data-layer.md`, nie ta reguła.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Zasady komunikacji przez surowy protokół DAO (C++, Python, inne klienty)
|
|
3
|
+
globs: *.cpp, *.py, *.rs, *.go
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# LiveChange DAO Protocol Arguments
|
|
7
|
+
|
|
8
|
+
When communicating with the LiveChange framework using the raw `@live-change/dao` protocol (e.g., from C++, Python, Rust, Go, or any other non-JS client), you MUST ALWAYS pass arguments as an **array**.
|
|
9
|
+
|
|
10
|
+
The framework treats DAO request and observable arguments like function arguments and uses the spread operator (`...args`) to pass them to the underlying action or view functions. If you pass an object instead of an array, the server will throw a `TypeError: Spread syntax requires ...iterable[Symbol.iterator] to be a function`.
|
|
11
|
+
|
|
12
|
+
Even if an action or view expects a single object as its parameter, that object MUST be wrapped in a single-element array.
|
|
13
|
+
|
|
14
|
+
## C++ Example (using nlohmann/json)
|
|
15
|
+
|
|
16
|
+
### Incorrect ❌
|
|
17
|
+
```cpp
|
|
18
|
+
nlohmann::json args = {
|
|
19
|
+
{"pairingKey", "123"},
|
|
20
|
+
{"connectionType", "device"}
|
|
21
|
+
};
|
|
22
|
+
connection->request({"serviceName", "actionName"}, args, settings);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Correct ✅
|
|
26
|
+
```cpp
|
|
27
|
+
// Wrap the object in an array
|
|
28
|
+
auto args = {
|
|
29
|
+
nlohmann::json::object({
|
|
30
|
+
{"pairingKey", "123"},
|
|
31
|
+
{"connectionType", "device"}
|
|
32
|
+
})
|
|
33
|
+
};
|
|
34
|
+
connection->request({"serviceName", "actionName"}, args, settings);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or explicitly:
|
|
38
|
+
```cpp
|
|
39
|
+
nlohmann::json args = nlohmann::json::array({
|
|
40
|
+
nlohmann::json::object({
|
|
41
|
+
{"pairingKey", "123"},
|
|
42
|
+
{"connectionType", "device"}
|
|
43
|
+
})
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Always double-check that your `args` payload is an array before sending it over the DAO connection.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: LiveChange frontend — use views (live/useFetch) for reads; commands only for mutations
|
|
3
|
+
globs: **/front/**/*.{vue,js,ts}
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# LiveChange frontend — odczyt przez widoki, nie przez command
|
|
8
|
+
|
|
9
|
+
Ten sam **model mentalny** co na backendzie (odczyt vs zapis), ale **inne API**: na froncie nie ma `app.viewGet` — klient korzysta ze **ścieżek widoków** z `usePath()`.
|
|
10
|
+
|
|
11
|
+
## Odczyt danych
|
|
12
|
+
|
|
13
|
+
- Buduj ścieżkę: `usePath()` (tylko synchronicznie w `setup`) → `path.<service>.<viewName>({ ...params })`; w `computed` używaj już utworzonego obiektu `path`, nie wywołuj ponownie `usePath()` / `path()`.
|
|
14
|
+
- Subskrypcja: `live(computed(() => path...))` albo `await Promise.all([live(...), ...])`.
|
|
15
|
+
- Jednorazowo: `useFetch(path.service.view({ ... }))` (np. w handlerze, po uploadzie).
|
|
16
|
+
- Dla niezależnych odczytów uruchamiaj `live(...)` równolegle (`Promise.all`), a sekwencyjnie tylko gdy drugi path zależy od wyniku pierwszego.
|
|
17
|
+
- W `Path.with(...)` buduj deklaratywnie ścieżki; do branchingu po typie/polu używaj `$switch`, nie runtime `if` na callbacku `.with`.
|
|
18
|
+
|
|
19
|
+
Nie używaj `api.command` ani `useActions()` do „załadowania” danych, podglądu, wyliczenia tylko do UI, ani „jakiego będzie następny numer” — to **zawsze** powinno iść przez **widok** (`definition.view` po stronie serwera) i `live` / `useFetch` po stronie klienta.
|
|
20
|
+
|
|
21
|
+
## Mutacja stanu
|
|
22
|
+
|
|
23
|
+
- `api.command(['service', 'actionName'], props)` lub `useActions()` → wywołanie **akcji** zmieniającej trwały stan (create/update/delete przez eventy itd.).
|
|
24
|
+
- `workingZone` i spinner przy commandach mają sens przy **zapisie**, nie przy odczycie.
|
|
25
|
+
|
|
26
|
+
## Antywzorzec
|
|
27
|
+
|
|
28
|
+
- Wywołanie akcji/command tylko po to, żeby dostać zwrócony string (np. podgląd numeru faktury) **bez** zmiany bazy — **źle**. Zamiast tego dodaj **widok** zwracający ten podgląd i pobierz go przez `live` / `useFetch`.
|
|
29
|
+
|
|
30
|
+
Szczegóły API: `live-change-stack/docs/docs/frontend/04-logic-and-data-layer.md`. Backend (`app.viewGet`, triggery): reguła `live-change-backend-views-vs-triggers-for-reads-writes.mdc` — nie stosuj jej w plikach Vue.
|
|
@@ -6,10 +6,6 @@ alwaysApply: false
|
|
|
6
6
|
|
|
7
7
|
# Frontend na live-change-stack – Vue 3 + PrimeVue + Tailwind
|
|
8
8
|
|
|
9
|
-
## i18n i `front/locales`
|
|
10
|
-
|
|
11
|
-
Za każdym razem gdy dodajesz lub zmieniasz teksty pod tłumaczenia, stosuj **`live-change-frontend-i18n-locales`**: te same klucze muszą trafić do **wszystkich** plików w **`front/locales/`** danej aplikacji (każdy język / wariant — nie tylko jeden plik).
|
|
12
|
-
|
|
13
9
|
## Stack
|
|
14
10
|
|
|
15
11
|
- Vue 3 + TypeScript
|
|
@@ -23,6 +19,8 @@ Za każdym razem gdy dodajesz lub zmieniasz teksty pod tłumaczenia, stosuj **`l
|
|
|
23
19
|
- **Nie** używaj `ref(null)` + `onMounted` do ładowania danych.
|
|
24
20
|
- Dane ładuj w `setup`/`script setup` przez `await Promise.all(...)` na `live(path()....)`.
|
|
25
21
|
- Rodzic powinien owijać stronę w `<Suspense>` (w projektach live-change robi to zazwyczaj `ViewRoot` globalnie).
|
|
22
|
+
- Sekwencyjnego `await live(...)` używaj tylko wtedy, gdy drugi path zależy od wyniku pierwszego.
|
|
23
|
+
- Gdy zależność da się opisać przez `.with(...)`, preferuj jedno zapytanie Path DSL zamiast imperatywnego łączenia wyników.
|
|
26
24
|
|
|
27
25
|
Przykład:
|
|
28
26
|
|
|
@@ -68,6 +66,50 @@ Schemat decyzji:
|
|
|
68
66
|
2. Edytuje rekord modelu (create/update)? → **Tak**: użyj `editorData`. **Nie**: użyj `actionData`.
|
|
69
67
|
3. `<command-form>` tylko do najprostszych jednorazowych przypadków.
|
|
70
68
|
|
|
69
|
+
## Autosave helpery – `synchronized` i `synchronizedList`
|
|
70
|
+
|
|
71
|
+
Używaj helperów dla danych z `live(...)`:
|
|
72
|
+
|
|
73
|
+
- `synchronized` dla pojedynczego obiektu do edycji.
|
|
74
|
+
- `synchronizedList` dla edytowalnych list (wierszy).
|
|
75
|
+
- Kontekst wspólny przekazuj przez `identifiers`, a identyfikatory wiersza przez `objectIdentifiers`.
|
|
76
|
+
- Dla draftów z payloadem zagnieżdżonym używaj `updateDataProperty: 'data'`.
|
|
77
|
+
|
|
78
|
+
Kiedy traktować ekran jako listę:
|
|
79
|
+
|
|
80
|
+
- UI ma `v-for` z wieloma edytowalnymi wierszami (tabela/lista konfiguracji/admin).
|
|
81
|
+
- Użytkownik edytuje wiele rekordów inline, a zapis ma działać per wiersz.
|
|
82
|
+
- Akcje backendowe potrzebują wspólnego kontekstu listy i kluczy konkretnego wiersza.
|
|
83
|
+
|
|
84
|
+
W takich przypadkach stosuj jeden `synchronizedList(...)` i edytuj dane bezpośrednio przez `syncList.value`.
|
|
85
|
+
Dla takich list nie buduj osobnej mapy `id -> synchronized(...)`.
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
const sync = synchronized({
|
|
89
|
+
source: sourceRef,
|
|
90
|
+
update: actions.service.updateThing,
|
|
91
|
+
identifiers: { thing: thingId },
|
|
92
|
+
recursive: true,
|
|
93
|
+
autoSave: true,
|
|
94
|
+
debounce: 600
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const syncList = synchronizedList({
|
|
98
|
+
source: rowsRef,
|
|
99
|
+
update: actions.service.updateRow,
|
|
100
|
+
delete: actions.service.deleteRow,
|
|
101
|
+
identifiers: { object, objectType },
|
|
102
|
+
objectIdentifiers: row => ({ row: row.to, object, objectType }),
|
|
103
|
+
recursive: true
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Przykłady ekranów, gdzie to dotyczy:
|
|
108
|
+
|
|
109
|
+
- listy uprawnień i ról,
|
|
110
|
+
- słowniki i konfiguracje wielowierszowe,
|
|
111
|
+
- tabele administracyjne z inline edycją pól.
|
|
112
|
+
|
|
71
113
|
### `api.command`
|
|
72
114
|
|
|
73
115
|
```js
|
|
@@ -187,6 +229,8 @@ const articlePath = computed(() => path.blog.article({ article: unref(articleId)
|
|
|
187
229
|
const [article] = await Promise.all([live(articlePath)])
|
|
188
230
|
```
|
|
189
231
|
|
|
232
|
+
`usePath()` wywołuj synchronicznie w `setup` i zapisz wynik w zmiennej `path`. W getterze `computed` buduj tylko `path.<service>.<view>(...)` — nie wywołuj ponownie `usePath()` ani aliasu `path()` w środku `computed` (brak aktywnej instancji → błąd przy `appContext`).
|
|
233
|
+
|
|
190
234
|
Dla warunkowego ładowania (np. tylko gdy zalogowany), zwróć wartość falsy:
|
|
191
235
|
|
|
192
236
|
```js
|
|
@@ -211,6 +255,21 @@ path.blog.articles({})
|
|
|
211
255
|
|
|
212
256
|
Dostęp: `article.authorProfile?.firstName`. Działa zarówno z `live()` jak i `RangeViewer`.
|
|
213
257
|
|
|
258
|
+
### Guardrails Path DSL (`.with`, `$switch`)
|
|
259
|
+
|
|
260
|
+
- Callback `.with(item => ...)` traktuj jako deklarację query DSL, nie jako kod biznesowy wykonywany na realnym rekordzie.
|
|
261
|
+
- W callbacku `.with` nie rób side effects, `api.command`, ani imperatywnego `if/else` porównującego pola proxy.
|
|
262
|
+
- Dla warunkowego wyboru ścieżki używaj `item.field.$switch({...}).$bind('target')`.
|
|
263
|
+
|
|
264
|
+
```js
|
|
265
|
+
path.accounting.settlementsByTransaction({ transactionType, transaction, range })
|
|
266
|
+
.with(settlement => settlement.subjectType.$switch({
|
|
267
|
+
invoice_CostInvoice: path.invoice.costInvoice({ costInvoice: settlement.subject }),
|
|
268
|
+
invoice_IncomeInvoice: path.invoice.incomeInvoice({ incomeInvoice: settlement.subject }),
|
|
269
|
+
hr_CivilContract: path.hr.civilContract({ civilContract: settlement.subject })
|
|
270
|
+
}).$bind('subjectDoc'))
|
|
271
|
+
```
|
|
272
|
+
|
|
214
273
|
## Listy zakresowe z reaktywnymi filtrami
|
|
215
274
|
|
|
216
275
|
Jeśli `pathFunction` dla listy zakresowej zależy od reaktywnych filtrów (np. miesiąc/status/szukaj), preferuj `ReactiveRangeViewer`.
|
|
@@ -232,6 +291,13 @@ Zasady:
|
|
|
232
291
|
/>
|
|
233
292
|
```
|
|
234
293
|
|
|
294
|
+
## Guardrails dla kursora zakresu (`RangeViewer` / `rangeBuckets`)
|
|
295
|
+
|
|
296
|
+
- Dla list opartych o indeks backend powinien udostępniać widoki oparte o `sortedIndexRangePath`.
|
|
297
|
+
- Nigdy nie nadpisuj `range.gt/gte/lt/lte` we frontendowym `pathFunction`.
|
|
298
|
+
- Pole `range` zostaw jako kursor paginacji; filtry domenowe (`month`, `year`, `status`) przekazuj osobno.
|
|
299
|
+
- Jeśli pojawia się pokusa ręcznego przepisywania kursora, przenieś logikę do projektu indeksu po stronie backendu (ew. fallback `prefixRange`), nie do hacków we froncie.
|
|
300
|
+
|
|
235
301
|
## WorkingZone dla akcji asynchronicznych
|
|
236
302
|
|
|
237
303
|
`ViewRoot` opakowuje każdą stronę w `<WorkingZone>`. Używaj `inject('workingZone')` dla akcji przycisków poza formularzami:
|
|
@@ -112,6 +112,21 @@ definition.trigger({
|
|
|
112
112
|
|
|
113
113
|
This means: when a user creates a Schedule via the UI or API, the timer is automatically set up. When they update it, the old timer is canceled and a new one created. When they delete it, the timer is canceled.
|
|
114
114
|
|
|
115
|
+
## Cron-service — planning and admin UI guardrails
|
|
116
|
+
|
|
117
|
+
When the domain needs **wall-clock schedules** or **fixed repeating intervals** that run a **trigger**, default to **`@live-change/cron-service`** (models **Schedule** / **Interval**, internal **timer** + **changeCron_*** lifecycle), not ad-hoc timers only.
|
|
118
|
+
|
|
119
|
+
**Backend:** define the **target `definition.trigger`** in your service; put **Schedule** / **Interval** rows in **cron** with **`trigger: { name, service, properties, returnTask }`**. Rely on **`changeCron_Schedule`** / **`changeCron_Interval`** for timer repair (already implemented in cron-service).
|
|
120
|
+
|
|
121
|
+
**Admin / task-frontend-style UI:** use the same integration as the reference pages:
|
|
122
|
+
|
|
123
|
+
- **Create:** `ActionForm` with `service="cron"` and `action="setSchedule"` or `action="setInterval"` (relations-driven forms).
|
|
124
|
+
- **List:** `RangeViewer` + `path.cron.schedules` / `path.cron.intervals` with **`reverseRange(range)`** as needed.
|
|
125
|
+
- **Per row:** `.with()` → `scheduleInfo` / `intervalInfo`, `runState` (`jobType` **`cron_Schedule`** or **`cron_Interval`**, **`job`** = id), and `task.tasksByCauseAndCreatedAt` for recent runs.
|
|
126
|
+
- **Delete:** `api.actions.cron.deleteSchedule` / `deleteInterval`.
|
|
127
|
+
|
|
128
|
+
See **server doc** `15-cron-and-intervals.md` → section **“API used by task-frontend”** for path examples and **Schedule** field semantics (**`NaN`** = “every” for that field).
|
|
129
|
+
|
|
115
130
|
## Step 4 – Specific lifecycle triggers (alternative)
|
|
116
131
|
|
|
117
132
|
If you only care about one lifecycle event, use the specific variant:
|
|
@@ -79,6 +79,57 @@ definition.action({
|
|
|
79
79
|
- use model paths (`Model.path`, `Model.rangePath`, `Model.sortedIndexRangePath`, `Model.indexObjectPath`)
|
|
80
80
|
- use `...App.rangeProperties` + `App.extractRange(props)` for range views
|
|
81
81
|
|
|
82
|
+
### Step 2a – RangeViewer/rangeBuckets compatibility
|
|
83
|
+
|
|
84
|
+
When a view is consumed by `RangeViewer` or `rangeBuckets`:
|
|
85
|
+
|
|
86
|
+
- prefer `Model.sortedIndexRangePath(...)` for index-backed list views,
|
|
87
|
+
- keep `App.extractRange(props)` as pagination cursor input,
|
|
88
|
+
- do not reinterpret `gt/gte/lt/lte` as domain filters.
|
|
89
|
+
|
|
90
|
+
Anti-patterns:
|
|
91
|
+
|
|
92
|
+
- using `indexRangePath` for frontend bucket pagination flow,
|
|
93
|
+
- injecting custom month/year bounds into cursor fields in frontend,
|
|
94
|
+
- rewriting cursor values in backend with unrelated filter semantics.
|
|
95
|
+
|
|
96
|
+
Preferred filtering strategy:
|
|
97
|
+
|
|
98
|
+
1. design index prefix for frequent filters,
|
|
99
|
+
2. use `App.utils.prefixRange` only as backend fallback,
|
|
100
|
+
3. keep string min/max hacks as last resort.
|
|
101
|
+
|
|
102
|
+
### Step 2b – Standalone indexes for union/equal sources
|
|
103
|
+
|
|
104
|
+
When index rows are built from multiple equal tables (union-like flow), do not force the index into one model definition.
|
|
105
|
+
|
|
106
|
+
Use `definition.index(...)` at service level (typically `indexes.js`) when:
|
|
107
|
+
|
|
108
|
+
- index combines rows from two or more source tables,
|
|
109
|
+
- source tables are peer entities (no natural single owner model),
|
|
110
|
+
- index is a projection layer for cross-table reads.
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
definition.index({
|
|
116
|
+
name: 'Urls',
|
|
117
|
+
function: async (input, output) => {
|
|
118
|
+
await input.table('url_Redirect').onChange((obj, oldObj) =>
|
|
119
|
+
output.change(obj && mapRedirect(obj), oldObj && mapRedirect(oldObj))
|
|
120
|
+
)
|
|
121
|
+
await input.table('url_Canonical').onChange((obj, oldObj) =>
|
|
122
|
+
output.change(obj && mapCanonical(obj), oldObj && mapCanonical(oldObj))
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Decision rule:
|
|
129
|
+
|
|
130
|
+
- model-local index -> `definition.model({ indexes: ... })`,
|
|
131
|
+
- union/peer-source index -> standalone `definition.index(...)` in `indexes.js`.
|
|
132
|
+
|
|
82
133
|
### Example: `daoPath` (preferred, DAO-backed)
|
|
83
134
|
|
|
84
135
|
```js
|
|
@@ -62,6 +62,25 @@ properties: {
|
|
|
62
62
|
}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
## Step 2b – Relation arity rules (critical)
|
|
66
|
+
|
|
67
|
+
Treat arity on two levels:
|
|
68
|
+
|
|
69
|
+
- **Annotation arity**: can the annotation itself be a list of configs?
|
|
70
|
+
- **Parent tuple arity**: can one config point to multiple parents/dimensions?
|
|
71
|
+
|
|
72
|
+
| Relation | Annotation arity | Parent tuple arity |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `propertyOf`, `itemOf`, `boundTo` | single config only | `what` can be one model or `[A, B, ...]` |
|
|
75
|
+
| `relatedTo` | single config or config list | each config uses `what` with one model or `[A, B, ...]` |
|
|
76
|
+
| `propertyOfAny`, `itemOfAny`, `boundToAny` | single config only | `to` can contain one or many names |
|
|
77
|
+
| `relatedToAny` | single config or config list | each config uses `to` with one or many names |
|
|
78
|
+
|
|
79
|
+
Guardrail:
|
|
80
|
+
|
|
81
|
+
- valid: `propertyOf: { what: [A, B] }`
|
|
82
|
+
- invalid: `propertyOf: [configA, configB]`
|
|
83
|
+
|
|
65
84
|
## Step 3 – Configure the relation
|
|
66
85
|
|
|
67
86
|
### `userItem`
|
|
@@ -110,7 +129,7 @@ so the relations/CRUD generator can treat it as a relation rather than a plain `
|
|
|
110
129
|
|
|
111
130
|
Notes:
|
|
112
131
|
|
|
113
|
-
- Usually you’ll have 1–2 parents, but
|
|
132
|
+
- Usually you’ll have 1–2 parents, but `what` may contain **any number** of parent models (including 3+).
|
|
114
133
|
- If the entity is a relation, avoid adding manual `...Id` fields in `properties` just to represent the link — CRUD generators won’t treat it as a relation.
|
|
115
134
|
|
|
116
135
|
Example:
|
|
@@ -124,10 +143,9 @@ definition.model({
|
|
|
124
143
|
properties: {
|
|
125
144
|
// optional extra fields
|
|
126
145
|
},
|
|
127
|
-
propertyOf:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
]
|
|
146
|
+
propertyOf: {
|
|
147
|
+
what: [CostInvoice, Contractor]
|
|
148
|
+
}
|
|
131
149
|
})
|
|
132
150
|
```
|
|
133
151
|
|
|
@@ -38,6 +38,21 @@ const [article, comments] = await Promise.all([
|
|
|
38
38
|
])
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
- Call `usePath()` **once** at the top of `setup` (synchronously). Inside `computed`, use only the returned `path` object to build paths — **never** call `usePath()` or the legacy `path()` inside the getter (there is often no active component instance, which breaks `getCurrentInstance()` / `appContext`).
|
|
42
|
+
|
|
43
|
+
Wrong:
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
computed(() => usePath().blog.article({ article: id }))
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Right:
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
const path = usePath()
|
|
53
|
+
const articlePath = computed(() => path.blog.article({ article: id }))
|
|
54
|
+
```
|
|
55
|
+
|
|
41
56
|
In templates access `.value`:
|
|
42
57
|
|
|
43
58
|
```vue
|