@platforma-sdk/ui-vue 1.54.13 → 1.55.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/src/defineApp.ts CHANGED
@@ -9,10 +9,9 @@ import {
9
9
  getPlatformaApiVersion,
10
10
  unwrapResult,
11
11
  type BlockOutputsBase,
12
- type Platforma,
13
12
  type BlockModelInfo,
14
13
  } from "@platforma-sdk/model";
15
- import type { Component, Reactive } from "vue";
14
+ import type { App as VueApp, Component, Reactive } from "vue";
16
15
  import { inject, markRaw, reactive } from "vue";
17
16
  import { createAppV1, type BaseAppV1 } from "./internal/createAppV1";
18
17
  import { createAppV2, type BaseAppV2 } from "./internal/createAppV2";
@@ -21,6 +20,7 @@ import type { AppSettings, ExtendSettings, Routes } from "./types";
21
20
  import { activateAgGrid } from "./aggrid";
22
21
 
23
22
  const pluginKey = Symbol("sdk-vue");
23
+ export const pluginDataKey = Symbol("plugin-data-access");
24
24
 
25
25
  export function useSdkPlugin(): SdkPlugin {
26
26
  return inject(pluginKey)!;
@@ -57,39 +57,28 @@ export function defineApp<
57
57
  export function defineApp<
58
58
  Args = unknown,
59
59
  Outputs extends BlockOutputsBase = BlockOutputsBase,
60
- Data = unknown,
60
+ UiState = unknown,
61
61
  Href extends `/${string}` = `/${string}`,
62
62
  Extend extends ExtendSettings<Href> = ExtendSettings<Href>,
63
63
  >(
64
- platforma: PlatformaV3<Args, Outputs, Data, Href> & {
65
- blockModelInfo: BlockModelInfo;
66
- },
67
- extendApp: (app: BaseAppV3<Args, Outputs, Data, Href>) => Extend,
68
- settings?: AppSettings,
69
- ): SdkPluginV3<Args, Outputs, Data, Href, Extend>;
64
+ platforma: PlatformaExtended<
65
+ PlatformaV1<Args, Outputs, UiState, Href> | PlatformaV2<Args, Outputs, UiState, Href>
66
+ >,
70
67
 
71
- export function defineApp<
72
- Args = unknown,
73
- Outputs extends BlockOutputsBase = BlockOutputsBase,
74
- UiStateOrData = unknown,
75
- Href extends `/${string}` = `/${string}`,
76
- Extend extends ExtendSettings<Href> = ExtendSettings<Href>,
77
- >(
78
- platforma: PlatformaExtended<Platforma<Args, Outputs, UiStateOrData, Href>>,
79
68
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
69
  extendApp: (app: any) => Extend,
81
70
  settings: AppSettings = {},
82
- ): SdkPlugin<Args, Outputs, UiStateOrData, Href, Extend> {
71
+ ):
72
+ | SdkPluginV1<Args, Outputs, UiState, Href, Extend>
73
+ | SdkPluginV2<Args, Outputs, UiState, Href, Extend> {
83
74
  let app:
84
- | undefined
85
- | AppV1<Args, Outputs, UiStateOrData, Href, Extend>
86
- | AppV2<Args, Outputs, UiStateOrData, Href, Extend>
87
- | AppV3<Args, Outputs, UiStateOrData, Href, Extend> = undefined;
75
+ | AppV1<Args, Outputs, UiState, Href, Extend>
76
+ | AppV2<Args, Outputs, UiState, Href, Extend>
77
+ | undefined = undefined;
88
78
 
89
79
  activateAgGrid();
90
80
 
91
81
  const runtimeApiVersion = platforma.apiVersion ?? 1; // undefined means 1 (backward compatibility)
92
-
93
82
  const blockRequestedApiVersion = getPlatformaApiVersion();
94
83
 
95
84
  const loadApp = async () => {
@@ -101,7 +90,7 @@ export function defineApp<
101
90
  if (platforma.apiVersion === undefined || platforma.apiVersion === 1) {
102
91
  await platforma.loadBlockState().then((state) => {
103
92
  plugin.loaded = true;
104
- const baseApp = createAppV1<Args, Outputs, UiStateOrData, Href>(state, platforma, settings);
93
+ const baseApp = createAppV1<Args, Outputs, UiState, Href>(state, platforma, settings);
105
94
 
106
95
  const localState = extendApp(baseApp);
107
96
 
@@ -117,13 +106,13 @@ export function defineApp<
117
106
  getRoute(href: Href): Component | undefined {
118
107
  return routes[href];
119
108
  },
120
- } as unknown as AppV1<Args, Outputs, UiStateOrData, Href, Extend>);
109
+ } as unknown as AppV1<Args, Outputs, UiState, Href, Extend>);
121
110
  });
