@platforma-sdk/ui-vue 1.66.2 → 1.68.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.
Files changed (75) hide show
  1. package/.turbo/turbo-build.log +25 -21
  2. package/.turbo/turbo-formatter$colon$check.log +2 -2
  3. package/.turbo/turbo-linter$colon$check.log +2 -2
  4. package/.turbo/turbo-types$colon$check.log +1 -1
  5. package/CHANGELOG.md +37 -0
  6. package/dist/__tests__/setup.d.ts +2 -0
  7. package/dist/__tests__/setup.d.ts.map +1 -0
  8. package/dist/components/PlAgCsvExporter/PlAgCsvExporter.js.map +1 -1
  9. package/dist/components/PlAgCsvExporter/PlAgCsvExporter.vue.d.ts +2 -0
  10. package/dist/components/PlAgCsvExporter/PlAgCsvExporter.vue.d.ts.map +1 -1
  11. package/dist/components/PlAgCsvExporter/PlAgCsvExporter.vue2.js +25 -17
  12. package/dist/components/PlAgCsvExporter/PlAgCsvExporter.vue2.js.map +1 -1
  13. package/dist/components/PlAgCsvExporter/export-csv.d.ts +27 -1
  14. package/dist/components/PlAgCsvExporter/export-csv.d.ts.map +1 -1
  15. package/dist/components/PlAgCsvExporter/export-csv.js +31 -33
  16. package/dist/components/PlAgCsvExporter/export-csv.js.map +1 -1
  17. package/dist/components/PlAgDataTable/PlAgDataTableV2.js.map +1 -1
  18. package/dist/components/PlAgDataTable/PlAgDataTableV2.style.js.map +1 -1
  19. package/dist/components/PlAgDataTable/PlAgDataTableV2.vue.d.ts.map +1 -1
  20. package/dist/components/PlAgDataTable/PlAgDataTableV2.vue2.js +68 -64
  21. package/dist/components/PlAgDataTable/PlAgDataTableV2.vue2.js.map +1 -1
  22. package/dist/components/PlAgDataTable/compositions/useGrid.d.ts.map +1 -1
  23. package/dist/components/PlAgDataTable/compositions/useGrid.js +1 -1
  24. package/dist/components/PlAgDataTable/compositions/useGrid.js.map +1 -1
  25. package/dist/components/PlAgDataTable/sources/table-source-v2.js +2 -2
  26. package/dist/components/PlAgDataTable/sources/table-source-v2.js.map +1 -1
  27. package/dist/components/PlDatasetSelector/PlDatasetSelector.js +9 -0
  28. package/dist/components/PlDatasetSelector/PlDatasetSelector.js.map +1 -0
  29. package/dist/components/PlDatasetSelector/PlDatasetSelector.style.css +1 -0
  30. package/dist/components/PlDatasetSelector/PlDatasetSelector.vue.d.ts +105 -0
  31. package/dist/components/PlDatasetSelector/PlDatasetSelector.vue.d.ts.map +1 -0
  32. package/dist/components/PlDatasetSelector/PlDatasetSelector.vue2.js +111 -0
  33. package/dist/components/PlDatasetSelector/PlDatasetSelector.vue2.js.map +1 -0
  34. package/dist/components/PlDatasetSelector/__tests__/PlDatasetSelector.jsdomtest.d.ts +2 -0
  35. package/dist/components/PlDatasetSelector/__tests__/PlDatasetSelector.jsdomtest.d.ts.map +1 -0
  36. package/dist/components/PlDatasetSelector/index.d.ts +2 -0
  37. package/dist/components/PlDatasetSelector/index.d.ts.map +1 -0
  38. package/dist/components/PlDatasetSelector/index.js +1 -0
  39. package/dist/composition/usePlugin.d.ts.map +1 -0
  40. package/dist/{usePlugin.js → composition/usePlugin.js} +2 -2
  41. package/dist/composition/usePlugin.js.map +1 -0
  42. package/dist/composition/useServices.d.ts +2 -0
  43. package/dist/composition/useServices.d.ts.map +1 -0
  44. package/dist/index.js +15 -14
  45. package/dist/index.js.map +1 -1
  46. package/dist/internal/createAppV3.d.ts +2 -3
  47. package/dist/internal/createAppV3.d.ts.map +1 -1
  48. package/dist/internal/createAppV3.js +1 -1
  49. package/dist/internal/createAppV3.js.map +1 -1
  50. package/dist/internal/service_factories.d.ts +1 -0
  51. package/dist/internal/service_factories.d.ts.map +1 -1
  52. package/dist/internal/service_factories.js +2 -1
  53. package/dist/internal/service_factories.js.map +1 -1
  54. package/dist/lib.d.ts +3 -2
  55. package/dist/lib.d.ts.map +1 -1
  56. package/dist/lib.js +3 -1
  57. package/package.json +9 -7
  58. package/src/__tests__/setup.ts +10 -0
  59. package/src/components/PlAgCsvExporter/PlAgCsvExporter.vue +16 -3
  60. package/src/components/PlAgCsvExporter/export-csv.ts +101 -64
  61. package/src/components/PlAgDataTable/PlAgDataTableV2.vue +6 -1
  62. package/src/components/PlAgDataTable/compositions/useGrid.ts +4 -1
  63. package/src/components/PlAgDataTable/sources/table-source-v2.ts +2 -2
  64. package/src/components/PlDatasetSelector/PlDatasetSelector.vue +164 -0
  65. package/src/components/PlDatasetSelector/__tests__/PlDatasetSelector.jsdomtest.ts +257 -0
  66. package/src/components/PlDatasetSelector/index.ts +1 -0
  67. package/src/{usePlugin.ts → composition/usePlugin.ts} +1 -1
  68. package/src/composition/useServices.ts +5 -0
  69. package/src/internal/createAppV3.ts +4 -4
  70. package/src/internal/service_factories.ts +1 -0
  71. package/src/lib.ts +4 -2
  72. package/vitest.config.mts +22 -0
  73. package/dist/usePlugin.d.ts.map +0 -1
  74. package/dist/usePlugin.js.map +0 -1
  75. /package/dist/{usePlugin.d.ts → composition/usePlugin.d.ts} +0 -0
