ima-claude 2.20.0 → 2.26.0
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/README.md +74 -9
- package/dist/cli.js +2 -1
- package/package.json +1 -1
- package/plugins/ima-claude/.claude-plugin/plugin.json +2 -2
- package/plugins/ima-claude/agents/explorer.md +29 -15
- package/plugins/ima-claude/agents/implementer.md +58 -13
- package/plugins/ima-claude/agents/memory.md +19 -19
- package/plugins/ima-claude/agents/reviewer.md +84 -34
- package/plugins/ima-claude/agents/tester.md +59 -16
- package/plugins/ima-claude/agents/wp-developer.md +66 -21
- package/plugins/ima-claude/hooks/bootstrap.sh +42 -44
- package/plugins/ima-claude/hooks/prompt_coach_digest.md +14 -17
- package/plugins/ima-claude/hooks/prompt_coach_system.md +10 -12
- package/plugins/ima-claude/personalities/README.md +17 -6
- package/plugins/ima-claude/personalities/enable-efficient.md +61 -0
- package/plugins/ima-claude/personalities/enable-terse.md +71 -0
- package/plugins/ima-claude/skills/agentic-workflows/SKILL.md +35 -71
- package/plugins/ima-claude/skills/architect/SKILL.md +54 -168
- package/plugins/ima-claude/skills/compound-bridge/SKILL.md +41 -94
- package/plugins/ima-claude/skills/design-to-code/SKILL.md +43 -78
- package/plugins/ima-claude/skills/discourse/SKILL.md +79 -194
- package/plugins/ima-claude/skills/discourse-admin/SKILL.md +41 -103
- package/plugins/ima-claude/skills/docs-organize/SKILL.md +63 -203
- package/plugins/ima-claude/skills/ember-discourse/SKILL.md +90 -200
- package/plugins/ima-claude/skills/espocrm/SKILL.md +14 -23
- package/plugins/ima-claude/skills/espocrm-api/SKILL.md +79 -192
- package/plugins/ima-claude/skills/functional-programmer/SKILL.md +33 -237
- package/plugins/ima-claude/skills/gh-cli/SKILL.md +26 -65
- package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +71 -104
- package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +32 -22
- package/plugins/ima-claude/skills/ima-brand/SKILL.md +18 -23
- package/plugins/ima-claude/skills/ima-copywriting/SKILL.md +68 -179
- package/plugins/ima-claude/skills/ima-doc2pdf/SKILL.md +32 -102
- package/plugins/ima-claude/skills/ima-editorial-scorecard/SKILL.md +38 -63
- package/plugins/ima-claude/skills/ima-editorial-workflow/SKILL.md +69 -114
- package/plugins/ima-claude/skills/ima-email-creator/SKILL.md +16 -22
- package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +21 -37
- package/plugins/ima-claude/skills/ima-git/SKILL.md +81 -0
- package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +39 -120
- package/plugins/ima-claude/skills/jquery/SKILL.md +107 -233
- package/plugins/ima-claude/skills/js-fp/SKILL.md +75 -296
- package/plugins/ima-claude/skills/js-fp-api/SKILL.md +52 -162
- package/plugins/ima-claude/skills/js-fp-react/SKILL.md +47 -270
- package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +55 -209
- package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +59 -204
- package/plugins/ima-claude/skills/livecanvas/SKILL.md +19 -32
- package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +92 -162
- package/plugins/ima-claude/skills/mcp-context7/SKILL.md +32 -64
- package/plugins/ima-claude/skills/mcp-gitea/SKILL.md +98 -188
- package/plugins/ima-claude/skills/mcp-github/SKILL.md +60 -124
- package/plugins/ima-claude/skills/mcp-memory/SKILL.md +1 -177
- package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +58 -115
- package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +32 -87
- package/plugins/ima-claude/skills/mcp-serena/SKILL.md +54 -80
- package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +40 -63
- package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +75 -116
- package/plugins/ima-claude/skills/php-authnet/SKILL.md +32 -65
- package/plugins/ima-claude/skills/php-fp/SKILL.md +50 -129
- package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +25 -73
- package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +103 -463
- package/plugins/ima-claude/skills/playwright/SKILL.md +69 -220
- package/plugins/ima-claude/skills/prompt-starter/SKILL.md +33 -83
- package/plugins/ima-claude/skills/prompt-starter/references/code-review.md +38 -0
- package/plugins/ima-claude/skills/py-fp/SKILL.md +78 -384
- package/plugins/ima-claude/skills/quasar-fp/SKILL.md +54 -255
- package/plugins/ima-claude/skills/quickstart/SKILL.md +7 -11
- package/plugins/ima-claude/skills/rails/SKILL.md +63 -184
- package/plugins/ima-claude/skills/resume-session/SKILL.md +14 -35
- package/plugins/ima-claude/skills/rg/SKILL.md +61 -146
- package/plugins/ima-claude/skills/ruby-fp/SKILL.md +66 -163
- package/plugins/ima-claude/skills/save-session/SKILL.md +10 -39
- package/plugins/ima-claude/skills/scorecard/SKILL.md +42 -40
- package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +42 -71
- package/plugins/ima-claude/skills/skill-creator/SKILL.md +79 -250
- package/plugins/ima-claude/skills/task-master/SKILL.md +11 -31
- package/plugins/ima-claude/skills/task-planner/SKILL.md +44 -153
- package/plugins/ima-claude/skills/task-runner/SKILL.md +61 -143
- package/plugins/ima-claude/skills/unit-testing/SKILL.md +59 -134
- package/plugins/ima-claude/skills/wp-ddev/SKILL.md +38 -120
- package/plugins/ima-claude/skills/wp-local/SKILL.md +26 -108
|
@@ -5,29 +5,9 @@ description: "Ember/Glimmer component development for Discourse plugins - gjs, a
|
|
|
5
5
|
|
|
6
6
|
# Ember for Discourse Plugins
|
|
7
7
|
|
|
8
|
-
Discourse runs Ember Octane with Glimmer components.
|
|
8
|
+
Discourse runs Ember Octane with Glimmer components. Extension model: `apiInitializer` → `renderInOutlet` → `.gjs` component.
|
|
9
9
|
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
- Adding frontend UI to a Discourse plugin
|
|
13
|
-
- Building admin panel components for a plugin
|
|
14
|
-
- Injecting content into Discourse's UI via plugin outlets
|
|
15
|
-
- Migrating old `decorateWidget` / raw handlebars to modern Glimmer
|
|
16
|
-
|
|
17
|
-
## Core Philosophy
|
|
18
|
-
|
|
19
|
-
> Glimmer components are close to pure functions: `(args, services) → template`. Minimize `@tracked` state. Prefer derived values over stored state.
|
|
20
|
-
|
|
21
|
-
FP lens applied to Ember:
|
|
22
|
-
- **Components** are view functions — args in, DOM out
|
|
23
|
-
- **`@tracked`** is isolated reactive state — use sparingly, only what truly changes
|
|
24
|
-
- **Derived values** via getters — no duplicate state, always in sync
|
|
25
|
-
- **Actions** are the imperative shell — side effects live in methods, not templates
|
|
26
|
-
- **Services** are dependency injection — inject, don't import singletons
|
|
27
|
-
|
|
28
|
-
**Foundation**: Reference `../discourse/SKILL.md` for the Ruby side of plugin development.
|
|
29
|
-
|
|
30
|
-
## The Modern Stack (use these)
|
|
10
|
+
## Modern Stack
|
|
31
11
|
|
|
32
12
|
| What | Modern | Deprecated / Avoid |
|
|
33
13
|
|------|--------|--------------------|
|
|
@@ -39,9 +19,19 @@ FP lens applied to Ember:
|
|
|
39
19
|
| Services | `@service` decorator | `Ember.inject.service()` |
|
|
40
20
|
| Event handling | `{{on "click" this.handler}}` | `{{action "handler"}}` |
|
|
41
21
|
|
|
42
|
-
##
|
|
22
|
+
## FP Lens
|
|
43
23
|
|
|
44
|
-
|
|
24
|
+
- Components are view functions — args in, DOM out
|
|
25
|
+
- `@tracked` = isolated reactive state — use sparingly, only for state the component owns and mutates
|
|
26
|
+
- Derived values via getters — no duplicate state
|
|
27
|
+
- Actions are the imperative shell — side effects in methods, not templates
|
|
28
|
+
- Services are DI — inject via `@service`, don't import singletons
|
|
29
|
+
|
|
30
|
+
**Related**: `../discourse/SKILL.md` for Ruby plugin side.
|
|
31
|
+
|
|
32
|
+
## .gjs Format
|
|
33
|
+
|
|
34
|
+
`.gjs` combines class + template in one file. Standard for all new components.
|
|
45
35
|
|
|
46
36
|
```gjs
|
|
47
37
|
// assets/javascripts/discourse/components/my-component.gjs
|
|
@@ -57,7 +47,7 @@ export default class MyComponent extends Component {
|
|
|
57
47
|
|
|
58
48
|
@tracked isExpanded = false;
|
|
59
49
|
|
|
60
|
-
// Derived
|
|
50
|
+
// Derived — getter, not @tracked
|
|
61
51
|
get greeting() {
|
|
62
52
|
return this.currentUser
|
|
63
53
|
? `Welcome back, ${this.currentUser.username}!`
|
|
@@ -72,26 +62,21 @@ export default class MyComponent extends Component {
|
|
|
72
62
|
<template>
|
|
73
63
|
<div class="my-component">
|
|
74
64
|
<p>{{this.greeting}}</p>
|
|
75
|
-
|
|
76
65
|
<DButton
|
|
77
66
|
@action={{this.toggleExpanded}}
|
|
78
67
|
@label={{if this.isExpanded "collapse" "expand"}}
|
|
79
68
|
/>
|
|
80
|
-
|
|
81
69
|
{{#if this.isExpanded}}
|
|
82
|
-
<div class="my-component__body">
|
|
83
|
-
{{yield}}
|
|
84
|
-
</div>
|
|
70
|
+
<div class="my-component__body">{{yield}}</div>
|
|
85
71
|
{{/if}}
|
|
86
72
|
</div>
|
|
87
73
|
</template>
|
|
88
74
|
}
|
|
89
75
|
```
|
|
90
76
|
|
|
91
|
-
|
|
77
|
+
Template-only (no class needed):
|
|
92
78
|
|
|
93
79
|
```gjs
|
|
94
|
-
// When a component is purely presentational — no class required
|
|
95
80
|
<template>
|
|
96
81
|
<div class="badge-card">
|
|
97
82
|
<img src={{@badge.image_url}} alt={{@badge.name}} />
|
|
@@ -100,9 +85,9 @@ export default class MyComponent extends Component {
|
|
|
100
85
|
</template>
|
|
101
86
|
```
|
|
102
87
|
|
|
103
|
-
## apiInitializer
|
|
88
|
+
## apiInitializer
|
|
104
89
|
|
|
105
|
-
|
|
90
|
+
One initializer file per plugin feature area.
|
|
106
91
|
|
|
107
92
|
```javascript
|
|
108
93
|
// assets/javascripts/discourse/initializers/my-plugin.js
|
|
@@ -111,17 +96,14 @@ import MyBanner from "../components/my-banner";
|
|
|
111
96
|
import MyComponent from "../components/my-component";
|
|
112
97
|
|
|
113
98
|
export default apiInitializer((api) => {
|
|
114
|
-
// Render into a plugin outlet
|
|
115
99
|
api.renderInOutlet("discovery-list-container-top", MyBanner);
|
|
116
|
-
|
|
117
|
-
// shouldRender on the component class controls conditional rendering
|
|
118
100
|
api.renderInOutlet("topic-above-post-stream", MyComponent);
|
|
119
101
|
});
|
|
120
102
|
```
|
|
121
103
|
|
|
122
|
-
## Plugin Outlets
|
|
104
|
+
## Plugin Outlets
|
|
123
105
|
|
|
124
|
-
|
|
106
|
+
Inject at pre-defined template hooks via `api.renderInOutlet`.
|
|
125
107
|
|
|
126
108
|
```gjs
|
|
127
109
|
// assets/javascripts/discourse/components/my-banner.gjs
|
|
@@ -131,21 +113,18 @@ import { service } from "@ember/service";
|
|
|
131
113
|
export default class MyBanner extends Component {
|
|
132
114
|
@service currentUser;
|
|
133
115
|
|
|
134
|
-
//
|
|
135
|
-
// outletArgs contains context from where the outlet sits in the DOM.
|
|
116
|
+
// Controls whether component renders. outletArgs = DOM context.
|
|
136
117
|
static shouldRender(outletArgs, helper) {
|
|
137
118
|
return helper.siteSettings.my_plugin_enabled && helper.currentUser;
|
|
138
119
|
}
|
|
139
120
|
|
|
140
121
|
<template>
|
|
141
|
-
<div class="my-banner">
|
|
142
|
-
Welcome, {{this.currentUser.username}}!
|
|
143
|
-
</div>
|
|
122
|
+
<div class="my-banner">Welcome, {{this.currentUser.username}}!</div>
|
|
144
123
|
</template>
|
|
145
124
|
}
|
|
146
125
|
```
|
|
147
126
|
|
|
148
|
-
|
|
127
|
+
Post stream outlets (Glimmer post stream — current):
|
|
149
128
|
|
|
150
129
|
```gjs
|
|
151
130
|
import Component from "@glimmer/component";
|
|
@@ -156,9 +135,8 @@ export default apiInitializer((api) => {
|
|
|
156
135
|
"post-content-cooked-html",
|
|
157
136
|
class extends Component {
|
|
158
137
|
static shouldRender(args) {
|
|
159
|
-
return args.post.wiki;
|
|
138
|
+
return args.post.wiki;
|
|
160
139
|
}
|
|
161
|
-
|
|
162
140
|
<template>
|
|
163
141
|
<div class="wiki-notice">This post is a wiki</div>
|
|
164
142
|
</template>
|
|
@@ -167,18 +145,14 @@ export default apiInitializer((api) => {
|
|
|
167
145
|
});
|
|
168
146
|
```
|
|
169
147
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
Search Discourse core for `<PluginOutlet @name=`:
|
|
148
|
+
Find outlet names:
|
|
173
149
|
```bash
|
|
174
150
|
rg '<PluginOutlet @name=' app/assets/javascripts/discourse/
|
|
175
151
|
```
|
|
176
152
|
|
|
177
|
-
Common outlets: `discovery-list-container-top`, `topic-above-post-stream`,
|
|
178
|
-
`above-main-container`, `header-icons`, `user-profile-primary`,
|
|
179
|
-
`post-content-cooked-html`, `after-topic-list-area`.
|
|
153
|
+
Common outlets: `discovery-list-container-top`, `topic-above-post-stream`, `above-main-container`, `header-icons`, `user-profile-primary`, `post-content-cooked-html`, `after-topic-list-area`.
|
|
180
154
|
|
|
181
|
-
##
|
|
155
|
+
## Component Patterns
|
|
182
156
|
|
|
183
157
|
### Args vs State
|
|
184
158
|
|
|
@@ -187,11 +161,9 @@ import Component from "@glimmer/component";
|
|
|
187
161
|
import { tracked } from "@glimmer/tracking";
|
|
188
162
|
|
|
189
163
|
export default class UserCard extends Component {
|
|
190
|
-
|
|
191
|
-
@tracked showDetails = false;
|
|
164
|
+
@tracked showDetails = false; // component owns this
|
|
192
165
|
|
|
193
|
-
//
|
|
194
|
-
get displayName() {
|
|
166
|
+
get displayName() { // derived from args — never @tracked
|
|
195
167
|
return this.args.user.name || this.args.user.username;
|
|
196
168
|
}
|
|
197
169
|
|
|
@@ -202,29 +174,26 @@ export default class UserCard extends Component {
|
|
|
202
174
|
<template>
|
|
203
175
|
<div class="user-card {{if this.isStaff 'user-card--staff'}}">
|
|
204
176
|
<h3>{{this.displayName}}</h3>
|
|
205
|
-
{{#if this.showDetails}}
|
|
206
|
-
<p>{{@user.bio_raw}}</p>
|
|
207
|
-
{{/if}}
|
|
177
|
+
{{#if this.showDetails}}<p>{{@user.bio_raw}}</p>{{/if}}
|
|
208
178
|
</div>
|
|
209
179
|
</template>
|
|
210
180
|
}
|
|
211
181
|
```
|
|
212
182
|
|
|
213
|
-
### Services
|
|
183
|
+
### Services
|
|
214
184
|
|
|
215
185
|
```gjs
|
|
216
186
|
import Component from "@glimmer/component";
|
|
217
187
|
import { service } from "@ember/service";
|
|
218
188
|
|
|
219
189
|
export default class MyFeature extends Component {
|
|
220
|
-
//
|
|
221
|
-
@service
|
|
222
|
-
@service
|
|
223
|
-
@service
|
|
224
|
-
@service
|
|
225
|
-
@service
|
|
226
|
-
@service
|
|
227
|
-
@service toasts; // toast notifications (Discourse 3.2+)
|
|
190
|
+
@service currentUser; // logged-in user (null if anonymous)
|
|
191
|
+
@service siteSettings; // site configuration
|
|
192
|
+
@service router; // programmatic navigation
|
|
193
|
+
@service store; // Ember Data store
|
|
194
|
+
@service session; // session data
|
|
195
|
+
@service modal; // open modals
|
|
196
|
+
@service toasts; // toast notifications (Discourse 3.2+)
|
|
228
197
|
|
|
229
198
|
get canUseFeature() {
|
|
230
199
|
return this.currentUser?.trust_level >= 2
|
|
@@ -232,14 +201,12 @@ export default class MyFeature extends Component {
|
|
|
232
201
|
}
|
|
233
202
|
|
|
234
203
|
<template>
|
|
235
|
-
{{#if this.canUseFeature}}
|
|
236
|
-
...
|
|
237
|
-
{{/if}}
|
|
204
|
+
{{#if this.canUseFeature}}...{{/if}}
|
|
238
205
|
</template>
|
|
239
206
|
}
|
|
240
207
|
```
|
|
241
208
|
|
|
242
|
-
### Actions
|
|
209
|
+
### Actions
|
|
243
210
|
|
|
244
211
|
```gjs
|
|
245
212
|
import Component from "@glimmer/component";
|
|
@@ -250,43 +217,31 @@ export default class SearchBox extends Component {
|
|
|
250
217
|
@tracked query = "";
|
|
251
218
|
@tracked results = [];
|
|
252
219
|
|
|
253
|
-
@action
|
|
254
|
-
updateQuery(event) {
|
|
255
|
-
this.query = event.target.value;
|
|
256
|
-
}
|
|
220
|
+
@action updateQuery(event) { this.query = event.target.value; }
|
|
257
221
|
|
|
258
222
|
@action
|
|
259
223
|
async search() {
|
|
260
224
|
if (!this.query.trim()) return;
|
|
261
|
-
// side effects belong in @action methods, not in getters or templates
|
|
262
225
|
this.results = await this.args.onSearch(this.query);
|
|
263
226
|
}
|
|
264
227
|
|
|
265
228
|
<template>
|
|
266
|
-
<input
|
|
267
|
-
type="text"
|
|
268
|
-
value={{this.query}}
|
|
269
|
-
{{on "input" this.updateQuery}}
|
|
270
|
-
/>
|
|
229
|
+
<input type="text" value={{this.query}} {{on "input" this.updateQuery}} />
|
|
271
230
|
<button type="button" {{on "click" this.search}}>Search</button>
|
|
272
231
|
</template>
|
|
273
232
|
}
|
|
274
233
|
```
|
|
275
234
|
|
|
276
|
-
## Admin UI
|
|
235
|
+
## Admin UI
|
|
277
236
|
|
|
278
|
-
Admin components live in
|
|
237
|
+
Admin components live in `admin/` asset tree, same Glimmer patterns.
|
|
279
238
|
|
|
280
239
|
```
|
|
281
240
|
assets/javascripts/
|
|
282
|
-
├── discourse/
|
|
283
|
-
│ └── initializers/
|
|
284
|
-
│ └── my-plugin.js # user-facing outlets
|
|
241
|
+
├── discourse/initializers/my-plugin.js
|
|
285
242
|
└── admin/
|
|
286
|
-
├── components/
|
|
287
|
-
|
|
288
|
-
└── routes/
|
|
289
|
-
└── admin-plugins-my-plugin.js
|
|
243
|
+
├── components/my-plugin-admin.gjs
|
|
244
|
+
└── routes/admin-plugins-my-plugin.js
|
|
290
245
|
```
|
|
291
246
|
|
|
292
247
|
```gjs
|
|
@@ -302,7 +257,6 @@ import LoadingSpinner from "discourse/components/loading-spinner";
|
|
|
302
257
|
|
|
303
258
|
export default class MyPluginAdmin extends Component {
|
|
304
259
|
@service currentUser;
|
|
305
|
-
|
|
306
260
|
@tracked stats = null;
|
|
307
261
|
@tracked isLoading = false;
|
|
308
262
|
|
|
@@ -322,40 +276,33 @@ export default class MyPluginAdmin extends Component {
|
|
|
322
276
|
<template>
|
|
323
277
|
<div class="my-plugin-admin">
|
|
324
278
|
<h2>My Plugin Admin</h2>
|
|
325
|
-
|
|
326
279
|
{{#if this.isLoading}}
|
|
327
280
|
<LoadingSpinner />
|
|
328
281
|
{{else if this.stats}}
|
|
329
282
|
<p>Total users: {{this.stats.total_users}}</p>
|
|
330
283
|
<p>Pending: {{this.stats.pending}}</p>
|
|
331
284
|
{{/if}}
|
|
332
|
-
|
|
333
|
-
<DButton
|
|
334
|
-
@action={{this.loadStats}}
|
|
335
|
-
@label="my_plugin.admin.load_stats"
|
|
336
|
-
@disabled={{this.isLoading}}
|
|
337
|
-
/>
|
|
285
|
+
<DButton @action={{this.loadStats}} @label="my_plugin.admin.load_stats" @disabled={{this.isLoading}} />
|
|
338
286
|
</div>
|
|
339
287
|
</template>
|
|
340
288
|
}
|
|
341
289
|
```
|
|
342
290
|
|
|
343
|
-
## AJAX
|
|
291
|
+
## AJAX
|
|
292
|
+
|
|
293
|
+
Always use `discourse/lib/ajax` — handles CSRF tokens, error formatting, session state automatically. Never use raw `fetch`.
|
|
344
294
|
|
|
345
295
|
```javascript
|
|
346
296
|
import { ajax } from "discourse/lib/ajax";
|
|
347
297
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
348
298
|
|
|
349
|
-
// GET
|
|
350
299
|
const data = await ajax("/my-plugin/endpoint.json");
|
|
351
300
|
|
|
352
|
-
// POST — Discourse's ajax helper automatically includes the CSRF token
|
|
353
301
|
const result = await ajax("/my-plugin/action", {
|
|
354
302
|
type: "POST",
|
|
355
303
|
data: { user_id: userId, value: someValue }
|
|
356
304
|
});
|
|
357
305
|
|
|
358
|
-
// Always handle errors with popupAjaxError for consistent UX
|
|
359
306
|
try {
|
|
360
307
|
await ajax("/my-plugin/action", { type: "DELETE" });
|
|
361
308
|
} catch (e) {
|
|
@@ -363,134 +310,77 @@ try {
|
|
|
363
310
|
}
|
|
364
311
|
```
|
|
365
312
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
## Security in Ember
|
|
369
|
-
|
|
370
|
-
### Client checks are UX only — backend enforces everything
|
|
371
|
-
|
|
372
|
-
```javascript
|
|
373
|
-
// Hiding/showing UI based on role is fine:
|
|
374
|
-
get showAdminTools() {
|
|
375
|
-
return this.currentUser?.admin;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// But the BACKEND must enforce the same check on every request.
|
|
379
|
-
// Client-side auth is trivially bypassed in browser devtools.
|
|
380
|
-
// There is no such thing as client-side security.
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
### No raw HTML injection with user content
|
|
384
|
-
|
|
385
|
-
```handlebars
|
|
386
|
-
{{!-- Safe — Glimmer auto-escapes output --}}
|
|
387
|
-
{{user.bio}}
|
|
388
|
-
|
|
389
|
-
{{!-- Unsafe — raw output, skip escaping only for server-sanitized HTML --}}
|
|
390
|
-
{{! avoid triple-mustache with user-supplied content }}
|
|
391
|
-
|
|
392
|
-
{{!-- For server-sanitized post content, Discourse provides: --}}
|
|
393
|
-
<div>{{html-safe post.cooked}}</div>
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
### Content Security Policy
|
|
313
|
+
## Security
|
|
397
314
|
|
|
398
|
-
|
|
399
|
-
-
|
|
400
|
-
-
|
|
401
|
-
- Modifying `innerHTML` directly — use Glimmer templates instead
|
|
402
|
-
- Importing from external CDNs — bundle or use Discourse's asset pipeline
|
|
315
|
+
- Client checks are UX only — backend enforces everything. Client-side auth is trivially bypassed.
|
|
316
|
+
- No raw HTML with user content — Glimmer auto-escapes `{{user.bio}}`. Use `{{html-safe post.cooked}}` only for server-sanitized HTML.
|
|
317
|
+
- CSP: no inline `<script>`, no `eval`, no `innerHTML`, no external CDN imports.
|
|
403
318
|
|
|
404
|
-
## Deprecations
|
|
319
|
+
## Deprecations
|
|
405
320
|
|
|
406
321
|
```javascript
|
|
407
|
-
// DEPRECATED —
|
|
408
|
-
api.registerConnectorClass("outlet-name", "connector-name", {
|
|
409
|
-
setupComponent(args, component) { ... }
|
|
410
|
-
});
|
|
322
|
+
// DEPRECATED — connector class (shows deprecation warning)
|
|
323
|
+
api.registerConnectorClass("outlet-name", "connector-name", { ... });
|
|
411
324
|
|
|
412
|
-
// DEPRECATED — widget system (
|
|
325
|
+
// DEPRECATED — widget system (being removed 2025/2026)
|
|
413
326
|
api.decorateWidget("post:after", (helper) => { ... });
|
|
414
327
|
api.createWidget("my-widget", { ... });
|
|
415
328
|
|
|
416
|
-
// DEPRECATED — raw handlebars
|
|
417
|
-
// Breaks with the Glimmer topic list (enabled by default 2025)
|
|
329
|
+
// DEPRECATED — raw handlebars (.hbr, .raw.hbs) — breaks Glimmer topic list (default 2025)
|
|
418
330
|
|
|
419
|
-
// DEPRECATED — classic component
|
|
420
|
-
import Component from "@ember/component"; // use @glimmer/component
|
|
331
|
+
// DEPRECATED — classic component
|
|
332
|
+
import Component from "@ember/component"; // use @glimmer/component
|
|
421
333
|
|
|
422
|
-
// DEPRECATED — Ember object mutation
|
|
423
|
-
this.set("myProp", value);
|
|
424
|
-
Ember.set(obj, "key", val);
|
|
334
|
+
// DEPRECATED — Ember object mutation
|
|
335
|
+
this.set("myProp", value); // use @tracked + direct assignment
|
|
336
|
+
Ember.set(obj, "key", val);
|
|
425
337
|
```
|
|
426
338
|
|
|
427
|
-
## File Naming
|
|
339
|
+
## File Naming
|
|
428
340
|
|
|
429
341
|
```
|
|
430
342
|
assets/javascripts/discourse/
|
|
431
|
-
├── initializers/
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
│ ├── my-feature.gjs # kebab-case filenames
|
|
435
|
-
│ └── my-other-thing.gjs
|
|
436
|
-
└── lib/
|
|
437
|
-
└── my-utils.js # pure helpers, no Ember dependency
|
|
343
|
+
├── initializers/my-plugin.js # apiInitializer — one per feature area
|
|
344
|
+
├── components/my-feature.gjs # kebab-case
|
|
345
|
+
└── lib/my-utils.js # pure helpers, no Ember dependency
|
|
438
346
|
|
|
439
347
|
assets/javascripts/admin/
|
|
440
|
-
├── components/
|
|
441
|
-
|
|
442
|
-
└── routes/
|
|
443
|
-
└── admin-plugins-my-plugin.js
|
|
348
|
+
├── components/admin-my-plugin.gjs # prefix admin components with "admin-"
|
|
349
|
+
└── routes/admin-plugins-my-plugin.js
|
|
444
350
|
```
|
|
445
351
|
|
|
446
|
-
##
|
|
352
|
+
## Checklist
|
|
447
353
|
|
|
448
|
-
- [ ]
|
|
449
|
-
- [ ]
|
|
450
|
-
- [ ]
|
|
451
|
-
- [ ] `@tracked` only for
|
|
452
|
-
- [ ] Derived values are getters
|
|
453
|
-
- [ ] Services
|
|
454
|
-
- [ ] AJAX via `discourse/lib/ajax` — not
|
|
455
|
-
- [ ] Errors
|
|
456
|
-
- [ ] No raw HTML
|
|
457
|
-
- [ ] Backend enforces all authorization
|
|
354
|
+
- [ ] `.gjs` (not `.hbs`/`.js` pairs) for new components
|
|
355
|
+
- [ ] `apiInitializer` entry point, not bare `withPluginApi`
|
|
356
|
+
- [ ] `api.renderInOutlet` — not `decorateWidget`
|
|
357
|
+
- [ ] `@tracked` only for component-owned state, not derived values
|
|
358
|
+
- [ ] Derived values are getters
|
|
359
|
+
- [ ] Services via `@service` — not imported singletons
|
|
360
|
+
- [ ] AJAX via `discourse/lib/ajax` — not `fetch`
|
|
361
|
+
- [ ] Errors via `popupAjaxError`
|
|
362
|
+
- [ ] No raw HTML with user-supplied content
|
|
363
|
+
- [ ] Backend enforces all authorization
|
|
458
364
|
|
|
459
|
-
## Quick Reference:
|
|
365
|
+
## Quick Reference: Imports
|
|
460
366
|
|
|
461
367
|
```javascript
|
|
462
|
-
// Glimmer/Ember core
|
|
463
368
|
import Component from "@glimmer/component";
|
|
464
369
|
import { tracked } from "@glimmer/tracking";
|
|
465
370
|
import { action } from "@ember/object";
|
|
466
371
|
import { service } from "@ember/service";
|
|
467
|
-
|
|
468
|
-
// Discourse plugin API
|
|
469
372
|
import { apiInitializer } from "discourse/lib/api";
|
|
470
373
|
import { ajax } from "discourse/lib/ajax";
|
|
471
374
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
472
|
-
|
|
473
|
-
// Discourse components
|
|
474
375
|
import DButton from "discourse/components/d-button";
|
|
475
376
|
import LoadingSpinner from "discourse/components/loading-spinner";
|
|
476
377
|
import DModal from "discourse/components/d-modal";
|
|
477
|
-
|
|
478
|
-
// i18n
|
|
479
|
-
import { i18n } from "discourse-i18n"; // current (replaces the old I18n.t())
|
|
378
|
+
import { i18n } from "discourse-i18n"; // replaces I18n.t()
|
|
480
379
|
```
|
|
481
380
|
|
|
482
|
-
##
|
|
483
|
-
|
|
484
|
-
### Admin UI Patterns
|
|
485
|
-
**File**: [`references/admin-ui.md`](references/admin-ui.md)
|
|
486
|
-
**Load when**: Building admin routes, tables, forms, settings UI
|
|
487
|
-
**Contains**: Full admin route + component example, admin table patterns, settings form
|
|
488
|
-
|
|
489
|
-
### Plugin Outlet Reference
|
|
490
|
-
**File**: [`references/outlets.md`](references/outlets.md)
|
|
491
|
-
**Load when**: Need to find the right outlet or understand outletArgs context
|
|
492
|
-
**Contains**: Common outlet names, outletArgs by context, shouldRender patterns
|
|
493
|
-
|
|
494
|
-
---
|
|
381
|
+
## Reference Files
|
|
495
382
|
|
|
496
|
-
|
|
383
|
+
| File | Load when |
|
|
384
|
+
|------|-----------|
|
|
385
|
+
| [`references/admin-ui.md`](references/admin-ui.md) | Building admin routes, tables, forms, settings UI |
|
|
386
|
+
| [`references/outlets.md`](references/outlets.md) | Finding outlet names, understanding outletArgs context |
|
|
@@ -11,39 +11,30 @@ description: >-
|
|
|
11
11
|
|
|
12
12
|
# EspoCRM - Skill Family Router
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
**Target version**: v9.x (9.0+)
|
|
17
|
-
**Architecture**: Entity-based REST API, PHP backend, Backbone.js frontend
|
|
14
|
+
**Target**: v9.x (9.0+) | **Architecture**: Entity-based REST API, PHP backend, Backbone.js frontend
|
|
18
15
|
|
|
19
16
|
## Decision Tree
|
|
20
17
|
|
|
21
18
|
```
|
|
22
19
|
What are you doing with EspoCRM?
|
|
23
20
|
├── REST API calls (external integration)?
|
|
24
|
-
│ → espocrm-api
|
|
25
|
-
│ → Auth, CRUD, filtering, webhooks, mass ops
|
|
21
|
+
│ → espocrm-api + php-fp or js-fp-api
|
|
26
22
|
│
|
|
27
23
|
├── PHP extension development (hooks, services, custom entities)?
|
|
28
24
|
│ → espocrm-extensions (Phase 2) + php-fp
|
|
29
|
-
│ → ORM, hooks, services, DI, custom controllers, modules
|
|
30
25
|
│
|
|
31
26
|
├── Frontend/UI customization (views, fields, layouts)?
|
|
32
27
|
│ → espocrm-ui (Phase 3)
|
|
33
|
-
│ → Backbone views, Espo.Ajax, Handlebars templates
|
|
34
28
|
│
|
|
35
29
|
└── Not sure / mixed?
|
|
36
|
-
→ Start with espocrm-api
|
|
37
|
-
→ Route to extension/UI skill once scope is clear
|
|
30
|
+
→ Start with espocrm-api, route to extension/UI once scope is clear
|
|
38
31
|
```
|
|
39
32
|
|
|
40
33
|
## Shared Context
|
|
41
34
|
|
|
42
|
-
|
|
43
|
-
EspoCRM organizes data as **Entity Types** (Account, Contact, Lead, Opportunity, custom types). Every entity type gets automatic REST endpoints. Custom entities created via Entity Manager are immediately API-accessible.
|
|
35
|
+
Every Entity Type (Account, Contact, Lead, Opportunity, custom) gets automatic REST endpoints. Custom entities via Entity Manager are immediately API-accessible.
|
|
44
36
|
|
|
45
|
-
### Salesforce
|
|
46
|
-
For developers familiar with Salesforce, this mapping accelerates onboarding:
|
|
37
|
+
### Salesforce Mapping
|
|
47
38
|
|
|
48
39
|
| Salesforce | EspoCRM |
|
|
49
40
|
|---|---|
|
|
@@ -56,19 +47,19 @@ For developers familiar with Salesforce, this mapping accelerates onboarding:
|
|
|
56
47
|
| LWC / Visualforce | Custom Views (JS, extending base views) |
|
|
57
48
|
| Platform Events / CDC | Webhooks ({Entity}.create, .update, .delete) |
|
|
58
49
|
| Bulk API 2.0 | No equivalent (loop individual calls or use Import) |
|
|
59
|
-
| Governor Limits | None (self-hosted
|
|
50
|
+
| Governor Limits | None (self-hosted) |
|
|
60
51
|
| AppExchange | EspoCRM Extensions marketplace |
|
|
61
52
|
|
|
62
53
|
### Key Differences from Salesforce
|
|
63
|
-
- **No SOQL** — queries use structured JSON WHERE filters (verbose but explicit)
|
|
64
|
-
- **No Bulk API** — mass operations exist (massUpdate, massDelete) but no batch create
|
|
65
|
-
- **No Composite API** — one request per operation
|
|
66
|
-
- **No governor limits** — self-hosted, manage at server/proxy level
|
|
67
|
-
- **Simpler auth** — API Key in one header vs. multi-step OAuth
|
|
68
|
-
- **Metadata is JSON files** — no deployment steps, changes take effect on cache clear
|
|
69
54
|
|
|
70
|
-
|
|
71
|
-
|
|
55
|
+
- No SOQL — structured JSON WHERE filters
|
|
56
|
+
- No Bulk API — mass ops (massUpdate, massDelete) but no batch create
|
|
57
|
+
- No Composite API — one request per operation
|
|
58
|
+
- No governor limits
|
|
59
|
+
- Simpler auth — API Key in one header
|
|
60
|
+
- Metadata is JSON files — changes take effect on cache clear
|
|
61
|
+
|
|
62
|
+
**Docs**: Use Context7 `resolve-library-id("espocrm")` → `/espocrm/documentation`
|
|
72
63
|
|
|
73
64
|
## Child Skill Status
|
|
74
65
|
|