122
111
  } else if (platforma.apiVersion === 2) {
123
112
  await platforma.loadBlockState().then((stateOrError) => {
124
113
  const state = unwrapResult(stateOrError);
125
114
  plugin.loaded = true;
126
- const baseApp = createAppV2<Args, Outputs, UiStateOrData, Href>(state, platforma, settings);
115
+ const baseApp = createAppV2<Args, Outputs, UiState, Href>(state, platforma, settings);
127
116
 
128
117
  const localState = extendApp(baseApp);
129
118
 
@@ -139,50 +128,106 @@ export function defineApp<
139
128
  getRoute(href: Href): Component | undefined {
140
129
  return routes[href];
141
130
  },
142
- } as unknown as AppV2<Args, Outputs, UiStateOrData, Href, Extend>);
131
+ } as unknown as AppV2<Args, Outputs, UiState, Href, Extend>);
143
132
  });
144
- } else if (platforma.apiVersion === 3) {
145
- await platforma.loadBlockState().then((stateOrError) => {
146
- const state = unwrapResult(stateOrError);
147
- plugin.loaded = true;
148
- const baseApp = createAppV3<Args, Outputs, UiStateOrData, Href>(state, platforma, settings);
133
+ }
134
+ };
149
135
 
150
- const localState = extendApp(baseApp);
136
+ const plugin = reactive({
137
+ apiVersion: platforma.apiVersion ?? 1,
138
+ loaded: false,
139
+ error: undefined as unknown,
140
+ useApp<PageHref extends Href = Href>() {
141
+ return notEmpty(app, "App is not loaded") as
142
+ | AppV1<Args, Outputs, UiState, PageHref, Extend>
143
+ | AppV2<Args, Outputs, UiState, PageHref, Extend>;
144
+ },
145
+ install(app: VueApp) {
146
+ app.provide(pluginKey, this);
147
+ loadApp().catch((err) => {
148
+ console.error("load initial state error", err);
149
+ plugin.error = err;
150
+ });
151
+ },
152
+ });
151
153
 
152
- const routes = Object.fromEntries(
153
- Object.entries(localState.routes as Routes<Href>).map(([href, component]) => {
154
- const c = typeof component === "function" ? component() : component;
155
- return [href, markRaw(c as Component)];
156
- }),
157
- );
154
+ return plugin as
155
+ | SdkPluginV1<Args, Outputs, UiState, Href, Extend>
156
+ | SdkPluginV2<Args, Outputs, UiState, Href, Extend>;
157
+ }
158
158
 