@@ -396,11 +396,11 @@ function selectDisplayableIndices(
396
396
  .filter(([, spec]) => {
397
397
  switch (spec.type) {
398
398
  case "axis":
399
- return !isPartitionedAxis(spec.id);
399
+ return !isColumnHidden(spec.spec) && !isPartitionedAxis(spec.id);
400
400
  case "column":
401
401
  return (
402
- !isLabelColumnSpec(spec.spec) &&
403
402
  !isColumnHidden(spec.spec) &&
403
+ !isLabelColumnSpec(spec.spec) &&
404
404
  !isLinkerColumnSpec(spec.spec)
405
405
  );
406
406
  }
@@ -0,0 +1,164 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Select a dataset and (optionally) a filter column, emitting a `PrimaryRef`.
4
+ *
5
+ * Behaves like {@link PlDropdownRef} when none of the offered datasets carry
6
+ * filter options. When the selected dataset has compatible filters, a second
7
+ * dropdown appears with the filters plus a "No filter" entry.
8
+ *
9
+ * Accepts `PrimaryRef | PlRef | undefined` as `modelValue` (a plain `PlRef`
10
+ * is treated as an unfiltered dataset), but always emits `PrimaryRef`
11
+ * (or `undefined` when cleared).
12
+ */
13
+ export default {
14
+ name: "PlDatasetSelector",
15
+ };
16
+ </script>
17
+
18
+ <script lang="ts" setup>
19
+ import type { DatasetOption, PlRef, PrimaryRef } from "@platforma-sdk/model";
20
+ import { createPrimaryRef, isPrimaryRef, plRefsEqual } from "@platforma-sdk/model";
21
+ import type { ListOption } from "@milaboratories/uikit";
22
+ import { PlDropdown, PlDropdownRef } from "@milaboratories/uikit";
23
+ import { computed } from "vue";
24
+
25
+ const slots = defineSlots<{
26
+ tooltip?: () => unknown;
27
+ }>();
28
+
29
+ /**
30
+ * v-model value. Accepts `PrimaryRef`, plain `PlRef` (treated as unfiltered),
31
+ * or `undefined`. Writes always emit `PrimaryRef` or `undefined`.
32
+ */
33
+ const model = defineModel<PrimaryRef | PlRef | undefined>();
34
+
35
+ const props = withDefaults(
36
+ defineProps<{
37
+ /** Available datasets, each optionally carrying compatible filter choices. */
38
+ options?: Readonly<DatasetOption[]>;
39
+ /** Label above the dataset dropdown. */
40
+ label?: string;
41
+ /** Helper text below the dataset dropdown (shown when there is no error). */
42
+ helper?: string;
43
+ /** Helper text shown while `options` is undefined (loading). */
44
+ loadingOptionsHelper?: string;
45
+ /** Error message displayed below the dataset dropdown. */
46
+ error?: unknown;
47
+ /** Placeholder when no dataset is selected. */
48
+ placeholder?: string;
49
+ /** Label above the filter dropdown. */
50
+ filterLabel?: string;
51
+ /** Placeholder for the filter dropdown. */
52
+ filterPlaceholder?: string;
53
+ /** Label of the "no filter" entry prepended to the filter options. */
54
+ noFilterLabel?: string;
55
+ /** Show a clear button on the dataset dropdown. */
56
+ clearable?: boolean;
57
+ /** Mark the dataset dropdown as required. */
58
+ required?: boolean;
59
+ /** Disable all interaction. */
60
+ disabled?: boolean;
61
+ }>(),
62
+ {
63
+ options: undefined,
64
+ label: undefined,
65
+ helper: undefined,
66
+ loadingOptionsHelper: undefined,
67
+ error: undefined,
68
+ placeholder: "...",
69
+ filterLabel: "",
70
+ filterPlaceholder: "...",
71
+ noFilterLabel: "No filter",
72
+ clearable: false,
73
+ required: false,
74
+ disabled: false,
75
+ },
76
+ );
77
+
78
+ const selectedDataset = computed<PlRef | undefined>(() => {
79
+ const v = model.value;
80
+ if (v === undefined) return undefined;
81
+ return isPrimaryRef(v) ? v.column : v;
82
+ });
83
+
84
+ const selectedFilter = computed<PlRef | undefined>(() => {
85
+ const v = model.value;
86
+ return isPrimaryRef(v) ? v.filter : undefined;
87
+ });
88
+
89
+ const currentDatasetOption = computed<DatasetOption | undefined>(() => {
90
+ const dataset = selectedDataset.value;
91
+ if (!dataset) return undefined;
92
+ return props.options?.find((o) => plRefsEqual(o.ref, dataset, true));
93
+ });
94
+
95
+ const hasFilters = computed(() => (currentDatasetOption.value?.filters?.length ?? 0) > 0);
96
+
97
+ /**
98
+ * Filter dropdown options. The first entry (`null`) is the "No filter" choice —
99
+ * null distinguishes it from `undefined` (dropdown clear button, disabled here).
100
+ */
101
+ const filterOptions = computed<ListOption<PlRef | null>[]>(() => {
102
+ const filters = currentDatasetOption.value?.filters;
103
+ if (!filters) return [];
104
+ return [
105
+ { label: props.noFilterLabel, value: null } as ListOption<PlRef | null>,
106
+ ...filters.map((f) => ({ label: f.label, value: f.ref }) as ListOption<PlRef | null>),
107
+ ];
108
+ });
109
+
110
+ const filterValue = computed<PlRef | null>(() => selectedFilter.value ?? null);
111
+
112
+ function emitValue(dataset: PlRef | undefined, filter: PlRef | undefined) {
113
+ model.value = dataset === undefined ? undefined : createPrimaryRef(dataset, filter);
114
+ }
115
+
116
+ function onDatasetChange(dataset: PlRef | undefined) {
117
+ emitValue(dataset, undefined);
118
+ }
119
+
120
+ function onFilterChange(value: PlRef | null | undefined) {
121
+ const dataset = selectedDataset.value;
122
+ if (!dataset) return;
123
+ emitValue(dataset, value ?? undefined);
124
+ }
125
+ </script>
126
+
127
+ <template>
128
+ <div class="pl-dataset-selector">
129
+ <PlDropdownRef
130
+ :model-value="selectedDataset"
131
+ :options="options"
132
+ :label="label"
133
+ :helper="helper"
134
+ :loading-options-helper="loadingOptionsHelper"
135
+ :error="error"
136
+ :placeholder="placeholder"
137
+ :clearable="clearable"
138
+ :required="required"
139
+ :disabled="disabled"
140
+ @update:model-value="onDatasetChange"
141
+ >
142
+ <template v-if="slots.tooltip" #tooltip>
143
+ <slot name="tooltip" />
144
+ </template>
145
+ </PlDropdownRef>
146
+ <PlDropdown
147
+ v-if="hasFilters"
148
+ :model-value="filterValue"
149
+ :options="filterOptions"
150
+ :label="filterLabel"
151
+ :placeholder="filterPlaceholder"
152
+ :disabled="disabled"
153
+ @update:model-value="onFilterChange"
154
+ />
155
+ </div>
156
+ </template>
157
+
158
+ <style scoped>
159
+ .pl-dataset-selector {
160
+ display: flex;
161
+ flex-direction: column;
162
+ gap: 12px;
163
+ }
164
+ </style>
@@ -0,0 +1,257 @@
1
+ import { flushPromises, mount } from "@vue/test-utils";
2
+ import type { DatasetOption, PrimaryRef } from "@platforma-sdk/model";
3
+ import { createPlRef, createPrimaryRef } from "@platforma-sdk/model";
4
+ import { describe, expect, it } from "vitest";
5
+ import PlDatasetSelector from "../PlDatasetSelector.vue";
6
+
7
+ const datasetA = createPlRef("1", "out-a", true);
8
+ const datasetB = createPlRef("2", "out-b", true);
9
+ const filterA1 = createPlRef("1", "filter-a1");
10
+ const filterA2 = createPlRef("1", "filter-a2");
11
+
12
+ const optionsWithFilters: DatasetOption[] = [
13
+ {
14
+ label: "Dataset A",
15
+ ref: datasetA,
16
+ filters: [
17
+ { label: "Top 1000", ref: filterA1 },
18
+ { label: "High quality", ref: filterA2 },
19
+ ],
20
+ },
21
+ // Dataset B has no filters — filter dropdown must stay hidden.
22
+ { label: "Dataset B", ref: datasetB },
23
+ ];
24
+
25
+ const datasetC = createPlRef("3", "out-c", true);
26
+
27
+ const optionsNoFilters: DatasetOption[] = [{ label: "Dataset B", ref: datasetB }];
28
+
29
+ const optionsWithEmptyFilters: DatasetOption[] = [
30
+ { label: "Dataset C", ref: datasetC, filters: [] },
31
+ ];
32
+
33
+ async function pickOption(index: number) {
34
+ const options = [...document.body.querySelectorAll(".dropdown-list-item")] as HTMLElement[];
35
+ options[index].click();
36
+ await flushPromises();
37
+ }
38
+
39
+ describe("PlDatasetSelector", () => {
40
+ it("renders a single dropdown when no dataset has filters", async () => {
41
+ const wrapper = mount(PlDatasetSelector, {
42
+ props: { modelValue: undefined, options: optionsNoFilters },
43
+ attachTo: document.body,
44
+ });
45
+ await flushPromises();
46
+
47
+ const selector = wrapper.find(".pl-dataset-selector");
48
+ expect(selector.exists()).toBe(true);
49
+ // Only PlDropdownRef is rendered — no filter dropdown.
50
+ expect(selector.element.children.length).toBe(1);
51
+ wrapper.unmount();
52
+ });
53
+
54
+ it("shows the filter dropdown when the selected dataset has filters", async () => {
55
+ const wrapper = mount(PlDatasetSelector, {
56
+ props: { modelValue: createPrimaryRef(datasetA), options: optionsWithFilters },
57
+ attachTo: document.body,
58
+ });
59
+ await flushPromises();
60
+
61
+ expect(wrapper.find(".pl-dataset-selector").element.children.length).toBe(2);
62
+ wrapper.unmount();
63
+ });
64
+
65
+ it("hides the filter dropdown when the selected dataset has no filters", async () => {
66
+ const wrapper = mount(PlDatasetSelector, {
67
+ props: { modelValue: createPrimaryRef(datasetB), options: optionsWithFilters },
68
+ attachTo: document.body,
69
+ });
70
+ await flushPromises();
71
+
72
+ expect(wrapper.find(".pl-dataset-selector").element.children.length).toBe(1);
73
+ wrapper.unmount();
74
+ });
75
+
76
+ it("emits PrimaryRef with filter: undefined when dataset changes", async () => {
77
+ const wrapper = mount(PlDatasetSelector, {
78
+ props: {
79
+ modelValue: createPrimaryRef(datasetA, filterA1),
80
+ options: optionsWithFilters,
81
+ "onUpdate:modelValue": (e) => wrapper.setProps({ modelValue: e }),
82
+ },
83
+ attachTo: document.body,
84
+ });
85
+ await flushPromises();
86
+
87
+ // Open the dataset dropdown (the first input — dataset comes first).
88
+ const inputs = wrapper.findAll("input");
89
+ await inputs[0].trigger("focus");
90
+
91
+ // Dataset A is already selected (index 0); pick Dataset B (index 1).
92
+ await pickOption(1);
93
+
94
+ const emitted = wrapper.emitted("update:modelValue");
95
+ expect(emitted).toBeDefined();
96
+ const last = emitted![emitted!.length - 1][0] as PrimaryRef;
97
+ expect(last).toEqual({ __isPrimaryRef: "v1", column: datasetB });
98
+ wrapper.unmount();
99
+ });
100
+
101
+ it("emits PrimaryRef with filter set when a filter is picked", async () => {
102
+ const wrapper = mount(PlDatasetSelector, {
103
+ props: {
104
+ modelValue: createPrimaryRef(datasetA),
105
+ options: optionsWithFilters,
106
+ "onUpdate:modelValue": (e) => wrapper.setProps({ modelValue: e }),
107
+ },
108
+ attachTo: document.body,
109
+ });
110
+ await flushPromises();
111
+
112
+ // Open the filter dropdown — it's the second input in the component.
113
+ const inputs = wrapper.findAll("input");
114
+ expect(inputs.length).toBe(2);
115
+ await inputs[1].trigger("focus");
116
+
117
+ // Options are: [No filter, Top 1000, High quality]. Pick "Top 1000" (index 1).
118
+ await pickOption(1);
119
+
120
+ const emitted = wrapper.emitted("update:modelValue");
121
+ expect(emitted).toBeDefined();
122
+ const last = emitted![emitted!.length - 1][0] as PrimaryRef;
123
+ expect(last).toEqual({ __isPrimaryRef: "v1", column: datasetA, filter: filterA1 });
124
+ wrapper.unmount();
125
+ });
126
+
127
+ it("emits PrimaryRef with filter: undefined when 'No filter' is picked", async () => {
128
+ const wrapper = mount(PlDatasetSelector, {
129
+ props: {
130
+ modelValue: createPrimaryRef(datasetA, filterA1),
131
+ options: optionsWithFilters,
132
+ "onUpdate:modelValue": (e) => wrapper.setProps({ modelValue: e }),
133
+ },
134
+ attachTo: document.body,
135
+ });
136
+ await flushPromises();
137
+
138
+ const inputs = wrapper.findAll("input");
139
+ await inputs[1].trigger("focus");
140
+ // Pick "No filter" (index 0).
141
+ await pickOption(0);
142
+
143
+ const emitted = wrapper.emitted("update:modelValue");
144
+ expect(emitted).toBeDefined();
145
+ const last = emitted![emitted!.length - 1][0] as PrimaryRef;
146
+ expect(last).toEqual({ __isPrimaryRef: "v1", column: datasetA });
147
+ expect("filter" in last).toBe(false);
148
+ wrapper.unmount();
149
+ });
150
+
151
+ it("accepts plain PlRef as modelValue for backward compat (filterless dataset)", async () => {
152
+ const wrapper = mount(PlDatasetSelector, {
153
+ props: { modelValue: datasetB, options: optionsWithFilters },
154
+ attachTo: document.body,
155
+ });
156
+ await flushPromises();
157
+
158
+ expect(wrapper.find(".pl-dataset-selector").element.children.length).toBe(1);
159
+ wrapper.unmount();
160
+ });
161
+
162
+ it("accepts plain PlRef as modelValue for backward compat (dataset with filters)", async () => {
163
+ const wrapper = mount(PlDatasetSelector, {
164
+ props: { modelValue: datasetA, options: optionsWithFilters },
165
+ attachTo: document.body,
166
+ });
167
+ await flushPromises();
168
+
169
+ // PlRef matching dataset A — filter dropdown should appear since A has filters.
170
+ expect(wrapper.find(".pl-dataset-selector").element.children.length).toBe(2);
171
+ wrapper.unmount();
172
+ });
173
+
174
+ it("hides filter dropdown when dataset has filters: [] (empty array)", async () => {
175
+ const wrapper = mount(PlDatasetSelector, {
176
+ props: { modelValue: createPrimaryRef(datasetC), options: optionsWithEmptyFilters },
177
+ attachTo: document.body,
178
+ });
179
+ await flushPromises();
180
+
181
+ expect(wrapper.find(".pl-dataset-selector").element.children.length).toBe(1);
182
+ wrapper.unmount();
183
+ });
184
+
185
+ it("filter dropdown defaults to 'No filter' when dataset has filters but none selected", async () => {
186
+ const wrapper = mount(PlDatasetSelector, {
187
+ props: {
188
+ modelValue: createPrimaryRef(datasetA),
189
+ options: optionsWithFilters,
190
+ "onUpdate:modelValue": (e) => wrapper.setProps({ modelValue: e }),
191
+ },
192
+ attachTo: document.body,
193
+ });
194
+ await flushPromises();
195
+
196
+ // No emission on mount — the component does not auto-select a filter.
197
+ expect(wrapper.emitted("update:modelValue")).toBeUndefined();
198
+
199
+ // Filter dropdown is visible.
200
+ const inputs = wrapper.findAll("input");
201
+ expect(inputs.length).toBe(2);
202
+
203
+ // Open filter dropdown and verify "No filter" is the first option.
204
+ await inputs[1].trigger("focus");
205
+ const items = document.body.querySelectorAll(".dropdown-list-item");
206
+ expect(items.length).toBe(3); // No filter, Top 1000, High quality
207
+ expect(items[0].textContent).toContain("No filter");
208
+ wrapper.unmount();
209
+ });
210
+
211
+ it("emits PrimaryRef without filter key when selecting a filterless dataset", async () => {
212
+ const wrapper = mount(PlDatasetSelector, {
213
+ props: {
214
+ modelValue: undefined,
215
+ options: optionsNoFilters,
216
+ "onUpdate:modelValue": (e) => wrapper.setProps({ modelValue: e }),
217
+ },
218
+ attachTo: document.body,
219
+ });
220
+ await flushPromises();
221
+
222
+ const inputs = wrapper.findAll("input");
223
+ await inputs[0].trigger("focus");
224
+ await pickOption(0);
225
+
226
+ const emitted = wrapper.emitted("update:modelValue");
227
+ expect(emitted).toBeDefined();
228
+ const last = emitted![emitted!.length - 1][0] as PrimaryRef;
229
+ expect(last).toEqual({ __isPrimaryRef: "v1", column: datasetB });
230
+ expect("filter" in last).toBe(false);
231
+ wrapper.unmount();
232
+ });
233
+
234
+ it("emits undefined when cleared via the dataset dropdown", async () => {
235
+ const wrapper = mount(PlDatasetSelector, {
236
+ props: {
237
+ modelValue: createPrimaryRef(datasetA, filterA1),
238
+ options: optionsWithFilters,
239
+ clearable: true,
240
+ "onUpdate:modelValue": (e) => wrapper.setProps({ modelValue: e }),
241
+ },
242
+ attachTo: document.body,
243
+ });
244
+ await flushPromises();
245
+
246
+ // PlDropdown's clear button carries the ".clear" class.
247
+ const clearBtn = wrapper.find(".clear");
248
+ expect(clearBtn.exists()).toBe(true);
249
+ await clearBtn.trigger("click");
250
+
251
+ const emitted = wrapper.emitted("update:modelValue");
252
+ expect(emitted).toBeDefined();
253
+ const last = emitted![emitted!.length - 1][0];
254
+ expect(last).toBeUndefined();
255
+ wrapper.unmount();
256
+ });
257
+ });
@@ -0,0 +1 @@
1
+ export { default as PlDatasetSelector } from "./PlDatasetSelector.vue";
@@ -1,5 +1,5 @@
1
1
  import { inject, type Reactive } from "vue";
2
- import { pluginDataKey } from "./defineApp";
2
+ import { pluginDataKey } from "../defineApp";
3
3
  import type {
4
4
  PluginHandle,
5
5
  InferFactoryData,
@@ -0,0 +1,5 @@
1
+ import { getServices } from "../internal/getServices";
2
+
3
+ export function useServices() {
4
+ return getServices();
5
+ }
@@ -12,8 +12,9 @@ import type {
12
12
  PluginHandle,
13
13
  InferFactoryData,
14
14
  InferFactoryOutputs,
15
- InferFactoryUiServices,
16
15
  PluginFactoryLike,
16
+ UiServices as AllUiServices,
17
+ InferFactoryUiServices,
17
18
  } from "@platforma-sdk/model";
18
19
  import {
19
20
  hasAbortError,
@@ -23,7 +24,6 @@ import {
23
24
  isPluginOutputKey,
24
25
  pluginOutputPrefix,
25
26
  } from "@platforma-sdk/model";
26
- import { type UiServices as AllUiServices } from "@milaboratories/pl-model-common";
27
27
  import type { Ref } from "vue";
28
28
  import { reactive, computed, ref, markRaw } from "vue";
29
29
  import type { OutputValues, OutputErrors, AppSettings } from "../types";
@@ -32,7 +32,7 @@ import { ensureOutputHasStableFlag, MultiError } from "../utils";
32
32
  import { applyPatch } from "fast-json-patch";
33
33
  import { UpdateSerializer } from "./UpdateSerializer";
34
34
  import { watchIgnorable } from "@vueuse/core";
35
- import type { PluginState, PluginAccess } from "../usePlugin";
35
+ import type { PluginState, PluginAccess } from "../composition/usePlugin";
36
36
  import { logDebug, logError } from "./utils";
37
37
  import { getServices } from "./getServices";
38
38
 
@@ -208,7 +208,7 @@ export function createAppV3<
208
208
  .dispose()
209
209
  .then(unwrapResult)
210
210
  .catch((err) => {
211
- error("error in dispose", err);
211
+ error("platforma error in dispose", err);
212
212
  });
213
213
  });
214
214
 
@@ -19,5 +19,6 @@ export function createUiServiceRegistry(options: UiServiceOptions) {
19
19
  return new UiServiceRegistry(Services, {
20
20
  PFrameSpec: () => new SpecDriver(),
21
21
  PFrame: () => options.proxy(Services.PFrame),
22
+ Dialog: () => options.proxy(Services.Dialog),
22
23
  });
23
24
  }
package/src/lib.ts CHANGED
@@ -33,10 +33,12 @@ export * from "./components/PlBtnExportArchive";
33
33
 
34
34
  export * from "./components/PlAdvancedFilter";
35
35
 
36
+ export * from "./components/PlDatasetSelector";
37
+
36
38
  export * from "./defineApp";
37
39
 
38
- export { usePlugin } from "./usePlugin";
39
- export type { PluginState } from "./usePlugin";
40
+ export { usePlugin } from "./composition/usePlugin";
41
+ export type { PluginState } from "./composition/usePlugin";
40
42
  export type {
41
43
  PluginHandle,
42
44
  PluginFactoryLike,
package/vitest.config.mts CHANGED
@@ -3,8 +3,30 @@ import { defineConfig } from "vitest/config";
3
3
 
4
4
  export default defineConfig(
5
5
  createVitestVueConfig({
6
+ resolve: {
7
+ conditions: ["sources"],
8
+ },
6
9
  test: {
7
10
  includeSource: ["src/**/*.{js,ts}"],
11
+ setupFiles: ["./src/__tests__/setup.ts"],
12
+ projects: [
13
+ {
14
+ extends: true,
15
+ test: {
16
+ name: "node",
17
+ environment: "node",
18
+ include: ["src/**/*.test.ts"],
19
+ },
20
+ },
21
+ {
22
+ extends: true,
23
+ test: {
24
+ name: "jsdom",
25
+ environment: "jsdom",
26
+ include: ["src/**/*.jsdomtest.ts"],
27
+ },
28
+ },
29
+ ],
8
30
  },
9
31
  }),
10
32
  );
@@ -1 +0,0 @@
1
- {"version":3,"file":"usePlugin.d.ts","sourceRoot":"","sources":["../src/usePlugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,KAAK,CAAC;AAE5C,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAChB,mBAAmB,EACnB,sBAAsB,EACtB,iBAAiB,EAClB,MAAM,sBAAsB,CAAC;AAE9B,sEAAsE;AACtE,MAAM,WAAW,WAAW,CAC1B,IAAI,GAAG,OAAO,EACd,OAAO,GAAG,OAAO,EACjB,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAElC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;QACvB,IAAI,EAAE,IAAI,CAAC;QACX,OAAO,EAAE,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5C;aAAG,CAAC,IAAI,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,SAAS;SAAE,GAChD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC5B,YAAY,EAAE,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjD;aAAG,CAAC,IAAI,MAAM,OAAO,CAAC,CAAC,EAAE,KAAK;SAAE,GAChC,MAAM,CAAC,MAAM,EAAE,KAAK,GAAG,SAAS,CAAC,CAAC;KACvC,CAAC,CAAC;IACH,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;CAC7B;AAED,wFAAwF;AACxF,MAAM,WAAW,YAAY;IAC3B,sBAAsB,CAAC,CAAC,SAAS,iBAAiB,EAChD,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,WAAW,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAC,CAAC,EAAE,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC;CACxF;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,iBAAiB,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,uFAW7E"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"usePlugin.js","names":[],"sources":["../src/usePlugin.ts"],"sourcesContent":["import { inject, type Reactive } from \"vue\";\nimport { pluginDataKey } from \"./defineApp\";\nimport type {\n PluginHandle,\n InferFactoryData,\n InferFactoryOutputs,\n InferFactoryUiServices,\n PluginFactoryLike,\n} from \"@platforma-sdk/model\";\n\n/** Per-plugin reactive model exposed to consumers via usePlugin(). */\nexport interface PluginState<\n Data = unknown,\n Outputs = unknown,\n Services = Record<string, unknown>,\n> {\n readonly model: Reactive<{\n data: Data;\n outputs: Outputs extends Record<string, unknown>\n ? { [K in keyof Outputs]: Outputs[K] | undefined }\n : Record<string, unknown>;\n outputErrors: Outputs extends Record<string, unknown>\n ? { [K in keyof Outputs]?: Error }\n : Record<string, Error | undefined>;\n }>;\n readonly services: Services;\n}\n\n/** Internal interface for plugin access — provided via Vue injection to usePlugin(). */\nexport interface PluginAccess {\n getOrCreatePluginState<F extends PluginFactoryLike>(\n handle: PluginHandle<F>,\n ): PluginState<InferFactoryData<F>, InferFactoryOutputs<F>, InferFactoryUiServices<F>>;\n}\n\n/**\n * Composable for accessing a plugin's reactive model: data, outputs, and outputErrors.\n *\n * Mirrors the `app.model` access pattern — `plugin.model.data` is reactive and deep-watched,\n * mutations are automatically queued and sent to storage.\n *\n * @param handle - Opaque plugin handle obtained from `app.plugins`.\n * @typeParam F - The plugin factory type (inferred from the handle)\n *\n * @example\n * ```vue\n * <script setup lang=\"ts\">\n * import { usePlugin, type InferPluginHandle } from '@platforma-sdk/ui-vue';\n * import type { CounterPlugin } from './plugins/counter';\n *\n * const props = defineProps<{ instance: InferPluginHandle<CounterPlugin> }>();\n * const plugin = usePlugin(props.instance);\n *\n * plugin.model.data.count += 1; // reactive, triggers storage update\n * plugin.model.outputs.displayText // computed, plugin's own outputs only\n * plugin.model.outputErrors.displayText // Error | undefined\n * </script>\n * ```\n */\nexport function usePlugin<F extends PluginFactoryLike>(handle: PluginHandle<F>) {\n const access = inject<PluginAccess>(pluginDataKey);\n\n if (!access) {\n throw new Error(\n \"usePlugin requires a V3 block (BlockModelV3). \" +\n \"Make sure the block uses apiVersion 3 and the plugin is installed.\",\n );\n }\n\n return access.getOrCreatePluginState<F>(handle);\n}\n"],"mappings":";;;AA2DA,SAAgB,EAAuC,GAAyB;CAC9E,IAAM,IAAS,EAAqB,EAAc;AAElD,KAAI,CAAC,EACH,OAAU,MACR,mHAED;AAGH,QAAO,EAAO,uBAA0B,EAAO"}