@live-change/frontend-template 0.9.198 → 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,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Design actions, views, triggers with indexes and batch processing patterns
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Skill: live-change-design-actions-views-triggers (Claude Code)
|
|
6
|
+
|
|
7
|
+
Use this skill to design **actions, views, and triggers** in LiveChange services while making good use of indexes and avoiding full-table scans.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- You add or change actions on existing models.
|
|
12
|
+
- You define new views (especially list/range views).
|
|
13
|
+
- You implement triggers (online/offline, batch processing, async result flows).
|
|
14
|
+
|
|
15
|
+
## Step 1 – Design an action
|
|
16
|
+
|
|
17
|
+
1. **Clarify the goal**:
|
|
18
|
+
- create / update / delete a record,
|
|
19
|
+
- or create a “command” that will be completed later.
|
|
20
|
+
2. **Define `properties`** clearly:
|
|
21
|
+
- only include what the client must provide,
|
|
22
|
+
- fetch the rest from the database via indexes.
|
|
23
|
+
3. **Use indexes**, not full scans:
|
|
24
|
+
- `indexObjectGet('bySomething', { ... })` for single-object lookups,
|
|
25
|
+
- `indexRangeGet('bySomething', { ... })` for lists.
|
|
26
|
+
4. **Return a useful result**:
|
|
27
|
+
- new object id,
|
|
28
|
+
- session keys,
|
|
29
|
+
- any data needed for the next step.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
definition.action({
|
|
35
|
+
name: 'someAction',
|
|
36
|
+
properties: {
|
|
37
|
+
someKey: { type: String }
|
|
38
|
+
},
|
|
39
|
+
async execute({ someKey }, { client, service }) {
|
|
40
|
+
const obj = await SomeModel.indexObjectGet('bySomeKey', { someKey })
|
|
41
|
+
if(!obj) throw new Error('notFound')
|
|
42
|
+
|
|
43
|
+
const id = app.generateUid()
|
|
44
|
+
|
|
45
|
+
await SomeOtherModel.create({
|
|
46
|
+
id
|
|
47
|
+
// ...
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return { id }
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Step 2 – Design a view
|
|
56
|
+
|
|
57
|
+
1. Decide if you need:
|
|
58
|
+
- a **single** object view, or
|
|
59
|
+
- a **list/range** view.
|
|
60
|
+
2. Define `properties` for the view:
|
|
61
|
+
- only parameters needed for filtering,
|
|
62
|
+
- types consistent with model fields.
|
|
63
|
+
3. Use the right primitive:
|
|
64
|
+
- `get` / `indexObjectGet` for single object,
|
|
65
|
+
- `indexRangeGet` for lists.
|
|
66
|
+
|
|
67
|
+
Range view example:
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
definition.view({
|
|
71
|
+
name: 'myItemsByStatus',
|
|
72
|
+
properties: {
|
|
73
|
+
status: { type: String }
|
|
74
|
+
},
|
|
75
|
+
async get({ status }, { client, service }) {
|
|
76
|
+
return MyModel.indexRangeGet('byStatus', { status })
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Step 3 – Online/offline triggers
|
|
82
|
+
|
|
83
|
+
1. Identify events:
|
|
84
|
+
- session or connection goes online,
|
|
85
|
+
- session or connection goes offline.
|
|
86
|
+
2. Define triggers with minimal `properties` (usually just an id).
|
|
87
|
+
3. Update only the necessary fields (`status`, `lastSeenAt`, etc.).
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
definition.trigger({
|
|
93
|
+
name: 'sessionConnectionOnline',
|
|
94
|
+
properties: {
|
|
95
|
+
connection: { type: String }
|
|
96
|
+
},
|
|
97
|
+
async execute({ connection }, { service }) {
|
|
98
|
+
await Connection.update(connection, {
|
|
99
|
+
status: 'online',
|
|
100
|
+
lastSeenAt: new Date()
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
definition.trigger({
|
|
106
|
+
name: 'sessionConnectionOffline',
|
|
107
|
+
properties: {
|
|
108
|
+
connection: { type: String }
|
|
109
|
+
},
|
|
110
|
+
async execute({ connection }, { service }) {
|
|
111
|
+
await Connection.update(connection, {
|
|
112
|
+
status: 'offline'
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Step 4 – Batch triggers (avoid full scans)
|
|
119
|
+
|
|
120
|
+
1. Pick a **batch size** (e.g. 32 or 128).
|
|
121
|
+
2. Use `rangeGet` with `gt: lastId` in a loop:
|
|
122
|
+
- start with `last = ''`,
|
|
123
|
+
- after each batch, set `last` to the last record’s id,
|
|
124
|
+
- stop when the batch is empty.
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
definition.trigger({
|
|
130
|
+
name: 'allOffline',
|
|
131
|
+
async execute({}, { service }) {
|
|
132
|
+
let last = ''
|
|
133
|
+
while(true) {
|
|
134
|
+
const items = await Connection.rangeGet({
|
|
135
|
+
gt: last,
|
|
136
|
+
limit: 32
|
|
137
|
+
})
|
|
138
|
+
if(items.length === 0) break
|
|
139
|
+
|
|
140
|
+
for(const item of items) {
|
|
141
|
+
await Connection.update(item.id, { status: 'offline' })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
last = items[items.length - 1].id
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Step 5 – Pending + resolve pattern for async results
|
|
151
|
+
|
|
152
|
+
Use this pattern when an action initiates a command that will be completed by an external process (device, worker, etc.) and you want the action to wait with a timeout.
|
|
153
|
+
|
|
154
|
+
### Steps
|
|
155
|
+
|
|
156
|
+
1. Implement a helper module with an in-memory `Map`:
|
|
157
|
+
- `waitForCommand(id, timeoutMs)` – returns a Promise,
|
|
158
|
+
- `resolveCommand(id, result)` – resolves and clears timeout.
|
|
159
|
+
2. In the main action:
|
|
160
|
+
- create a record with `status: 'pending'`,
|
|
161
|
+
- call `waitForCommand(id, timeoutMs)` and `return` the result.
|
|
162
|
+
3. In the reporting action:
|
|
163
|
+
- update the record (`status: 'completed'`, `result`),
|
|
164
|
+
- call `resolveCommand(id, result)`.
|
|
165
|
+
|
|
166
|
+
Helper sketch:
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
const pendingCommands = new Map()
|
|
170
|
+
|
|
171
|
+
export function waitForCommand(commandId, timeoutMs = 115000) {
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const timer = setTimeout(() => {
|
|
174
|
+
pendingCommands.delete(commandId)
|
|
175
|
+
reject(new Error('timeout'))
|
|
176
|
+
}, timeoutMs)
|
|
177
|
+
pendingCommands.set(commandId, { resolve, reject, timer })
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function resolveCommand(commandId, result) {
|
|
182
|
+
const pending = pendingCommands.get(commandId)
|
|
183
|
+
if(pending) {
|
|
184
|
+
clearTimeout(pending.timer)
|
|
185
|
+
pendingCommands.delete(commandId)
|
|
186
|
+
pending.resolve(result)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Design models with userItem, itemOf, propertyOf relations and access control
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Skill: live-change-design-models-relations (Claude Code)
|
|
6
|
+
|
|
7
|
+
Use this skill when you design or refactor **models and relations** in a LiveChange service.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- You are adding a new model to a service.
|
|
12
|
+
- You want to switch from manual CRUD/views to proper relations.
|
|
13
|
+
- You need consistent access control and index usage.
|
|
14
|
+
|
|
15
|
+
## Step 1 – Decide the relation type
|
|
16
|
+
|
|
17
|
+
For each new model, decide how it relates to the rest of the domain:
|
|
18
|
+
|
|
19
|
+
- **`userItem`** – the object belongs to the signed-in user (e.g. user’s device).
|
|
20
|
+
- **`itemOf`** – a list of children belonging to a parent model (e.g. device connections).
|
|
21
|
+
- **`propertyOf`** – a single state object with the same id as the parent (e.g. cursor state).
|
|
22
|
+
- **no relation** – for global data or other special cases.
|
|
23
|
+
|
|
24
|
+
Choose one main relation; other associations can be plain fields + indexes.
|
|
25
|
+
|
|
26
|
+
## Step 2 – Define `properties` clearly
|
|
27
|
+
|
|
28
|
+
1. Use a **multi-line** style for properties, with clear `type`, `default`, `validation`, etc.
|
|
29
|
+
2. Avoid unreadable one-liners combining everything.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
properties: {
|
|
35
|
+
name: {
|
|
36
|
+
type: String,
|
|
37
|
+
validation: ['nonEmpty']
|
|
38
|
+
},
|
|
39
|
+
status: {
|
|
40
|
+
type: String,
|
|
41
|
+
default: 'offline'
|
|
42
|
+
},
|
|
43
|
+
capabilities: {
|
|
44
|
+
type: Array,
|
|
45
|
+
of: {
|
|
46
|
+
type: String
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Step 3 – Configure the relation
|
|
53
|
+
|
|
54
|
+
### `userItem`
|
|
55
|
+
|
|
56
|
+
1. Add a `userItem` block inside the model definition.
|
|
57
|
+
2. Set roles for read/write and list which fields can be written.
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
userItem: {
|
|
61
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
62
|
+
writeAccessControl: { roles: ['owner', 'admin'] },
|
|
63
|
+
writeableProperties: ['name']
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `itemOf`
|
|
68
|
+
|
|
69
|
+
1. Decide the parent model.
|
|
70
|
+
2. If the parent is in another service, declare it via `foreignModel` (see next step).
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
itemOf: {
|
|
74
|
+
what: Device,
|
|
75
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
76
|
+
writeAccessControl: { roles: ['owner', 'admin'] }
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `propertyOf`
|
|
81
|
+
|
|
82
|
+
1. Use when the child should share the same id as the parent.
|
|
83
|
+
2. This simplifies lookups and avoids extra indexes.
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
propertyOf: {
|
|
87
|
+
what: Device,
|
|
88
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
89
|
+
writeAccessControl: { roles: ['owner', 'admin'] }
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `propertyOf` with multiple parents (1:1 link to each)
|
|
94
|
+
|
|
95
|
+
Use this when a model should act as a dedicated 1:1 link between multiple entities (e.g. invoice ↔ contractor role links),
|
|
96
|
+
so the relations/CRUD generator can treat it as a relation rather than a plain `someId` property.
|
|
97
|
+
|
|
98
|
+
Notes:
|
|
99
|
+
|
|
100
|
+
- Usually you’ll have 1–2 parents, but the `propertyOf` list may contain **any number** of parent models (including 3+).
|
|
101
|
+
- 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.
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
const CostInvoice = definition.foreignModel('invoice', 'CostInvoice')
|
|
107
|
+
const Contractor = definition.foreignModel('company', 'Contractor')
|
|
108
|
+
|
|
109
|
+
definition.model({
|
|
110
|
+
name: 'Supplier',
|
|
111
|
+
properties: {
|
|
112
|
+
// optional extra fields
|
|
113
|
+
},
|
|
114
|
+
propertyOf: [
|
|
115
|
+
{ what: CostInvoice },
|
|
116
|
+
{ what: Contractor }
|
|
117
|
+
]
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Step 4 – Use `foreignModel` for cross-service relations
|
|
122
|
+
|
|
123
|
+
1. At the top of the domain file, declare:
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
const Device = definition.foreignModel('deviceManager', 'Device')
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
2. Then use `Device` in `itemOf` or `propertyOf`:
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
itemOf: {
|
|
133
|
+
what: Device,
|
|
134
|
+
readAccessControl: { roles: ['owner', 'admin'] }
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Step 5 – Add indexes
|
|
139
|
+
|
|
140
|
+
1. Identify frequent queries:
|
|
141
|
+
- by a single field (e.g. `sessionKey`),
|
|
142
|
+
- by combinations (e.g. `(device, status)`).
|
|
143
|
+
2. Declare indexes in the model:
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
indexes: {
|
|
147
|
+
bySessionKey: {
|
|
148
|
+
property: ['sessionKey']
|
|
149
|
+
},
|
|
150
|
+
byDeviceAndStatus: {
|
|
151
|
+
property: ['device', 'status']
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
3. Use these indexes in views/actions, via `indexObjectGet` / `indexRangeGet`.
|
|
157
|
+
|
|
158
|
+
## Step 6 – Set access control on relations
|
|
159
|
+
|
|
160
|
+
1. For `userItem`, `itemOf`, and `propertyOf`, always define:
|
|
161
|
+
- `readAccessControl`,
|
|
162
|
+
- `writeAccessControl`.
|
|
163
|
+
2. Don’t rely on unspecified defaults; access rules should be explicit in the model.
|
|
164
|
+
|
|
165
|
+
## Step 7 – Check auto-generated views/actions
|
|
166
|
+
|
|
167
|
+
1. After adding relations, review the auto-generated views/actions:
|
|
168
|
+
- “my X” views and CRUD for `userItem`,
|
|
169
|
+
- parent-scoped lists for `itemOf`/`propertyOf`.
|
|
170
|
+
2. Only add custom views/actions when:
|
|
171
|
+
- you need special filters,
|
|
172
|
+
- or custom logic not covered by the generated ones.
|
|
173
|
+
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Create or restructure a LiveChange backend service with proper directory layout
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Skill: live-change-design-service (Claude Code)
|
|
6
|
+
|
|
7
|
+
Use this skill when you need to **create or restructure a LiveChange service** in this project or any other live-change-stack project.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- You are adding a **new domain service** (payments, devices, notifications, etc.).
|
|
12
|
+
- You are splitting logic out of an existing service.
|
|
13
|
+
- You want to make sure the service structure follows the project conventions.
|
|
14
|
+
|
|
15
|
+
## Step 1 – Choose the service name
|
|
16
|
+
|
|
17
|
+
1. Pick a short, domain-oriented name, e.g. `deviceManager`, `payments`, `notifications`.
|
|
18
|
+
2. This name will be used:
|
|
19
|
+
- as `name` in `app.createServiceDefinition({ name })`,
|
|
20
|
+
- as the `name` entry in `app.config.js` (`services: [{ name: '...' }]`),
|
|
21
|
+
- in `services.list.js` as the property key.
|
|
22
|
+
|
|
23
|
+
## Step 2 – Create the service directory
|
|
24
|
+
|
|
25
|
+
1. Create `server/services/<serviceName>/`.
|
|
26
|
+
2. Inside, create at least:
|
|
27
|
+
- `definition.js`
|
|
28
|
+
- `index.js`
|
|
29
|
+
3. Optionally also:
|
|
30
|
+
- `config.js` – for resolving `definition.config`,
|
|
31
|
+
- domain files like `models.js`, `authenticator.js`, `actions.js`, or more fine-grained files.
|
|
32
|
+
|
|
33
|
+
## Step 3 – Implement `definition.js`
|
|
34
|
+
|
|
35
|
+
1. Import `app` from `@live-change/framework`.
|
|
36
|
+
2. If the service uses relations or access control:
|
|
37
|
+
- import `relationsPlugin` from `@live-change/relations-plugin`,
|
|
38
|
+
- import `accessControlService` from `@live-change/access-control-service`.
|
|
39
|
+
3. Call `app.createServiceDefinition({ name: '...', use: [...] })`.
|
|
40
|
+
4. **Do not register models, actions or views** in this file – only the definition.
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { app } from '@live-change/framework'
|
|
46
|
+
import relationsPlugin from '@live-change/relations-plugin'
|
|
47
|
+
import accessControlService from '@live-change/access-control-service'
|
|
48
|
+
|
|
49
|
+
const definition = app.createServiceDefinition({
|
|
50
|
+
name: 'myService',
|
|
51
|
+
use: [relationsPlugin, accessControlService]
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
export default definition
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Step 4 – Implement `index.js`
|
|
58
|
+
|
|
59
|
+
1. Import `definition` from `./definition.js`.
|
|
60
|
+
2. Import all domain files (models, views, actions, triggers, authenticators) as side-effect imports.
|
|
61
|
+
3. Export `definition` as the default export.
|
|
62
|
+
4. Do **not** put heavy logic into `index.js`.
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
import definition from './definition.js'
|
|
68
|
+
|
|
69
|
+
import './models.js'
|
|
70
|
+
import './authenticator.js'
|
|
71
|
+
import './actions.js'
|
|
72
|
+
|
|
73
|
+
export default definition
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Step 5 – (Optional) Implement `config.js`
|
|
77
|
+
|
|
78
|
+
1. Import `definition`.
|
|
79
|
+
2. Read `definition.config` and apply default values.
|
|
80
|
+
3. Export a plain object.
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
import definition from './definition.js'
|
|
86
|
+
|
|
87
|
+
const {
|
|
88
|
+
someOption = 'default'
|
|
89
|
+
} = definition.config
|
|
90
|
+
|
|
91
|
+
export default { someOption }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Step 6 – Register the service in `services.list.js`
|
|
95
|
+
|
|
96
|
+
1. Import from the service directory `index.js`:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
import myService from './services/myService/index.js'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
2. Add the service to the exported object:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
export default {
|
|
106
|
+
// ...
|
|
107
|
+
myService
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Step 7 – Register the service in `app.config.js`
|
|
112
|
+
|
|
113
|
+
1. Ensure there is an entry in `services`:
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
services: [
|
|
117
|
+
// ...
|
|
118
|
+
{ name: 'myService' }
|
|
119
|
+
]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
2. Keep a sensible order:
|
|
123
|
+
- core/common services and plugins (user, session, accessControl, etc.) first,
|
|
124
|
+
- domain-specific application services later.
|
|
125
|
+
|
|
126
|
+
## Step 8 – Handle dependencies on other services
|
|
127
|
+
|
|
128
|
+
1. If the service needs to reference models from other services:
|
|
129
|
+
- use `definition.foreignModel('otherService', 'ModelName')` in domain files,
|
|
130
|
+
- do **not** import their model files directly.
|
|
131
|
+
2. Make sure the other services are listed **before** this one in `app.config.js`.
|
|
132
|
+
|
|
@@ -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
|
+
```
|