159
- app = Object.assign(baseApp, {
160
- ...localState,
161
- getRoute(href: Href): Component | undefined {
162
- return routes[href];
163
- },
164
- } as unknown as AppV3<Args, Outputs, UiStateOrData, Href, Extend>);
165
- });
159
+ export function defineAppV3<
160
+ Data = unknown,
161
+ Args = unknown,
162
+ Outputs extends BlockOutputsBase = BlockOutputsBase,
163
+ Href extends `/${string}` = `/${string}`,
164
+ Extend extends ExtendSettings<Href> = ExtendSettings<Href>,
165
+ >(
166
+ platforma: PlatformaV3<Data, Args, Outputs, Href> & {
167
+ blockModelInfo: BlockModelInfo;
168
+ },
169
+ extendApp: (app: BaseAppV3<Data, Args, Outputs, Href>) => Extend,
170
+ settings: AppSettings = {},
171
+ ): SdkPluginV3<Data, Args, Outputs, Href, Extend> {
172
+ let app: AppV3<Data, Args, Outputs, Href, Extend> | undefined = undefined;
173
+
174
+ // Captured during install() so V3 can provide plugin data access after async load
175
+ let vueAppInstance: VueApp | undefined;
176
+
177
+ activateAgGrid();
178
+
179
+ const runtimeApiVersion = 3;
180
+ const blockRequestedApiVersion = getPlatformaApiVersion();
181
+
182
+ const loadApp = async () => {
183
+ if (blockRequestedApiVersion !== runtimeApiVersion) {
184
+ throw new Error(`Block requested API version ${blockRequestedApiVersion} but runtime API version is ${runtimeApiVersion}.
185
+ Please update the desktop app to use the latest API version.`);
166
186
  }
187
+
188
+ await platforma.loadBlockState().then((stateOrError) => {
189
+ const state = unwrapResult(stateOrError);
190
+ plugin.loaded = true;
191
+ const { app: baseApp, pluginAccess } = createAppV3<Data, Args, Outputs, Href>(
192
+ state,
193
+ platforma,
194
+ settings,
195
+ );
196
+
197
+ if (!vueAppInstance) {
198
+ throw new Error(
199
+ "Plugin data injection failed: Vue app instance not captured during install()",
200
+ );
201
+ }
202
+ vueAppInstance.provide(pluginDataKey, pluginAccess);
203
+
204
+ const localState = extendApp(baseApp);
205
+
206
+ const routes = Object.fromEntries(
207
+ Object.entries(localState.routes as Routes<Href>).map(([href, component]) => {
208
+ const c = typeof component === "function" ? component() : component;
209
+ return [href, markRaw(c as Component)];
210
+ }),
211
+ );
212
+
213
+ app = Object.assign(baseApp, {
214
+ ...localState,
215
+ getRoute(href: Href): Component | undefined {
216
+ return routes[href];
217
+ },
218
+ } as unknown as AppV3<Data, Args, Outputs, Href, Extend>);
219
+ });
167
220
  };
168
221
 
169
222
  const plugin = reactive({
170
- apiVersion: platforma.apiVersion ?? 1,
223
+ apiVersion: 3,
171
224
  loaded: false,
172
225
  error: undefined as unknown,
173
- // Href to get typed query parameters for a specific route
174
226
  useApp<PageHref extends Href = Href>() {
175
- return notEmpty(app, "App is not loaded") as App<
176
- Args,
177
- Outputs,
178
- UiStateOrData,
179
- PageHref,
180
- Extend
181
- >;
227
+ return notEmpty(app, "App is not loaded") as AppV3<Data, Args, Outputs, PageHref, Extend>;
182
228
  },
183
- // @todo type portability issue with Vue
184
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
185
- install(app: any) {
229
+ install(app: VueApp) {
230
+ vueAppInstance = app;
186
231
  app.provide(pluginKey, this);
187
232
  loadApp().catch((err) => {
188
233
  console.error("load initial state error", err);
@@ -191,7 +236,7 @@ export function defineApp<
191
236
  },
192
237
  });
193
238
 
194
- return plugin as SdkPlugin<Args, Outputs, UiStateOrData, Href, Extend>;
239
+ return plugin as SdkPluginV3<Data, Args, Outputs, Href, Extend>;
195
240
  }
196
241
 
197
242
  export type AppV1<
@@ -213,24 +258,17 @@ export type AppV2<
213
258
  Reactive<Omit<Local, "routes">> & { getRoute(href: Href): Component | undefined };
214
259
 
215
260
  export type AppV3<
216
- Args = unknown,
217
- Outputs extends BlockOutputsBase = BlockOutputsBase,
218
261
  Data = unknown,
262
+ Args = unknown,
263
+ Outputs extends BlockOutputsBase = NonNullable<unknown>,
219
264
  Href extends `/${string}` = `/${string}`,
220
265
  Local extends ExtendSettings<Href> = ExtendSettings<Href>,
221
- > = BaseAppV3<Args, Outputs, Data, Href> &
266
+ > = BaseAppV3<Data, Args, Outputs, Href> &
222
267
  Reactive<Omit<Local, "routes">> & { getRoute(href: Href): Component | undefined };
223
268
 
224
- export type App<
225
- Args = unknown,
226
- Outputs extends BlockOutputsBase = BlockOutputsBase,
227
- UiState = unknown,
228
- Href extends `/${string}` = `/${string}`,
229
- Local extends ExtendSettings<Href> = ExtendSettings<Href>,
230
- > =
231
- | AppV1<Args, Outputs, UiState, Href, Local>
232
- | AppV2<Args, Outputs, UiState, Href, Local>
233
- | AppV3<Args, Outputs, UiState, Href, Local>;
269
+ // ---------------------------------------------------------------------------
270
+ // SdkPlugin types
271
+ // ---------------------------------------------------------------------------
234
272
 
235
273
  export type SdkPluginV1<
236
274
  Args = unknown,
@@ -243,7 +281,7 @@ export type SdkPluginV1<
243
281
  loaded: boolean;
244
282
  error: unknown;
245
283
  useApp<PageHref extends Href = Href>(): AppV1<Args, Outputs, UiState, PageHref, Local>;
246
- install(app: unknown): void;
284
+ install(app: VueApp): void;
247
285
  };
248
286
 
249
287
  export type SdkPluginV2<
@@ -257,30 +295,21 @@ export type SdkPluginV2<
257
295
  loaded: boolean;
258
296
  error: unknown;
259
297
  useApp<PageHref extends Href = Href>(): AppV2<Args, Outputs, UiState, PageHref, Local>;
260
- install(app: unknown): void;
298
+ install(app: VueApp): void;
261
299
  };
262
300
 
263
301
  export type SdkPluginV3<
302
+ Data = unknown,
264
303
  Args = unknown,
265
304
  Outputs extends BlockOutputsBase = BlockOutputsBase,
266
- Data = unknown,
267
305
  Href extends `/${string}` = `/${string}`,
268
306
  Local extends ExtendSettings<Href> = ExtendSettings<Href>,
269
307
  > = {
270
308
  apiVersion: 3;
271
309
  loaded: boolean;
272
310
  error: unknown;
273
- useApp<PageHref extends Href = Href>(): AppV3<Args, Outputs, Data, PageHref, Local>;
274
- install(app: unknown): void;
311
+ useApp<PageHref extends Href = Href>(): AppV3<Data, Args, Outputs, PageHref, Local>;
312
+ install(app: VueApp): void;
275
313
  };
276
314
 
277
- export type SdkPlugin<
278
- Args = unknown,
279
- Outputs extends BlockOutputsBase = BlockOutputsBase,
280
- UiState = unknown,
281
- Href extends `/${string}` = `/${string}`,
282
- Local extends ExtendSettings<Href> = ExtendSettings<Href>,
283
- > =
284
- | SdkPluginV1<Args, Outputs, UiState, Href, Local>
285
- | SdkPluginV2<Args, Outputs, UiState, Href, Local>
286
- | SdkPluginV3<Args, Outputs, UiState, Href, Local>;
315
+ export type SdkPlugin = SdkPluginV1 | SdkPluginV2 | SdkPluginV3;
@@ -7,19 +7,33 @@ import type {
7
7
  PlatformaV3,
8
8
  ValueWithUTag,
9
9
  AuthorMarker,
10
+ PlatformaExtended,
11
+ } from "@platforma-sdk/model";
12
+ import {
13
+ hasAbortError,
14
+ unwrapResult,
15
+ deriveDataFromStorage,
16
+ getPluginData,
17
+ normalizeBlockStorage,
10
18
  } from "@platforma-sdk/model";
11
- import { hasAbortError, unwrapResult, deriveDataFromStorage } from "@platforma-sdk/model";
12
19
  import type { Ref } from "vue";
13
20
  import { reactive, computed, ref } from "vue";
14
21
  import type { OutputValues, OutputErrors, AppSettings } from "../types";
15
22
  import { parseQuery } from "../urls";
16
- import { MultiError } from "../utils";
23
+ import { ensureOutputHasStableFlag, MultiError } from "../utils";
17
24
  import { applyPatch } from "fast-json-patch";
18
25
  import { UpdateSerializer } from "./UpdateSerializer";
19
26
  import { watchIgnorable } from "@vueuse/core";
20
27
 
21
28
  export const patchPoolingDelay = 150;
22
29
 
30
+ /** Internal interface for plugin data access — injected separately from the app. */
31
+ export interface PluginDataAccess {
32
+ readonly pluginDataMap: Record<string, unknown>;
33
+ setPluginData(pluginId: string, value: unknown): Promise<boolean>;
34
+ initPluginDataSlot(pluginId: string): void;
35
+ }
36
+
23
37
  export const createNextAuthorMarker = (marker: AuthorMarker | undefined): AuthorMarker => ({
24
38
  authorId: marker?.authorId ?? uniqueId(),
25
39
  localVersion: (marker?.localVersion ?? 0) + 1,
@@ -48,13 +62,13 @@ const stringifyForDebug = (v: unknown) => {
48
62
  * @returns A reactive application object with methods, getters, and state.
49
63
  */
50
64
  export function createAppV3<
65
+ Data = unknown,
51
66
  Args = unknown,
52
67
  Outputs extends BlockOutputsBase = BlockOutputsBase,
53
- Data = unknown,
54
68
  Href extends `/${string}` = `/${string}`,
55
69
  >(
56
- state: ValueWithUTag<BlockStateV3<Outputs, Data, Href>>,
57
- platforma: PlatformaV3<Args, Outputs, Data, Href>,
70
+ state: ValueWithUTag<BlockStateV3<Data, Outputs, Href>>,
71
+ platforma: PlatformaExtended<PlatformaV3<Data, Args, Outputs, Href>>,
58
72
  settings: AppSettings,
59
73
  ) {
60
74
  const debug = (msg: string, ...rest: unknown[]) => {
@@ -98,7 +112,19 @@ export function createAppV3<
98
112
  const debounceSpan = settings.debounceSpan ?? 200;
99
113
 
100
114
  const setDataQueue = new UpdateSerializer({ debounceSpan });
115
+ const pluginDataQueues = new Map<string, UpdateSerializer>();
116
+ const getPluginDataQueue = (pluginId: string): UpdateSerializer => {
117
+ let queue = pluginDataQueues.get(pluginId);
118
+ if (!queue) {
119
+ queue = new UpdateSerializer({ debounceSpan });
120
+ pluginDataQueues.set(pluginId, queue);
121
+ }
122
+ return queue;
123
+ };
101
124
  const setNavigationStateQueue = new UpdateSerializer({ debounceSpan });
125
+
126
+ /** Reactive map of plugin data keyed by pluginId. Optimistic state for plugin components. */
127
+ const pluginDataMap = reactive<Record<string, unknown>>({});
102
128
  /**
103
129
  * Reactive snapshot of the application state, including args, outputs, UI state, and navigation state.
104
130
  */
@@ -113,7 +139,20 @@ export function createAppV3<
113
139
  }>;
114
140
 
115
141
  const updateData = async (value: Data) => {
116
- return platforma.mutateStorage({ operation: "update-data", value }, nextAuthorMarker());
142
+ return platforma.mutateStorage({ operation: "update-block-data", value }, nextAuthorMarker());
143
+ };
144
+
145
+ const updatePluginData = async (pluginId: string, value: unknown) => {
146
+ return platforma.mutateStorage(
147
+ { operation: "update-plugin-data", pluginId, value },
148
+ nextAuthorMarker(),
149
+ );
150
+ };
151
+
152
+ /** Derives plugin data for a given pluginId from the current snapshot. */
153
+ const derivePluginDataFromSnapshot = (pluginId: string): unknown => {
154
+ const storage = normalizeBlockStorage(snapshot.value.blockStorage);
155
+ return getPluginData(storage, pluginId);
117
156
  };
118
157
 
119
158
  const setNavigationState = async (state: NavigationState<Href>) => {
@@ -122,7 +161,15 @@ export function createAppV3<
122
161
 
123
162
  const outputs = computed<OutputValues<Outputs>>(() => {
124
163
  const entries = Object.entries(snapshot.value.outputs as Partial<Readonly<Outputs>>).map(
125
- ([k, vOrErr]) => [k, vOrErr.ok && vOrErr.value !== undefined ? vOrErr.value : undefined],
164
+ ([k, outputWithStatus]) =>
165
+ platforma.blockModelInfo.outputs[k]?.withStatus
166
+ ? [k, ensureOutputHasStableFlag(outputWithStatus)]
167
+ : [
168
+ k,
169
+ outputWithStatus.ok && outputWithStatus.value !== undefined
170
+ ? outputWithStatus.value
171
+ : undefined,
172
+ ],
126
173
  );
127
174
  return Object.fromEntries(entries);
128
175
  });
@@ -205,6 +252,10 @@ export function createAppV3<
205
252
  ignoreUpdates(() => {
206
253
  snapshot.value = applyPatch(snapshot.value, patches.value, false, false).newDocument;
207
254
  updateAppModel({ data: deriveDataFromStorage<Data>(snapshot.value.blockStorage) });
255
+ // Reconcile plugin data from external source
256
+ for (const pluginId of Object.keys(pluginDataMap)) {
257
+ pluginDataMap[pluginId] = derivePluginDataFromSnapshot(pluginId);
258
+ }
208
259
  data.isExternalSnapshot = isAuthorChanged;
209
260
  });
210
261
  } else {
@@ -261,7 +312,28 @@ export function createAppV3<
261
312
  },
262
313
  async allSettled() {
263
314
  await delay(0);
264
- return setDataQueue.allSettled();
315
+ const allQueues = [
316
+ setDataQueue.allSettled(),
317
+ ...Array.from(pluginDataQueues.values()).map((q) => q.allSettled()),
318
+ ];
319
+ await Promise.all(allQueues);
320
+ },
321
+ };
322
+
323
+ /** Plugin internals — provided via separate injection key, not exposed on useApp(). */
324
+ const pluginAccess: PluginDataAccess = {
325
+ pluginDataMap,
326
+ setPluginData(pluginId: string, value: unknown): Promise<boolean> {
327
+ pluginDataMap[pluginId] = value;
328
+ debug("setPluginData", pluginId, value);
329
+ return getPluginDataQueue(pluginId).run(() =>
330
+ updatePluginData(pluginId, value).then(unwrapResult),
331
+ );
332
+ },
333
+ initPluginDataSlot(pluginId: string): void {
334
+ if (!(pluginId in pluginDataMap)) {
335
+ pluginDataMap[pluginId] = derivePluginDataFromSnapshot(pluginId);
336
+ }
265
337
  },
266
338
  };
267
339
 
@@ -275,19 +347,19 @@ export function createAppV3<
275
347
  ),
276
348
  };
277
349
 
278
- const app = reactive(Object.assign(appModel, methods, getters));
350
+ const app = Object.assign(reactive(Object.assign(appModel, getters)), methods);
279
351
 
280
352
  if (settings.debug) {
281
353
  // @ts-expect-error (to inspect in console in debug mode)
282
354
  globalThis.__block_app__ = app;
283
355
  }
284
356
 
285
- return app;
357
+ return { app, pluginAccess };
286
358
  }
287
359
 
288
360
  export type BaseAppV3<
361
+ Data = unknown,
289
362
  Args = unknown,
290
363
  Outputs extends BlockOutputsBase = BlockOutputsBase,
291
- Data = unknown,
292
364
  Href extends `/${string}` = `/${string}`,
293
- > = ReturnType<typeof createAppV3<Args, Outputs, Data, Href>>;
365
+ > = ReturnType<typeof createAppV3<Data, Args, Outputs, Href>>["app"];
@@ -9,7 +9,7 @@ import type {
9
9
  Platforma,
10
10
  } from "@platforma-sdk/model";
11
11
  import type { BaseAppV1, createAppV1 } from "./createAppV1";
12
- import { type App } from "../defineApp";
12
+ import { type AppV1 } from "../defineApp";
13
13
  import { computed, type Component } from "vue";
14
14
 
15
15
  declare function __createModel<M, V = unknown>(options: ModelOptions<M, V>): Model<M>;
@@ -84,7 +84,7 @@ const _local = () => {
84
84
  };
85
85
  };
