infaira-canvas 0.2.0 → 0.3.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.
@@ -288,9 +288,15 @@ import LocalizationMessages from '../localization.json';
288
288
  // ─── Types ────────────────────────────────────────────────────────────────────
289
289
 
290
290
  export interface IActionOptions {
291
+ /** Parse response body as JSON before resolving. */
291
292
  json?: boolean;
293
+ /** Stable identifier for cancellation. Required when cancelPrevious is set. */
292
294
  key?: string;
295
+ /** When true and "key" is also set, any prior in-flight call sharing the
296
+ * same key is cancelled (its Promise resolves to null). Use for live-filter
297
+ * widgets where only the latest call result should land in state. */
293
298
  cancelPrevious?: boolean;
299
+ /** Skip the 100ms batch window and fire immediately. */
294
300
  executeImmediately?: boolean;
295
301
  }
296
302
 
@@ -316,13 +322,23 @@ export interface IContextProvider {
316
322
  * - executeAction('Buildings', 'List', { siteId }) → ICan native
317
323
  * - executeAction('mythos:Inventory', 'UpdateStock', p) → Mythos engine
318
324
  * The 'mythos:' prefix is the only difference at the call site; the
319
- * ICan backend routes the call appropriately. */
325
+ * ICan backend routes the call appropriately.
326
+ * options.cancelPrevious + options.key: when set, an in-flight call with
327
+ * the same key is cancelled (resolves null) and the new call wins. */
320
328
  executeAction(model: string, action: string, parameters: unknown, options?: IActionOptions): Promise<unknown>;
321
329
  executeService(app: string, service: string, parameters: unknown, options?: IActionOptions): Promise<unknown>;
322
330
  /** List Mythos actions callable via executeAction('mythos:...', ...).
323
331
  * Optional — present only when MYTHOS_BASE_URL is configured on the ICan
324
332
  * backend. Treat absence as "Mythos integration disabled". */
325
333
  listMythosActions?(): Promise<IMythosAction[]>;
334
+ /** Fire a Mythos external event with an optional payload. */
335
+ fireMythosEvent?(eventType: string, payload?: Record<string, unknown>): Promise<void>;
336
+ /** Build an IDataFunction over a Mythos collection for DataList/DataTable. */
337
+ fromMythosCollection?(model: string, collection: string): (
338
+ max: number,
339
+ lastPageToken: string,
340
+ args?: { search?: string } & Record<string, unknown>,
341
+ ) => Promise<{ items: Array<Record<string, unknown>>; pageToken: string }>;
326
342
  fireEvent(eventId: string): Promise<void>;
327
343
  hasAppRole(app: string, role: string): boolean;
328
344
  themeName?: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "infaira-canvas",
3
- "version": "0.2.0",
4
- "description": "InfAIra Canvas CLI — scaffold widgets that talk to ICan and Mythos.",
3
+ "version": "0.3.0",
4
+ "description": "InfAIra Canvas CLI — scaffold widgets that talk to ICan and Mythos (actions, events, public portals, paginated collections).",
5
5
  "keywords": [
6
6
  "infaira",
7
7
  "ican",
@@ -38,8 +38,10 @@ In the portal, `icanContext` is always defined — the widget is never mounted u
38
38
  | `language` | `string` | Active language code, e.g. `'en'` |
39
39
  | `themeName` | `string \| undefined` | e.g. `'Dark'`, `'Glass Light'` |
40
40
  | `themeType` | `'Dark' \| 'Light' \| 'Glass-Dark' \| 'Glass-Light' \| undefined` | Structured theme type for chart colours |
41
- | `executeAction` | `(model, action, params?, options?) => Promise<unknown>` | Call an action. ICan native by default; prefix `model` with `mythos:` to route to the Mythos engine instead. |
41
+ | `executeAction` | `(model, action, params?, options?) => Promise<unknown>` | Call an action. ICan native by default; prefix `model` with `mythos:` to route to the Mythos engine instead. `options.cancelPrevious` + `options.key` to deduplicate live-filter calls. |
42
42
  | `listMythosActions` | `() => Promise<IMythosAction[]> \| undefined` | List Mythos actions callable from this widget. Present only when the ICan backend has `MYTHOS_BASE_URL` configured. |
43
+ | `fireMythosEvent` | `(event_type, payload?) => Promise<void> \| undefined` | Fire a named external event into the Mythos bus. Triggers any matching Mythos event handlers. |
44
+ | `fromMythosCollection` | `(model, collection) => IDataFunction \| undefined` | Build a paginated data source over a Mythos collection — drops into DataList / DataTable. |
43
45
  | `fireEvent` | `(eventId) => Promise<void>` | Trigger a portal event |
44
46
  | `hasAppRole` | `(app, role) => boolean` | Check if the user has a role |
45
47
  | `$L` | `(code, params?) => string` | Translate a localisation key |
@@ -125,6 +127,61 @@ const handleRun = async () => {
125
127
 
126
128
  A working reference widget lives at `Widgets/ICan widgets/mythosdemo/` — pick an action, fill params, run, render the response.
127
129
 
130
+ ### Cancelling stale calls (`options.cancelPrevious`)
131
+
132
+ Widgets that fire on every keystroke — search bars, live filters — end up racing requests against themselves. Use `cancelPrevious` + `key` to ensure only the latest call's result lands:
133
+
134
+ ```tsx
135
+ const onSearch = (text: string) => {
136
+ icanContext.executeAction(
137
+ 'mythos:Inventory',
138
+ 'Search',
139
+ { q: text },
140
+ { cancelPrevious: true, key: 'inventory-search' },
141
+ ).then((res) => {
142
+ if (res !== null) setResults(res); // null means this call was cancelled
143
+ });
144
+ };
145
+ ```
146
+
147
+ Calls with the same `key` are tracked; when a newer one is issued, older ones resolve to `null` instead of delivering stale data into your state.
148
+
149
+ ### Firing Mythos events
150
+
151
+ `icanContext.fireMythosEvent('order_placed', { id: 4821 })` publishes the event into Mythos's bus. Any Mythos workflow with a matching External Event trigger fires. Useful for widget → workflow dispatch where you don't need a return value.
152
+
153
+ ```tsx
154
+ await icanContext.fireMythosEvent?.('user_logged_in', { userId });
155
+ ```
156
+
157
+ Optional — `fireMythosEvent` is `undefined` when Mythos integration is disabled. Guard with `?.()`.
158
+
159
+ ### Reading from a Mythos collection
160
+
161
+ `icanContext.fromMythosCollection(model, collection)` returns a function with the same shape ICan's DataList / DataTable already accept:
162
+
163
+ ```tsx
164
+ const fetchPage = icanContext.fromMythosCollection!('Inventory', 'items');
165
+
166
+ <DataList
167
+ dataFunction={fetchPage}
168
+ pageSize={50}
169
+ renderItem={(item) => <div>{item.sku} — {item.qty}</div>}
170
+ />
171
+ ```
172
+
173
+ The function takes `(max, lastPageToken, args?)` and returns `{ items, pageToken }`. Empty `pageToken` means "no more pages". Supports `args.search` for text filtering against the collection's searchable fields.
174
+
175
+ ### Public portals (anonymous Mythos access)
176
+
177
+ When a widget runs on a `is_public: true` portal, the host calls `/api/public/execute-batch` instead of the authenticated endpoint. For `mythos:` calls in that batch:
178
+
179
+ - The Mythos action **must** have `no_auth: true` set in its API Route config
180
+ - The call is forwarded to Mythos's public route `/api/<scope>/models/{name}/actions/{name}` with no JWT
181
+ - Mythos enforces `no_auth` at its middleware layer — actions without that flag return `401` to the widget
182
+
183
+ No code change in the widget itself — the same `executeAction('mythos:X', 'Y', params)` call works in both authenticated and public portals.
184
+
128
185
  ---
129
186
 
130
187
  ## 3. Widget settings panel — `IWidgetPropConfig`
@@ -714,11 +714,16 @@ declare module "ican/context" {
714
714
  *
715
715
  * Both call shapes return a Promise that resolves to the action's output
716
716
  * envelope. For Mythos, the envelope is `{ run_id, status, output? }`.
717
+ *
718
+ * `options.cancelPrevious` + `options.key`: when an in-flight call shares
719
+ * the same key, its Promise resolves to `null` and the newer call wins.
720
+ * Designed for live-filter widgets where only the latest result matters.
717
721
  */
718
722
  executeAction(
719
723
  model: string,
720
724
  action: string,
721
- params?: Record<string, unknown>
725
+ params?: Record<string, unknown>,
726
+ options?: { cancelPrevious?: boolean; key?: string }
722
727
  ): Promise<unknown>;
723
728
  /**
724
729
  * List every Mythos action published and callable from this widget.
@@ -727,6 +732,31 @@ declare module "ican/context" {
727
732
  * once at mount and cache the result themselves.
728
733
  */
729
734
  listMythosActions?: () => Promise<IMythosAction[]>;
735
+ /**
736
+ * Fire an external event into the Mythos engine. The event_type matches
737
+ * what Mythos event-handlers subscribe to (configured in the model's
738
+ * Events tab). Mirrors UXP's fireEvent, with an optional payload.
739
+ * Optional — present only when MYTHOS_BASE_URL is configured.
740
+ */
741
+ fireMythosEvent?: (
742
+ eventType: string,
743
+ payload?: Record<string, unknown>
744
+ ) => Promise<void>;
745
+ /**
746
+ * Build an IDataFunction over a Mythos collection — drop-in data source
747
+ * for DataList / DataTable. Mirrors UXP's fromLucyDataCollection.
748
+ * const fn = icanContext.fromMythosCollection('Inventory', 'items')
749
+ * const { items, pageToken } = await fn(50, '', { search: 'foo' })
750
+ * Optional — present only when MYTHOS_BASE_URL is configured.
751
+ */
752
+ fromMythosCollection?: (
753
+ model: string,
754
+ collection: string
755
+ ) => (
756
+ max: number,
757
+ lastPageToken: string,
758
+ args?: { search?: string } & Record<string, unknown>
759
+ ) => Promise<{ items: Array<Record<string, unknown>>; pageToken: string }>;
730
760
  /** Resolve a relative ICan backend path against the portal base URL. */
731
761
  resolveUrl(path: string): string;
732
762
  /** Logging helper namespaced to this widget instance. */
@@ -1443,6 +1443,22 @@
1443
1443
  { model_id: 'dev-model-2', model_name: 'Buildings', action_id: 'dev-action-2', action_name: 'List', description: 'Dev stub' },
1444
1444
  ]);
1445
1445
  },
1446
+ fireMythosEvent: function (eventType, payload) {
1447
+ console.log('[ICan Dev] fireMythosEvent:', eventType, payload);
1448
+ return Promise.resolve();
1449
+ },
1450
+ fromMythosCollection: function (model, collection) {
1451
+ return function (max, lastPageToken, args) {
1452
+ console.log('[ICan Dev] fromMythosCollection:', model, collection, { max: max, lastPageToken: lastPageToken, args: args });
1453
+ // Return one page of synthetic docs so DataList renders something.
1454
+ return Promise.resolve({
1455
+ items: Array.from({ length: Math.min(max, 5) }, function (_, i) {
1456
+ return { id: 'dev-' + i, name: model + ' row ' + i, value: i * 10 };
1457
+ }),
1458
+ pageToken: '',
1459
+ });
1460
+ };
1461
+ },
1446
1462
  fireEvent: function (id) {
1447
1463
  console.log('[ICan Dev] fireEvent:', id);
1448
1464
  return Promise.resolve();
package/templates/ui.html CHANGED
@@ -1142,6 +1142,21 @@
1142
1142
  { model_id: 'dev-model-2', model_name: 'Buildings', action_id: 'dev-action-2', action_name: 'List', description: 'Dev stub' },
1143
1143
  ]);
1144
1144
  },
1145
+ fireMythosEvent: function (eventType, payload) {
1146
+ console.log('[ICan Dev] fireMythosEvent:', eventType, payload);
1147
+ return Promise.resolve();
1148
+ },
1149
+ fromMythosCollection: function (model, collection) {
1150
+ return function (max, lastPageToken, args) {
1151
+ console.log('[ICan Dev] fromMythosCollection:', model, collection, { max: max, lastPageToken: lastPageToken, args: args });
1152
+ return Promise.resolve({
1153
+ items: Array.from({ length: Math.min(max, 5) }, function (_, i) {
1154
+ return { id: 'dev-' + i, name: model + ' row ' + i, value: i * 10 };
1155
+ }),
1156
+ pageToken: '',
1157
+ });
1158
+ };
1159
+ },
1145
1160
  fireEvent: function (id) {
1146
1161
  console.log('[ICan Dev] fireEvent:', id);
1147
1162
  return Promise.resolve();