86
86
 
87
- type ExtApp = App<1, BlockOutputsBase, unknown, "/", ReturnType<typeof _local>>;
87
+ type ExtApp = AppV1<1, BlockOutputsBase, unknown, "/", ReturnType<typeof _local>>;
88
88
 
89
89
  type _UpdateArgsParams = Parameters<Parameters<_App1["updateArgs"]>[0]>[0];
90
90
 
@@ -9,7 +9,7 @@ import type {
9
9
  Platforma,
10
10
  } from "@platforma-sdk/model";
11
11
  import type { BaseAppV2, createAppV2 } from "./createAppV2";
12
- import { type App } from "../defineApp";
12
+ import { type AppV2 } from "../defineApp";
13
13
  import { computed, type Component } from "vue";
14
14
 
15
15
  declare function __createModel<M, V = unknown>(options: ModelOptions<M, V>): Model<M>;
@@ -84,7 +84,7 @@ const _local = () => {
84
84
  };
85
85
  };
86
86
 
87
- type ExtApp = App<1, BlockOutputsBase, unknown, "/", ReturnType<typeof _local>>;
87
+ type ExtApp = AppV2<1, BlockOutputsBase, unknown, "/", ReturnType<typeof _local>>;
88
88
 
89
89
  type _UpdateArgsParams = Parameters<Parameters<_App1["updateArgs"]>[0]>[0];
90
90
 
package/src/lib.ts CHANGED
@@ -35,6 +35,8 @@ export * from "./components/PlAdvancedFilter";
35
35
 
36
36
  export * from "./defineApp";
37
37
 
38
+ export { usePluginData } from "./usePluginData";
39
+
38
40
  export * from "./createModel";
39
41
 
40
42
  export * from "./types";
package/src/types.ts CHANGED
@@ -68,7 +68,7 @@ export type AppSettings = {
68
68
  */
69
69
  debug?: boolean;
70
70
  /**
71
- * Debounce span in ms (default is 100ms)
71
+ * Debounce span in ms (default is 200ms)
72
72
  */
73
73
  debounceSpan?: number;
74
74
  /**
@@ -0,0 +1,63 @@
1
+ import { computed, inject } from "vue";
2
+ import { deepClone } from "@milaboratories/helpers";
3
+ import { pluginDataKey } from "./defineApp";
4
+ import type { PluginDataAccess } from "./internal/createAppV3";
5
+
6
+ /**
7
+ * Composable for accessing and updating plugin-specific data.
8
+ *
9
+ * Plugin components are self-contained: they use this composable to read/write
10
+ * their own data slice from BlockStorage without knowing about the parent block's data.
11
+ *
12
+ * Requires a V3 block (BlockModelV3). Throws if used in a V1/V2 block.
13
+ *
14
+ * @param pluginId - The plugin instance ID (must match the ID used in BlockModelV3.plugin()).
15
+ * Must be a static value; changing it after mount will not re-bind the composable.
16
+ * @returns `{ data, updateData }` where `data` is a reactive ref to the plugin's data,
17
+ * and `updateData` returns a promise resolving to `true` if the mutation was sent,
18
+ * or `false` if data is not yet available.
19
+ *
20
+ * @example
21
+ * ```vue
22
+ * <script setup lang="ts">
23
+ * const { data, updateData } = usePluginData<CounterData>('counter');
24
+ *
25
+ * function increment() {
26
+ * updateData((d) => ({ ...d, count: d.count + 1 }));
27
+ * }
28
+ * </script>
29
+ * ```
30
+ */
31
+ export function usePluginData<Data>(pluginId: string) {
32
+ const access = inject<PluginDataAccess>(pluginDataKey);
33
+
34
+ if (!access) {
35
+ throw new Error(
36
+ "usePluginData requires a V3 block (BlockModelV3). " +
37
+ "Make sure the block uses apiVersion 3 and the plugin is installed.",
38
+ );
39
+ }
40
+
41
+ // Initialize the plugin data slot from snapshot if not already done
42
+ access.initPluginDataSlot(pluginId);
43
+
44
+ // Reactive reference to the plugin's data in the shared optimistic map
45
+ const data = computed<Data | undefined>(() => {
46
+ return access.pluginDataMap[pluginId] as Data | undefined;
47
+ });
48
+
49
+ /**
50
+ * Update plugin data with optimistic feedback.
51
+ *
52
+ * @param cb - Callback that receives a deep clone of current data and returns new data.
53
+ * @returns Promise that resolves to true when the mutation is sent
54
+ */
55
+ const updateData = (cb: (current: Data) => Data): Promise<boolean> => {
56
+ const current = data.value;
57
+ if (current === undefined) return Promise.resolve(false);
58
+ const newValue = cb(deepClone(current));
59
+ return access.setPluginData(pluginId, newValue);
60
+ };
61
+
62
+ return { data, updateData };
63
+ }