@morscherlab/mint-sdk 1.0.0 → 1.0.1

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 (68) hide show
  1. package/dist/BaseModal-B9UA8Y_I.js +165 -0
  2. package/dist/BaseModal-B9UA8Y_I.js.map +1 -0
  3. package/dist/BaseSelect-DksaKYq_.js +176 -0
  4. package/dist/BaseSelect-DksaKYq_.js.map +1 -0
  5. package/dist/ExperimentPopover-CCYB1oWp.js +361 -0
  6. package/dist/ExperimentPopover-CCYB1oWp.js.map +1 -0
  7. package/dist/ExperimentPopover-D0bg_fqM.js +3 -0
  8. package/dist/ExperimentSelectorModal-B_kPbXcg.js +4 -0
  9. package/dist/ExperimentSelectorModal-wm7yUdAr.js +720 -0
  10. package/dist/ExperimentSelectorModal-wm7yUdAr.js.map +1 -0
  11. package/dist/SettingsModal-L7Ejny45.js +5 -0
  12. package/dist/SettingsModal-LEKI6Ebl.js +521 -0
  13. package/dist/SettingsModal-LEKI6Ebl.js.map +1 -0
  14. package/dist/{auth-BulIv_km.js → auth-D9q2GIcv.js} +3 -80
  15. package/dist/auth-D9q2GIcv.js.map +1 -0
  16. package/dist/components/DataFrame.vue.d.ts +3 -0
  17. package/dist/components/ExperimentDataViewer.vue.d.ts +2 -0
  18. package/dist/components/PluginWorkspaceView.vue.d.ts +2 -2
  19. package/dist/components/index.js +7 -2
  20. package/dist/{components-DtX3LDLq.js → components-CdjRzHI2.js} +533 -2025
  21. package/dist/components-CdjRzHI2.js.map +1 -0
  22. package/dist/composables/index.js +9 -3
  23. package/dist/composables/usePluginClient.d.ts +2 -1
  24. package/dist/{composables-wNt7VtkF.js → composables-DJgqPrlR.js} +7 -12
  25. package/dist/{composables-wNt7VtkF.js.map → composables-DJgqPrlR.js.map} +1 -1
  26. package/dist/experiment-utils-hGXMHlAc.js +109 -0
  27. package/dist/experiment-utils-hGXMHlAc.js.map +1 -0
  28. package/dist/index.js +16 -5
  29. package/dist/index.js.map +1 -1
  30. package/dist/install.js +7 -2
  31. package/dist/install.js.map +1 -1
  32. package/dist/permissions.js +81 -0
  33. package/dist/permissions.js.map +1 -0
  34. package/dist/stores/index.js +1 -1
  35. package/dist/styles.css +3233 -3185
  36. package/dist/templates/index.js +3 -1
  37. package/dist/templates-Do43ZIMb.js +5065 -0
  38. package/dist/templates-Do43ZIMb.js.map +1 -0
  39. package/dist/{templates-DSbHJC4v.js → useControlSchema-0n8Bcftq.js} +10 -5335
  40. package/dist/useControlSchema-0n8Bcftq.js.map +1 -0
  41. package/dist/useDropdownState-Ben4DnjJ.js +47 -0
  42. package/dist/useDropdownState-Ben4DnjJ.js.map +1 -0
  43. package/dist/useEventListener-CfVkP9Xz.js +57 -0
  44. package/dist/useEventListener-CfVkP9Xz.js.map +1 -0
  45. package/dist/useExperimentSelector-BpZklTbV.js +469 -0
  46. package/dist/useExperimentSelector-BpZklTbV.js.map +1 -0
  47. package/dist/useFormBuilder-COfYWDuC.js +729 -0
  48. package/dist/useFormBuilder-COfYWDuC.js.map +1 -0
  49. package/dist/{useProtocolTemplates-DwBhEPPU.js → useProtocolTemplates-TUQO_F3n.js} +8 -1298
  50. package/dist/useProtocolTemplates-TUQO_F3n.js.map +1 -0
  51. package/dist/utils/pluginIcon.d.ts +29 -2
  52. package/package.json +5 -1
  53. package/src/__tests__/components/DataFrame.test.ts +37 -0
  54. package/src/__tests__/components/PluginIcon.test.ts +77 -0
  55. package/src/__tests__/composables/usePluginClient.test.ts +11 -10
  56. package/src/components/AppTopBar.vue +7 -6
  57. package/src/components/DataFrame.vue +27 -2
  58. package/src/components/ExperimentDataViewer.vue +5 -1
  59. package/src/components/PluginIcon.story.vue +31 -1
  60. package/src/components/PluginIcon.vue +94 -4
  61. package/src/composables/usePluginClient.ts +3 -12
  62. package/src/styles/components/dataframe.css +26 -0
  63. package/src/styles/components/plugin-icon.css +5 -0
  64. package/src/utils/pluginIcon.ts +159 -2
  65. package/dist/auth-BulIv_km.js.map +0 -1
  66. package/dist/components-DtX3LDLq.js.map +0 -1
  67. package/dist/templates-DSbHJC4v.js.map +0 -1
  68. package/dist/useProtocolTemplates-DwBhEPPU.js.map +0 -1
@@ -0,0 +1,47 @@
1
+ import { t as useEventListener } from "./useEventListener-CfVkP9Xz.js";
2
+ import { ref } from "vue";
3
+ //#region src/composables/useDropdownState.ts
4
+ /** Shared disclosure state for dropdown-style SDK components. */
5
+ function useDropdownState(options = {}) {
6
+ const isOpen = ref(false);
7
+ const rootRef = ref(null);
8
+ const closeOnOutsideClick = options.closeOnOutsideClick ?? true;
9
+ const closeOnEscape = options.closeOnEscape ?? true;
10
+ function open() {
11
+ if (isOpen.value) return;
12
+ isOpen.value = true;
13
+ options.onOpen?.();
14
+ }
15
+ function close() {
16
+ if (!isOpen.value) return;
17
+ isOpen.value = false;
18
+ options.onClose?.();
19
+ }
20
+ function toggle() {
21
+ if (isOpen.value) close();
22
+ else open();
23
+ }
24
+ function handleClickOutside(event) {
25
+ if (!closeOnOutsideClick || !isOpen.value || !rootRef.value) return;
26
+ const target = event.target;
27
+ if (target instanceof Node && !rootRef.value.contains(target) && !options.insideRefs?.some((ref) => ref.value?.contains(target))) close();
28
+ }
29
+ function handleKeydown(event) {
30
+ if (closeOnEscape && event.key === "Escape") close();
31
+ }
32
+ if (closeOnOutsideClick) useEventListener(() => document, "click", handleClickOutside);
33
+ if (closeOnEscape) useEventListener(() => document, "keydown", handleKeydown);
34
+ return {
35
+ isOpen,
36
+ rootRef,
37
+ open,
38
+ close,
39
+ toggle,
40
+ handleClickOutside,
41
+ handleKeydown
42
+ };
43
+ }
44
+ //#endregion
45
+ export { useDropdownState as t };
46
+
47
+ //# sourceMappingURL=useDropdownState-Ben4DnjJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDropdownState-Ben4DnjJ.js","names":[],"sources":["../src/composables/useDropdownState.ts"],"sourcesContent":["import { ref, type Ref } from 'vue'\nimport { useEventListener } from './useEventListener'\n\nexport interface UseDropdownStateOptions {\n closeOnOutsideClick?: boolean\n closeOnEscape?: boolean\n insideRefs?: Array<Ref<HTMLElement | null>>\n onOpen?: () => void\n onClose?: () => void\n}\n\nexport interface UseDropdownStateReturn {\n isOpen: Ref<boolean>\n rootRef: Ref<HTMLElement | null>\n open: () => void\n close: () => void\n toggle: () => void\n handleClickOutside: (event: MouseEvent) => void\n handleKeydown: (event: KeyboardEvent) => void\n}\n\n/** Shared disclosure state for dropdown-style SDK components. */\nexport function useDropdownState(options: UseDropdownStateOptions = {}): UseDropdownStateReturn {\n const isOpen = ref(false)\n const rootRef = ref<HTMLElement | null>(null)\n const closeOnOutsideClick = options.closeOnOutsideClick ?? true\n const closeOnEscape = options.closeOnEscape ?? true\n\n function open(): void {\n if (isOpen.value) return\n isOpen.value = true\n options.onOpen?.()\n }\n\n function close(): void {\n if (!isOpen.value) return\n isOpen.value = false\n options.onClose?.()\n }\n\n function toggle(): void {\n if (isOpen.value) {\n close()\n } else {\n open()\n }\n }\n\n function handleClickOutside(event: MouseEvent): void {\n if (!closeOnOutsideClick || !isOpen.value || !rootRef.value) return\n const target = event.target\n if (\n target instanceof Node &&\n !rootRef.value.contains(target) &&\n !options.insideRefs?.some(ref => ref.value?.contains(target))\n ) {\n close()\n }\n }\n\n function handleKeydown(event: KeyboardEvent): void {\n if (closeOnEscape && event.key === 'Escape') {\n close()\n }\n }\n\n if (closeOnOutsideClick) {\n useEventListener(() => document, 'click', handleClickOutside)\n }\n if (closeOnEscape) {\n useEventListener(() => document, 'keydown', handleKeydown)\n }\n\n return {\n isOpen,\n rootRef,\n open,\n close,\n toggle,\n handleClickOutside,\n handleKeydown,\n }\n}\n"],"mappings":";;;;AAsBA,SAAgB,iBAAiB,UAAmC,EAAE,EAA0B;CAC9F,MAAM,SAAS,IAAI,MAAM;CACzB,MAAM,UAAU,IAAwB,KAAK;CAC7C,MAAM,sBAAsB,QAAQ,uBAAuB;CAC3D,MAAM,gBAAgB,QAAQ,iBAAiB;CAE/C,SAAS,OAAa;AACpB,MAAI,OAAO,MAAO;AAClB,SAAO,QAAQ;AACf,UAAQ,UAAU;;CAGpB,SAAS,QAAc;AACrB,MAAI,CAAC,OAAO,MAAO;AACnB,SAAO,QAAQ;AACf,UAAQ,WAAW;;CAGrB,SAAS,SAAe;AACtB,MAAI,OAAO,MACT,QAAO;MAEP,OAAM;;CAIV,SAAS,mBAAmB,OAAyB;AACnD,MAAI,CAAC,uBAAuB,CAAC,OAAO,SAAS,CAAC,QAAQ,MAAO;EAC7D,MAAM,SAAS,MAAM;AACrB,MACE,kBAAkB,QAClB,CAAC,QAAQ,MAAM,SAAS,OAAO,IAC/B,CAAC,QAAQ,YAAY,MAAK,QAAO,IAAI,OAAO,SAAS,OAAO,CAAC,CAE7D,QAAO;;CAIX,SAAS,cAAc,OAA4B;AACjD,MAAI,iBAAiB,MAAM,QAAQ,SACjC,QAAO;;AAIX,KAAI,oBACF,wBAAuB,UAAU,SAAS,mBAAmB;AAE/D,KAAI,cACF,wBAAuB,UAAU,WAAW,cAAc;AAG5D,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACD"}
@@ -0,0 +1,57 @@
1
+ import { isRef, onMounted, onUnmounted, watch } from "vue";
2
+ //#region src/composables/useEventListener.ts
3
+ /** Bind a DOM event listener on mount, remove it on unmount, and rebind when a ref target changes. */
4
+ function useEventListener(target, type, listener, options) {
5
+ let stopCurrent;
6
+ let mounted = false;
7
+ function stop() {
8
+ stopCurrent?.();
9
+ stopCurrent = void 0;
10
+ }
11
+ function start() {
12
+ stop();
13
+ if (!mounted || !isEnabled(options)) return;
14
+ const resolvedTarget = resolveEventTarget(target);
15
+ if (!resolvedTarget) return;
16
+ const eventListener = listener;
17
+ const eventOptions = getEventOptions(options);
18
+ resolvedTarget.addEventListener(type, eventListener, eventOptions);
19
+ stopCurrent = () => {
20
+ resolvedTarget.removeEventListener(type, eventListener, eventOptions);
21
+ };
22
+ }
23
+ onMounted(() => {
24
+ mounted = true;
25
+ start();
26
+ });
27
+ if (isRef(target)) watch(target, start, { flush: "post" });
28
+ const enabled = getEnabledOption(options);
29
+ if (isRef(enabled)) watch(enabled, start, { flush: "post" });
30
+ onUnmounted(() => {
31
+ mounted = false;
32
+ stop();
33
+ });
34
+ return stop;
35
+ }
36
+ function resolveEventTarget(target) {
37
+ if (isRef(target)) return target.value;
38
+ if (typeof target === "function") return target();
39
+ return target;
40
+ }
41
+ function getEnabledOption(options) {
42
+ return typeof options === "object" ? options.enabled : void 0;
43
+ }
44
+ function isEnabled(options) {
45
+ const enabled = getEnabledOption(options);
46
+ if (isRef(enabled)) return enabled.value;
47
+ return enabled ?? true;
48
+ }
49
+ function getEventOptions(options) {
50
+ if (typeof options !== "object") return options;
51
+ const { enabled: _enabled, ...eventOptions } = options;
52
+ return eventOptions;
53
+ }
54
+ //#endregion
55
+ export { useEventListener as t };
56
+
57
+ //# sourceMappingURL=useEventListener-CfVkP9Xz.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useEventListener-CfVkP9Xz.js","names":[],"sources":["../src/composables/useEventListener.ts"],"sourcesContent":["import { isRef, onMounted, onUnmounted, watch, type Ref } from 'vue'\n\nexport type EventTargetGetter<TTarget extends EventTarget = EventTarget> =\n () => TTarget | null | undefined\n\nexport type EventTargetLike<TTarget extends EventTarget = EventTarget> =\n | TTarget\n | Ref<TTarget | null | undefined>\n | EventTargetGetter<TTarget>\n | null\n | undefined\n\nexport type EventListenerEnabled = boolean | Ref<boolean>\n\nexport interface UseEventListenerObjectOptions extends AddEventListenerOptions {\n enabled?: EventListenerEnabled\n}\n\nexport type UseEventListenerOptions = boolean | UseEventListenerObjectOptions\nexport type EventListenerStop = () => void\n\nexport type EventMapFor<TTarget extends EventTarget> =\n TTarget extends Window\n ? WindowEventMap\n : TTarget extends Document\n ? DocumentEventMap\n : TTarget extends HTMLElement\n ? HTMLElementEventMap\n : Record<string, Event>\n\nexport type EventFor<\n TTarget extends EventTarget,\n TType extends string,\n> = TType extends keyof EventMapFor<TTarget>\n ? EventMapFor<TTarget>[TType]\n : Event\n\n/** Bind a DOM event listener on mount, remove it on unmount, and rebind when a ref target changes. */\nexport function useEventListener<\n TTarget extends EventTarget = EventTarget,\n TType extends string = string,\n>(\n target: EventTargetLike<TTarget>,\n type: TType,\n listener: (event: EventFor<TTarget, TType>) => void,\n options?: UseEventListenerOptions,\n): EventListenerStop {\n let stopCurrent: EventListenerStop | undefined\n let mounted = false\n\n function stop(): void {\n stopCurrent?.()\n stopCurrent = undefined\n }\n\n function start(): void {\n stop()\n if (!mounted || !isEnabled(options)) return\n const resolvedTarget = resolveEventTarget(target)\n if (!resolvedTarget) return\n\n const eventListener = listener as EventListener\n const eventOptions = getEventOptions(options)\n resolvedTarget.addEventListener(type, eventListener, eventOptions)\n stopCurrent = () => {\n resolvedTarget.removeEventListener(type, eventListener, eventOptions)\n }\n }\n\n onMounted(() => {\n mounted = true\n start()\n })\n\n if (isRef(target)) {\n watch(target, start, { flush: 'post' })\n }\n const enabled = getEnabledOption(options)\n if (isRef(enabled)) {\n watch(enabled, start, { flush: 'post' })\n }\n\n onUnmounted(() => {\n mounted = false\n stop()\n })\n\n return stop\n}\n\nfunction resolveEventTarget(target: EventTargetLike): EventTarget | null | undefined {\n if (isRef(target)) return target.value\n if (typeof target === 'function') return target()\n return target\n}\n\nfunction getEnabledOption(options: UseEventListenerOptions | undefined): EventListenerEnabled | undefined {\n return typeof options === 'object' ? options.enabled : undefined\n}\n\nfunction isEnabled(options: UseEventListenerOptions | undefined): boolean {\n const enabled = getEnabledOption(options)\n if (isRef(enabled)) return enabled.value\n return enabled ?? true\n}\n\nfunction getEventOptions(options: UseEventListenerOptions | undefined): boolean | AddEventListenerOptions | undefined {\n if (typeof options !== 'object') return options\n const { enabled: _enabled, ...eventOptions } = options\n return eventOptions\n}\n"],"mappings":";;;AAsCA,SAAgB,iBAId,QACA,MACA,UACA,SACmB;CACnB,IAAI;CACJ,IAAI,UAAU;CAEd,SAAS,OAAa;AACpB,iBAAe;AACf,gBAAc,KAAA;;CAGhB,SAAS,QAAc;AACrB,QAAM;AACN,MAAI,CAAC,WAAW,CAAC,UAAU,QAAQ,CAAE;EACrC,MAAM,iBAAiB,mBAAmB,OAAO;AACjD,MAAI,CAAC,eAAgB;EAErB,MAAM,gBAAgB;EACtB,MAAM,eAAe,gBAAgB,QAAQ;AAC7C,iBAAe,iBAAiB,MAAM,eAAe,aAAa;AAClE,sBAAoB;AAClB,kBAAe,oBAAoB,MAAM,eAAe,aAAa;;;AAIzE,iBAAgB;AACd,YAAU;AACV,SAAO;GACP;AAEF,KAAI,MAAM,OAAO,CACf,OAAM,QAAQ,OAAO,EAAE,OAAO,QAAQ,CAAC;CAEzC,MAAM,UAAU,iBAAiB,QAAQ;AACzC,KAAI,MAAM,QAAQ,CAChB,OAAM,SAAS,OAAO,EAAE,OAAO,QAAQ,CAAC;AAG1C,mBAAkB;AAChB,YAAU;AACV,QAAM;GACN;AAEF,QAAO;;AAGT,SAAS,mBAAmB,QAAyD;AACnF,KAAI,MAAM,OAAO,CAAE,QAAO,OAAO;AACjC,KAAI,OAAO,WAAW,WAAY,QAAO,QAAQ;AACjD,QAAO;;AAGT,SAAS,iBAAiB,SAAgF;AACxG,QAAO,OAAO,YAAY,WAAW,QAAQ,UAAU,KAAA;;AAGzD,SAAS,UAAU,SAAuD;CACxE,MAAM,UAAU,iBAAiB,QAAQ;AACzC,KAAI,MAAM,QAAQ,CAAE,QAAO,QAAQ;AACnC,QAAO,WAAW;;AAGpB,SAAS,gBAAgB,SAA6F;AACpH,KAAI,OAAO,YAAY,SAAU,QAAO;CACxC,MAAM,EAAE,SAAS,UAAU,GAAG,iBAAiB;AAC/C,QAAO"}
@@ -0,0 +1,469 @@
1
+ import { r as useSettingsStore, t as useAuthStore } from "./auth-D9q2GIcv.js";
2
+ import { o as datePresetToISO } from "./experiment-utils-hGXMHlAc.js";
3
+ import { computed, onScopeDispose, reactive, ref, watch } from "vue";
4
+ import axios from "axios";
5
+ //#region src/composables/useApi.ts
6
+ var apiClientInstance = null;
7
+ var interceptorAttached = false;
8
+ function joinUrlPath(baseUrl, path) {
9
+ if (!path) return baseUrl;
10
+ if (path.startsWith("?") || path.startsWith("#")) return `${baseUrl.replace(/\/+$/, "")}${path}`;
11
+ const normalizedBase = baseUrl.replace(/\/+$/, "");
12
+ const normalizedPath = path.replace(/^\/+/, "/");
13
+ return `${normalizedBase}${normalizedPath.startsWith("/") ? normalizedPath : `/${normalizedPath}`}`;
14
+ }
15
+ function getBasePath(baseUrl) {
16
+ if (!baseUrl) return "/";
17
+ try {
18
+ const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost";
19
+ return new URL(baseUrl, origin).pathname.replace(/\/+$/, "") || "/";
20
+ } catch {
21
+ return baseUrl.replace(/^https?:\/\/[^/]+/i, "").replace(/\/+$/, "") || "/";
22
+ }
23
+ }
24
+ function normalizeRequestUrl(baseUrl, url) {
25
+ if (!url || /^https?:\/\//.test(url)) return url;
26
+ const basePath = getBasePath(baseUrl);
27
+ if (basePath === "/") return url;
28
+ const normalizedUrl = url.startsWith("/") ? url : `/${url}`;
29
+ if (normalizedUrl === basePath || normalizedUrl.startsWith(`${basePath}?`) || normalizedUrl.startsWith(`${basePath}#`)) return normalizedUrl.slice(basePath.length);
30
+ if (normalizedUrl.startsWith(`${basePath}/`)) return normalizedUrl.slice(basePath.length) || "";
31
+ return url;
32
+ }
33
+ function asMutableHeaders(headers) {
34
+ return headers ? headers : null;
35
+ }
36
+ function hasAuthorizationHeader(headers) {
37
+ const bag = asMutableHeaders(headers);
38
+ if (!bag) return false;
39
+ if (typeof bag.has === "function") return bag.has("Authorization");
40
+ return Object.keys(bag).some((key) => key.toLowerCase() === "authorization");
41
+ }
42
+ function setAuthorizationHeader(headers, value) {
43
+ const bag = asMutableHeaders(headers);
44
+ if (!bag) return;
45
+ if (typeof bag.set === "function") {
46
+ bag.set("Authorization", value);
47
+ return;
48
+ }
49
+ bag.Authorization = value;
50
+ }
51
+ function deleteAuthorizationHeader(headers) {
52
+ const bag = asMutableHeaders(headers);
53
+ if (!bag) return;
54
+ if (typeof bag.delete === "function") {
55
+ bag.delete("Authorization");
56
+ return;
57
+ }
58
+ for (const key of Object.keys(bag)) if (key.toLowerCase() === "authorization") delete bag[key];
59
+ }
60
+ function getApiClient() {
61
+ if (!apiClientInstance) apiClientInstance = axios.create({ headers: { "Content-Type": "application/json" } });
62
+ return apiClientInstance;
63
+ }
64
+ /** Axios-based API client that injects the plugin base URL and JWT auth header on every request. */
65
+ function useApi(options = {}) {
66
+ const settingsStore = useSettingsStore();
67
+ const authStore = useAuthStore();
68
+ const apiClient = getApiClient();
69
+ if (!authStore.isInitialized) authStore.initialize();
70
+ if (!interceptorAttached) {
71
+ apiClient.interceptors.request.use((config) => {
72
+ const request = config;
73
+ if (request._mintSkipAuth) {
74
+ delete request._mintSkipAuth;
75
+ deleteAuthorizationHeader(request.headers);
76
+ return request;
77
+ }
78
+ const currentAuthStore = useAuthStore();
79
+ if (currentAuthStore.token && config.headers && !hasAuthorizationHeader(config.headers)) setAuthorizationHeader(config.headers, `Bearer ${currentAuthStore.token}`);
80
+ return config;
81
+ });
82
+ interceptorAttached = true;
83
+ }
84
+ function getBaseUrl() {
85
+ return options.baseUrl ?? settingsStore.getApiBaseUrl();
86
+ }
87
+ function normalizeUrl(url) {
88
+ return normalizeRequestUrl(getBaseUrl(), url);
89
+ }
90
+ function requestConfig(config) {
91
+ const base = {
92
+ baseURL: getBaseUrl(),
93
+ timeout: options.timeout ?? settingsStore.requestTimeout,
94
+ ...config
95
+ };
96
+ if (options.withAuth === false) {
97
+ base._mintSkipAuth = true;
98
+ deleteAuthorizationHeader(base.headers);
99
+ }
100
+ return base;
101
+ }
102
+ async function get(url, config) {
103
+ return (await apiClient.get(normalizeUrl(url), requestConfig(config))).data;
104
+ }
105
+ async function post(url, data, config) {
106
+ return (await apiClient.post(normalizeUrl(url), data, requestConfig(config))).data;
107
+ }
108
+ async function put(url, data, config) {
109
+ return (await apiClient.put(normalizeUrl(url), data, requestConfig(config))).data;
110
+ }
111
+ async function patch(url, data, config) {
112
+ return (await apiClient.patch(normalizeUrl(url), data, requestConfig(config))).data;
113
+ }
114
+ async function del(url, config) {
115
+ return (await apiClient.delete(normalizeUrl(url), requestConfig(config))).data;
116
+ }
117
+ async function upload(url, file, fieldName = "file", additionalData) {
118
+ const formData = new FormData();
119
+ formData.append(fieldName, file);
120
+ if (additionalData) Object.entries(additionalData).forEach(([key, value]) => {
121
+ if (value !== void 0 && value !== null) formData.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
122
+ });
123
+ return (await apiClient.post(normalizeUrl(url), formData, requestConfig({ headers: { "Content-Type": void 0 } }))).data;
124
+ }
125
+ async function download(url, filename) {
126
+ const response = await apiClient.get(normalizeUrl(url), requestConfig({ responseType: "blob" }));
127
+ const blob = new Blob([response.data]);
128
+ const blobUrl = URL.createObjectURL(blob);
129
+ if (filename) {
130
+ const link = document.createElement("a");
131
+ link.href = blobUrl;
132
+ link.download = filename;
133
+ document.body.appendChild(link);
134
+ link.click();
135
+ document.body.removeChild(link);
136
+ setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
137
+ return "";
138
+ }
139
+ return blobUrl;
140
+ }
141
+ function buildUrl(path) {
142
+ const baseUrl = getBaseUrl();
143
+ return joinUrlPath(baseUrl, normalizeRequestUrl(baseUrl, path));
144
+ }
145
+ function buildWsUrl(path) {
146
+ return joinUrlPath(settingsStore.getWsBaseUrl(), path);
147
+ }
148
+ return {
149
+ client: apiClient,
150
+ get,
151
+ post,
152
+ put,
153
+ patch,
154
+ delete: del,
155
+ upload,
156
+ download,
157
+ buildUrl,
158
+ buildWsUrl
159
+ };
160
+ }
161
+ //#endregion
162
+ //#region src/composables/useRequestSyncState.ts
163
+ /** Shared loading/error/timestamp state for generated plugin request helpers. */
164
+ function useRequestSyncState(defaultErrorMessage = "Request failed.") {
165
+ const loading = ref(false);
166
+ const error = ref(null);
167
+ const lastLoadedAt = ref(null);
168
+ const lastSavedAt = ref(null);
169
+ const lastRunAt = ref(null);
170
+ function clearError() {
171
+ error.value = null;
172
+ }
173
+ function readErrorMessage(value, fallback = defaultErrorMessage) {
174
+ if (value instanceof Error && value.message) return value.message;
175
+ if (typeof value === "string" && value.trim()) return value;
176
+ if (typeof value === "object" && value !== null && "message" in value && typeof value.message === "string" && value.message) return value.message;
177
+ return fallback;
178
+ }
179
+ function setError(value, fallback) {
180
+ const message = readErrorMessage(value, fallback);
181
+ error.value = message;
182
+ return message;
183
+ }
184
+ function markLoaded(date = /* @__PURE__ */ new Date()) {
185
+ lastLoadedAt.value = date;
186
+ }
187
+ function markSaved(date = /* @__PURE__ */ new Date()) {
188
+ lastSavedAt.value = date;
189
+ }
190
+ function markRun(date = /* @__PURE__ */ new Date()) {
191
+ lastRunAt.value = date;
192
+ }
193
+ async function run(operation, options = {}) {
194
+ loading.value = true;
195
+ clearError();
196
+ try {
197
+ const response = await operation();
198
+ if (options.success === "load") markLoaded();
199
+ else if (options.success === "save") markSaved();
200
+ else if (options.success === "run") markRun();
201
+ return response;
202
+ } catch (err) {
203
+ setError(err, options.errorMessage);
204
+ throw err;
205
+ } finally {
206
+ loading.value = false;
207
+ }
208
+ }
209
+ return {
210
+ loading,
211
+ error,
212
+ lastLoadedAt,
213
+ lastSavedAt,
214
+ lastRunAt,
215
+ clearError,
216
+ readErrorMessage,
217
+ setError,
218
+ markLoaded,
219
+ markSaved,
220
+ markRun,
221
+ run
222
+ };
223
+ }
224
+ //#endregion
225
+ //#region src/composables/useDebouncedWatch.ts
226
+ /** Watch a Vue source with debounced callback execution and explicit cancel/flush controls. */
227
+ function useDebouncedWatch(source, callback, options = {}) {
228
+ const { delay = 300, ...watchOptions } = options;
229
+ const isPending = ref(false);
230
+ let timer = null;
231
+ let hasLatestValue = false;
232
+ let latestValue;
233
+ let latestOldValue;
234
+ let callbackCleanup = null;
235
+ let stopped = false;
236
+ function clearTimer() {
237
+ if (timer) {
238
+ clearTimeout(timer);
239
+ timer = null;
240
+ }
241
+ isPending.value = false;
242
+ }
243
+ function runCallbackCleanup() {
244
+ if (!callbackCleanup) return;
245
+ callbackCleanup();
246
+ callbackCleanup = null;
247
+ }
248
+ function cancel() {
249
+ clearTimer();
250
+ hasLatestValue = false;
251
+ latestOldValue = void 0;
252
+ }
253
+ function flush() {
254
+ if (!hasLatestValue) return;
255
+ const value = latestValue;
256
+ const oldValue = latestOldValue;
257
+ cancel();
258
+ runCallbackCleanup();
259
+ callback(value, oldValue, (cleanup) => {
260
+ callbackCleanup = cleanup;
261
+ });
262
+ }
263
+ function schedule(value, oldValue) {
264
+ cancel();
265
+ hasLatestValue = true;
266
+ latestValue = value;
267
+ latestOldValue = oldValue;
268
+ isPending.value = true;
269
+ timer = setTimeout(flush, delay);
270
+ }
271
+ const stopWatch = watch(source, (value, oldValue, onCleanup) => {
272
+ schedule(value, oldValue);
273
+ onCleanup(() => {
274
+ cancel();
275
+ runCallbackCleanup();
276
+ });
277
+ }, watchOptions);
278
+ function stop() {
279
+ if (stopped) return;
280
+ stopped = true;
281
+ stopWatch();
282
+ cancel();
283
+ runCallbackCleanup();
284
+ }
285
+ onScopeDispose(stop);
286
+ return {
287
+ isPending,
288
+ cancel,
289
+ flush,
290
+ stop
291
+ };
292
+ }
293
+ //#endregion
294
+ //#region src/composables/useExperimentSelector.ts
295
+ function getPlatformContext() {
296
+ if (typeof window === "undefined") return void 0;
297
+ return window.__MINT_PLATFORM__;
298
+ }
299
+ function getPlatformApiUrl() {
300
+ return getPlatformContext()?.platformApiUrl;
301
+ }
302
+ /** Fetches a paginated, filtered experiment list from the platform API for picker and selector UIs. */
303
+ function useExperimentSelector(options = {}) {
304
+ const { limit = 100, immediate = false, experimentType, apiBaseUrl } = options;
305
+ const platformBase = apiBaseUrl ?? getPlatformApiUrl();
306
+ const api = useApi();
307
+ const experiments = ref([]);
308
+ const total = ref(0);
309
+ const selectedExperiment = ref(null);
310
+ const request = useRequestSyncState("Failed to fetch experiments");
311
+ const isLoading = request.loading;
312
+ const error = request.error;
313
+ const lastLoadedAt = request.lastLoadedAt;
314
+ const page = ref(0);
315
+ const sortKey = ref("created_at:desc");
316
+ const experimentTypes = ref([]);
317
+ const projects = ref([]);
318
+ let filterOptionsFetched = false;
319
+ const hasMore = computed(() => experiments.value.length < total.value);
320
+ const filters = reactive({
321
+ search: void 0,
322
+ status: void 0,
323
+ project: void 0,
324
+ experimentType: void 0,
325
+ datePreset: void 0
326
+ });
327
+ function parseSortKey() {
328
+ const [field, order] = sortKey.value.split(":");
329
+ return {
330
+ sortBy: field || "created_at",
331
+ sortOrder: order || "desc"
332
+ };
333
+ }
334
+ async function fetchExperiments() {
335
+ try {
336
+ await request.run(async () => {
337
+ const params = new URLSearchParams();
338
+ const allowedTypes = getPlatformContext()?.allowedExperimentTypes;
339
+ const effectiveType = experimentType ?? (allowedTypes?.length === 1 ? allowedTypes[0] : void 0) ?? filters.experimentType ?? void 0;
340
+ if (effectiveType) params.set("experiment_type", effectiveType);
341
+ if (filters.status) params.set("status", filters.status);
342
+ if (filters.search) params.set("search", filters.search);
343
+ if (filters.project) params.set("project", filters.project);
344
+ const { sortBy, sortOrder } = parseSortKey();
345
+ params.set("sort_by", sortBy);
346
+ params.set("sort_order", sortOrder);
347
+ if (filters.datePreset) params.set("created_after", datePresetToISO(filters.datePreset));
348
+ params.set("limit", String(limit));
349
+ params.set("skip", String(page.value * limit));
350
+ const query = params.toString();
351
+ const url = `${platformBase ?? ""}/experiments${query ? `?${query}` : ""}`;
352
+ const data = await api.get(url);
353
+ let filtered = data.experiments;
354
+ if (!effectiveType && allowedTypes && allowedTypes.length > 1) {
355
+ const typeSet = new Set(allowedTypes);
356
+ filtered = filtered.filter((e) => typeSet.has(e.experiment_type));
357
+ }
358
+ if (page.value === 0) experiments.value = filtered;
359
+ else experiments.value = [...experiments.value, ...filtered];
360
+ if (!effectiveType && allowedTypes && allowedTypes.length > 1) if (data.experiments.length < limit) total.value = experiments.value.length;
361
+ else total.value = experiments.value.length + 1;
362
+ else total.value = data.total;
363
+ }, {
364
+ success: "load",
365
+ errorMessage: "Failed to fetch experiments"
366
+ });
367
+ } catch {
368
+ if (page.value === 0) {
369
+ experiments.value = [];
370
+ total.value = 0;
371
+ }
372
+ }
373
+ }
374
+ async function fetchFilterOptions() {
375
+ if (filterOptionsFetched) return;
376
+ filterOptionsFetched = true;
377
+ const base = platformBase ?? "";
378
+ const [typesRes, projectsRes] = await Promise.allSettled([api.get(`${base}/experiments/experiment-types`), api.get(`${base}/projects`)]);
379
+ if (typesRes.status === "fulfilled" && Array.isArray(typesRes.value)) experimentTypes.value = typesRes.value.map((t) => ({
380
+ value: t.value,
381
+ label: t.label,
382
+ color: t.color
383
+ }));
384
+ if (projectsRes.status === "fulfilled" && projectsRes.value?.projects && Array.isArray(projectsRes.value.projects)) projects.value = projectsRes.value.projects.map((p) => ({
385
+ value: p.name,
386
+ label: p.name
387
+ }));
388
+ }
389
+ async function loadMore() {
390
+ if (!hasMore.value || isLoading.value) return;
391
+ page.value++;
392
+ await fetchExperiments();
393
+ }
394
+ function reset() {
395
+ page.value = 0;
396
+ experiments.value = [];
397
+ total.value = 0;
398
+ fetchExperiments();
399
+ }
400
+ function select(experiment) {
401
+ selectedExperiment.value = experiment;
402
+ }
403
+ function clear() {
404
+ selectedExperiment.value = null;
405
+ filters.search = void 0;
406
+ filters.status = void 0;
407
+ filters.project = void 0;
408
+ filters.experimentType = void 0;
409
+ filters.datePreset = void 0;
410
+ sortKey.value = "created_at:desc";
411
+ page.value = 0;
412
+ request.clearError();
413
+ }
414
+ const groupedByProject = computed(() => {
415
+ const groups = /* @__PURE__ */ new Map();
416
+ for (const exp of experiments.value) {
417
+ const key = exp.project_name ?? exp.project ?? "No project";
418
+ const list = groups.get(key);
419
+ if (list) list.push(exp);
420
+ else groups.set(key, [exp]);
421
+ }
422
+ return [...groups.entries()].sort(([a], [b]) => {
423
+ if (a === "No project") return 1;
424
+ if (b === "No project") return -1;
425
+ return a.localeCompare(b);
426
+ });
427
+ });
428
+ const searchWatch = useDebouncedWatch(() => filters.search, () => {
429
+ page.value = 0;
430
+ fetchExperiments();
431
+ }, { delay: 300 });
432
+ watch(() => [
433
+ filters.status,
434
+ filters.project,
435
+ filters.experimentType,
436
+ filters.datePreset,
437
+ sortKey.value
438
+ ], () => {
439
+ searchWatch.cancel();
440
+ page.value = 0;
441
+ fetchExperiments();
442
+ });
443
+ if (immediate) fetchExperiments();
444
+ return {
445
+ experiments,
446
+ total,
447
+ selectedExperiment,
448
+ filters,
449
+ isLoading,
450
+ error,
451
+ lastLoadedAt,
452
+ page,
453
+ hasMore,
454
+ sortKey,
455
+ experimentTypes,
456
+ projects,
457
+ groupedByProject,
458
+ fetch: fetchExperiments,
459
+ loadMore,
460
+ reset,
461
+ select,
462
+ clear,
463
+ fetchFilterOptions
464
+ };
465
+ }
466
+ //#endregion
467
+ export { useApi as i, useDebouncedWatch as n, useRequestSyncState as r, useExperimentSelector as t };
468
+
469
+ //# sourceMappingURL=useExperimentSelector-BpZklTbV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useExperimentSelector-BpZklTbV.js","names":[],"sources":["../src/composables/useApi.ts","../src/composables/useRequestSyncState.ts","../src/composables/useDebouncedWatch.ts","../src/composables/useExperimentSelector.ts"],"sourcesContent":["import axios, { type AxiosInstance, type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios'\nimport { useSettingsStore } from '../stores/settings'\nimport { useAuthStore } from '../stores/auth'\n\nlet apiClientInstance: AxiosInstance | null = null\nlet interceptorAttached = false\n\ninterface MintAxiosRequestConfig extends AxiosRequestConfig {\n _mintSkipAuth?: boolean\n}\n\ninterface MintInternalAxiosRequestConfig extends InternalAxiosRequestConfig {\n _mintSkipAuth?: boolean\n}\n\ntype MutableHeaders = Record<string, unknown> & {\n has?: (header: string) => boolean\n set?: (header: string, value: string) => void\n delete?: (header: string) => void\n}\n\nfunction joinUrlPath(baseUrl: string, path: string): string {\n if (!path) return baseUrl\n if (path.startsWith('?') || path.startsWith('#')) return `${baseUrl.replace(/\\/+$/, '')}${path}`\n const normalizedBase = baseUrl.replace(/\\/+$/, '')\n const normalizedPath = path.replace(/^\\/+/, '/')\n return `${normalizedBase}${normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`}`\n}\n\nfunction getBasePath(baseUrl: string): string {\n if (!baseUrl) return '/'\n\n try {\n const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'\n return new URL(baseUrl, origin).pathname.replace(/\\/+$/, '') || '/'\n } catch {\n const path = baseUrl.replace(/^https?:\\/\\/[^/]+/i, '')\n return path.replace(/\\/+$/, '') || '/'\n }\n}\n\nfunction normalizeRequestUrl(baseUrl: string, url: string): string {\n if (!url || /^https?:\\/\\//.test(url)) return url\n\n const basePath = getBasePath(baseUrl)\n if (basePath === '/') return url\n\n const normalizedUrl = url.startsWith('/') ? url : `/${url}`\n if (\n normalizedUrl === basePath\n || normalizedUrl.startsWith(`${basePath}?`)\n || normalizedUrl.startsWith(`${basePath}#`)\n ) {\n return normalizedUrl.slice(basePath.length)\n }\n if (normalizedUrl.startsWith(`${basePath}/`)) {\n return normalizedUrl.slice(basePath.length) || ''\n }\n return url\n}\n\nfunction asMutableHeaders(headers: AxiosRequestConfig['headers']): MutableHeaders | null {\n return headers ? (headers as MutableHeaders) : null\n}\n\nfunction hasAuthorizationHeader(headers: AxiosRequestConfig['headers']): boolean {\n const bag = asMutableHeaders(headers)\n if (!bag) return false\n if (typeof bag.has === 'function') return bag.has('Authorization')\n return Object.keys(bag).some((key) => key.toLowerCase() === 'authorization')\n}\n\nfunction setAuthorizationHeader(headers: AxiosRequestConfig['headers'], value: string): void {\n const bag = asMutableHeaders(headers)\n if (!bag) return\n if (typeof bag.set === 'function') {\n bag.set('Authorization', value)\n return\n }\n bag.Authorization = value\n}\n\nfunction deleteAuthorizationHeader(headers: AxiosRequestConfig['headers']): void {\n const bag = asMutableHeaders(headers)\n if (!bag) return\n if (typeof bag.delete === 'function') {\n bag.delete('Authorization')\n return\n }\n for (const key of Object.keys(bag)) {\n if (key.toLowerCase() === 'authorization') delete bag[key]\n }\n}\n\nfunction getApiClient(): AxiosInstance {\n if (!apiClientInstance) {\n apiClientInstance = axios.create({\n headers: {\n 'Content-Type': 'application/json',\n },\n })\n }\n return apiClientInstance\n}\n\nexport interface ApiClientOptions {\n baseUrl?: string\n timeout?: number\n withAuth?: boolean\n}\n\nexport interface UseApiReturn {\n client: AxiosInstance\n get: <T>(url: string, config?: AxiosRequestConfig) => Promise<T>\n post: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) => Promise<T>\n put: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) => Promise<T>\n patch: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) => Promise<T>\n delete: <T>(url: string, config?: AxiosRequestConfig) => Promise<T>\n upload: <T>(url: string, file: File, fieldName?: string, additionalData?: Record<string, unknown>) => Promise<T>\n download: (url: string, filename?: string) => Promise<string>\n buildUrl: (path: string) => string\n buildWsUrl: (path: string) => string\n}\n\n/** Axios-based API client that injects the plugin base URL and JWT auth header on every request. */\nexport function useApi(options: ApiClientOptions = {}): UseApiReturn {\n const settingsStore = useSettingsStore()\n const authStore = useAuthStore()\n const apiClient = getApiClient()\n\n // Ensure auth store is initialized (reads token from localStorage)\n if (!authStore.isInitialized) {\n authStore.initialize()\n }\n\n // Attach auth interceptor only once (reads token dynamically, not from closure)\n if (!interceptorAttached) {\n apiClient.interceptors.request.use((config) => {\n const request = config as MintInternalAxiosRequestConfig\n if (request._mintSkipAuth) {\n delete request._mintSkipAuth\n deleteAuthorizationHeader(request.headers)\n return request\n }\n\n const currentAuthStore = useAuthStore()\n if (currentAuthStore.token && config.headers && !hasAuthorizationHeader(config.headers)) {\n setAuthorizationHeader(config.headers, `Bearer ${currentAuthStore.token}`)\n }\n return config\n })\n interceptorAttached = true\n }\n\n function getBaseUrl(): string {\n return options.baseUrl ?? settingsStore.getApiBaseUrl()\n }\n\n function normalizeUrl(url: string): string {\n return normalizeRequestUrl(getBaseUrl(), url)\n }\n\n // Build per-request config that applies this caller's options\n function requestConfig(config?: AxiosRequestConfig): MintAxiosRequestConfig {\n const base: MintAxiosRequestConfig = {\n baseURL: getBaseUrl(),\n timeout: options.timeout ?? settingsStore.requestTimeout,\n ...config,\n }\n // Strip auth header if explicitly disabled\n if (options.withAuth === false) {\n base._mintSkipAuth = true\n deleteAuthorizationHeader(base.headers)\n }\n return base\n }\n\n // Generic request methods\n async function get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.get<T>(normalizeUrl(url), requestConfig(config))\n return response.data\n }\n\n async function post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.post<T>(normalizeUrl(url), data, requestConfig(config))\n return response.data\n }\n\n async function put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.put<T>(normalizeUrl(url), data, requestConfig(config))\n return response.data\n }\n\n async function patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.patch<T>(normalizeUrl(url), data, requestConfig(config))\n return response.data\n }\n\n async function del<T>(url: string, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.delete<T>(normalizeUrl(url), requestConfig(config))\n return response.data\n }\n\n // File upload helper\n async function upload<T>(url: string, file: File, fieldName = 'file', additionalData?: Record<string, unknown>): Promise<T> {\n const formData = new FormData()\n formData.append(fieldName, file)\n\n if (additionalData) {\n Object.entries(additionalData).forEach(([key, value]) => {\n if (value !== undefined && value !== null) {\n formData.append(key, typeof value === 'object' ? JSON.stringify(value) : String(value))\n }\n })\n }\n\n const response = await apiClient.post<T>(normalizeUrl(url), formData, requestConfig({\n // Let Axios set Content-Type with the correct multipart boundary\n headers: { 'Content-Type': undefined },\n }))\n return response.data\n }\n\n // Download helper - returns blob URL\n async function download(url: string, filename?: string): Promise<string> {\n const response = await apiClient.get(normalizeUrl(url), requestConfig({ responseType: 'blob' }))\n const blob = new Blob([response.data])\n const blobUrl = URL.createObjectURL(blob)\n\n // Optionally trigger download\n if (filename) {\n const link = document.createElement('a')\n link.href = blobUrl\n link.download = filename\n document.body.appendChild(link)\n link.click()\n document.body.removeChild(link)\n // Revoke after a short delay to allow download to start;\n // return empty string since the URL will be invalidated\n setTimeout(() => URL.revokeObjectURL(blobUrl), 100)\n return ''\n }\n\n // Caller is responsible for revoking when no filename is provided\n return blobUrl\n }\n\n // Build full URL for external use (e.g., <a href=\"...\">)\n function buildUrl(path: string): string {\n const baseUrl = getBaseUrl()\n return joinUrlPath(baseUrl, normalizeRequestUrl(baseUrl, path))\n }\n\n // WebSocket URL builder\n function buildWsUrl(path: string): string {\n return joinUrlPath(settingsStore.getWsBaseUrl(), path)\n }\n\n return {\n client: apiClient,\n get,\n post,\n put,\n patch,\n delete: del,\n upload,\n download,\n buildUrl,\n buildWsUrl,\n }\n}\n","import { ref, type Ref } from 'vue'\n\nexport type RequestSyncSuccessKind = 'load' | 'save' | 'run'\n\nexport interface RequestSyncRunOptions {\n success?: RequestSyncSuccessKind\n errorMessage?: string\n}\n\nexport interface UseRequestSyncStateReturn {\n /** Whether a request is in progress. */\n loading: Ref<boolean>\n /** Message from the last failed request, or null. */\n error: Ref<string | null>\n /** Timestamp of the last successful load operation, or null. */\n lastLoadedAt: Ref<Date | null>\n /** Timestamp of the last successful save operation, or null. */\n lastSavedAt: Ref<Date | null>\n /** Timestamp of the last successful run operation, or null. */\n lastRunAt: Ref<Date | null>\n /** Clear the current error without changing loading or timestamps. */\n clearError: () => void\n /** Convert thrown values into a developer-facing error message. */\n readErrorMessage: (value: unknown, fallback?: string) => string\n /** Store and return a normalized error message. */\n setError: (value: unknown, fallback?: string) => string\n /** Mark the resource as successfully loaded. */\n markLoaded: (date?: Date) => void\n /** Mark the resource as successfully saved. */\n markSaved: (date?: Date) => void\n /** Mark the operation as successfully run. */\n markRun: (date?: Date) => void\n /** Run a request with shared loading/error handling and optional sync marking. */\n run: <T>(operation: () => Promise<T>, options?: RequestSyncRunOptions) => Promise<T>\n}\n\n/** Shared loading/error/timestamp state for generated plugin request helpers. */\nexport function useRequestSyncState(\n defaultErrorMessage = 'Request failed.',\n): UseRequestSyncStateReturn {\n const loading = ref(false)\n const error = ref<string | null>(null)\n const lastLoadedAt = ref<Date | null>(null)\n const lastSavedAt = ref<Date | null>(null)\n const lastRunAt = ref<Date | null>(null)\n\n function clearError(): void {\n error.value = null\n }\n\n function readErrorMessage(value: unknown, fallback = defaultErrorMessage): string {\n if (value instanceof Error && value.message) {\n return value.message\n }\n if (typeof value === 'string' && value.trim()) {\n return value\n }\n if (\n typeof value === 'object'\n && value !== null\n && 'message' in value\n && typeof (value as { message?: unknown }).message === 'string'\n && (value as { message: string }).message\n ) {\n return (value as { message: string }).message\n }\n return fallback\n }\n\n function setError(value: unknown, fallback?: string): string {\n const message = readErrorMessage(value, fallback)\n error.value = message\n return message\n }\n\n function markLoaded(date = new Date()): void {\n lastLoadedAt.value = date\n }\n\n function markSaved(date = new Date()): void {\n lastSavedAt.value = date\n }\n\n function markRun(date = new Date()): void {\n lastRunAt.value = date\n }\n\n async function run<T>(\n operation: () => Promise<T>,\n options: RequestSyncRunOptions = {},\n ): Promise<T> {\n loading.value = true\n clearError()\n try {\n const response = await operation()\n if (options.success === 'load') {\n markLoaded()\n } else if (options.success === 'save') {\n markSaved()\n } else if (options.success === 'run') {\n markRun()\n }\n return response\n } catch (err) {\n setError(err, options.errorMessage)\n throw err\n } finally {\n loading.value = false\n }\n }\n\n return {\n loading,\n error,\n lastLoadedAt,\n lastSavedAt,\n lastRunAt,\n clearError,\n readErrorMessage,\n setError,\n markLoaded,\n markSaved,\n markRun,\n run,\n }\n}\n","import {\n onScopeDispose,\n ref,\n watch,\n type Ref,\n type WatchOptions,\n type WatchSource,\n type WatchStopHandle,\n} from 'vue'\n\nexport type DebouncedWatchCleanup = () => void\nexport type DebouncedWatchSource<T> = WatchSource<T> | WatchSource<unknown>[] | (() => T)\nexport type DebouncedWatchCallback<T> = (\n value: T,\n oldValue: T | undefined,\n onCleanup: (cleanup: DebouncedWatchCleanup) => void,\n) => void | Promise<void>\n\nexport interface UseDebouncedWatchOptions extends WatchOptions {\n /** Debounce interval in milliseconds. */\n delay?: number\n}\n\nexport interface UseDebouncedWatchReturn {\n /** Whether a callback is currently scheduled. */\n isPending: Ref<boolean>\n /** Cancel the pending callback without stopping the watcher. */\n cancel: () => void\n /** Run the pending callback immediately, if one is scheduled. */\n flush: () => void\n /** Stop the watcher and clear pending callbacks. */\n stop: WatchStopHandle\n}\n\n/** Watch a Vue source with debounced callback execution and explicit cancel/flush controls. */\nexport function useDebouncedWatch<T>(\n source: DebouncedWatchSource<T>,\n callback: DebouncedWatchCallback<T>,\n options: UseDebouncedWatchOptions = {},\n): UseDebouncedWatchReturn {\n const { delay = 300, ...watchOptions } = options\n const isPending = ref(false)\n\n let timer: ReturnType<typeof setTimeout> | null = null\n let hasLatestValue = false\n let latestValue: T\n let latestOldValue: T | undefined\n let callbackCleanup: DebouncedWatchCleanup | null = null\n let stopped = false\n\n function clearTimer(): void {\n if (timer) {\n clearTimeout(timer)\n timer = null\n }\n isPending.value = false\n }\n\n function runCallbackCleanup(): void {\n if (!callbackCleanup) return\n callbackCleanup()\n callbackCleanup = null\n }\n\n function cancel(): void {\n clearTimer()\n hasLatestValue = false\n latestOldValue = undefined\n }\n\n function flush(): void {\n if (!hasLatestValue) return\n const value = latestValue\n const oldValue = latestOldValue\n cancel()\n runCallbackCleanup()\n void callback(value, oldValue, (cleanup) => {\n callbackCleanup = cleanup\n })\n }\n\n function schedule(value: T, oldValue: T | undefined): void {\n cancel()\n hasLatestValue = true\n latestValue = value\n latestOldValue = oldValue\n isPending.value = true\n timer = setTimeout(flush, delay)\n }\n\n const stopWatch = watch(\n source as WatchSource<unknown> | WatchSource<unknown>[],\n (value, oldValue, onCleanup) => {\n schedule(value as T, oldValue as T | undefined)\n onCleanup(() => {\n cancel()\n runCallbackCleanup()\n })\n },\n watchOptions,\n )\n\n function stop(): void {\n if (stopped) return\n stopped = true\n stopWatch()\n cancel()\n runCallbackCleanup()\n }\n\n onScopeDispose(stop)\n\n return {\n isPending,\n cancel,\n flush,\n stop,\n }\n}\n","import { ref, reactive, computed, watch, type Ref, type ComputedRef } from 'vue'\nimport { useApi } from './useApi'\nimport { useDebouncedWatch } from './useDebouncedWatch'\nimport { useRequestSyncState } from './useRequestSyncState'\nimport { datePresetToISO } from './experiment-utils'\nimport type {\n ExperimentSummary,\n ExperimentListResponse,\n ExperimentFilters,\n ExperimentTypeOption,\n ExperimentSortField,\n PlatformContext,\n SelectOption,\n} from '../types'\n\nfunction getPlatformContext(): PlatformContext | undefined {\n if (typeof window === 'undefined') return undefined\n return (window as unknown as { __MINT_PLATFORM__?: PlatformContext }).__MINT_PLATFORM__\n}\n\nfunction getPlatformApiUrl(): string | undefined {\n return getPlatformContext()?.platformApiUrl\n}\n\nexport interface UseExperimentSelectorOptions {\n experimentType?: string\n apiBaseUrl?: string\n limit?: number\n immediate?: boolean\n}\n\nexport interface UseExperimentSelectorReturn {\n /** Current page of fetched experiment rows. */\n experiments: Ref<ExperimentSummary[]>\n /** Total matching experiment count reported by the platform. */\n total: Ref<number>\n /** Currently selected experiment, or null. */\n selectedExperiment: Ref<ExperimentSummary | null>\n /** Mutable filters used for the next fetch. */\n filters: ExperimentFilters\n /** Whether experiment rows are currently loading. */\n isLoading: Ref<boolean>\n /** Error message from the last failed fetch, or null. */\n error: Ref<string | null>\n /** Timestamp of the last successful experiment fetch, or null. */\n lastLoadedAt: Ref<Date | null>\n /** Current zero-based page index. */\n page: Ref<number>\n /** Whether another page can be loaded. */\n hasMore: ComputedRef<boolean>\n // Sort\n /** Combined sort key in the form `<field>:<asc|desc>`. */\n sortKey: Ref<string>\n // Filter options\n /** Available experiment type options for filter UI. */\n experimentTypes: Ref<ExperimentTypeOption[]>\n /** Available project options for filter UI. */\n projects: Ref<SelectOption<string>[]>\n // Grouped view\n /** Fetched experiments grouped by project name. */\n groupedByProject: ComputedRef<[string, ExperimentSummary[]][]>\n // Methods\n /** Fetch the current page with the current filters. */\n fetch: () => Promise<void>\n /** Fetch and append the next page when available. */\n loadMore: () => Promise<void>\n /** Clear fetched rows and refetch page zero. */\n reset: () => void\n /** Select an experiment. */\n select: (experiment: ExperimentSummary) => void\n /** Clear selection and filters. */\n clear: () => void\n /** Fetch experiment type and project filter options. */\n fetchFilterOptions: () => Promise<void>\n}\n\n/** Fetches a paginated, filtered experiment list from the platform API for picker and selector UIs. */\nexport function useExperimentSelector(\n options: UseExperimentSelectorOptions = {},\n): UseExperimentSelectorReturn {\n const { limit = 100, immediate = false, experimentType, apiBaseUrl } = options\n const platformBase = apiBaseUrl ?? getPlatformApiUrl()\n const api = useApi()\n\n const experiments = ref<ExperimentSummary[]>([])\n const total = ref(0)\n const selectedExperiment = ref<ExperimentSummary | null>(null)\n const request = useRequestSyncState('Failed to fetch experiments')\n const isLoading = request.loading\n const error = request.error\n const lastLoadedAt = request.lastLoadedAt\n const page = ref(0)\n\n // Sort: combined key like \"created_at:desc\"\n const sortKey = ref<string>('created_at:desc')\n\n // Filter option data (fetched once, cached)\n const experimentTypes = ref<ExperimentTypeOption[]>([])\n const projects = ref<SelectOption<string>[]>([])\n let filterOptionsFetched = false\n\n const hasMore = computed(() => experiments.value.length < total.value)\n\n const filters: ExperimentFilters = reactive({\n search: undefined,\n status: undefined,\n project: undefined,\n experimentType: undefined,\n datePreset: undefined,\n })\n\n function parseSortKey(): { sortBy: ExperimentSortField; sortOrder: 'asc' | 'desc' } {\n const [field, order] = sortKey.value.split(':')\n return {\n sortBy: (field || 'created_at') as ExperimentSortField,\n sortOrder: (order || 'desc') as 'asc' | 'desc',\n }\n }\n\n async function fetchExperiments(): Promise<void> {\n try {\n await request.run(async () => {\n const params = new URLSearchParams()\n // Priority: explicit option > platform context (single type) > filter dropdown > no filter\n const allowedTypes = getPlatformContext()?.allowedExperimentTypes\n const effectiveType = experimentType\n ?? (allowedTypes?.length === 1 ? allowedTypes[0] : undefined)\n ?? filters.experimentType\n ?? undefined\n if (effectiveType) params.set('experiment_type', effectiveType)\n if (filters.status) params.set('status', filters.status)\n if (filters.search) params.set('search', filters.search)\n if (filters.project) params.set('project', filters.project)\n\n // Sort params\n const { sortBy, sortOrder } = parseSortKey()\n params.set('sort_by', sortBy)\n params.set('sort_order', sortOrder)\n\n // Date preset → created_after\n if (filters.datePreset) {\n params.set('created_after', datePresetToISO(filters.datePreset))\n }\n\n params.set('limit', String(limit))\n params.set('skip', String(page.value * limit))\n\n const query = params.toString()\n const base = platformBase ?? ''\n const url = `${base}/experiments${query ? `?${query}` : ''}`\n const data = await api.get<ExperimentListResponse>(url)\n\n // Client-side filter for multiple allowed types (backend only supports single type)\n let filtered = data.experiments\n if (!effectiveType && allowedTypes && allowedTypes.length > 1) {\n const typeSet = new Set(allowedTypes)\n filtered = filtered.filter(e => typeSet.has(e.experiment_type))\n }\n\n if (page.value === 0) {\n experiments.value = filtered\n } else {\n experiments.value = [...experiments.value, ...filtered]\n }\n // When client-side filtering is active (multiple allowedTypes), we can't\n // use data.total since it counts all types. Check if server has more pages.\n if (!effectiveType && allowedTypes && allowedTypes.length > 1) {\n if (data.experiments.length < limit) {\n // Server returned less than a full page — no more data\n total.value = experiments.value.length\n } else {\n // Might be more pages on the server\n total.value = experiments.value.length + 1\n }\n } else {\n total.value = data.total\n }\n }, { success: 'load', errorMessage: 'Failed to fetch experiments' })\n } catch {\n if (page.value === 0) {\n experiments.value = []\n total.value = 0\n }\n }\n }\n\n async function fetchFilterOptions(): Promise<void> {\n if (filterOptionsFetched) return\n filterOptionsFetched = true\n\n const base = platformBase ?? ''\n const [typesRes, projectsRes] = await Promise.allSettled([\n api.get<Array<{ value: string; label: string; color?: string }>>(`${base}/experiments/experiment-types`),\n api.get<{ projects: Array<{ id: number; name: string }>; total: number }>(`${base}/projects`),\n ])\n\n if (typesRes.status === 'fulfilled' && Array.isArray(typesRes.value)) {\n experimentTypes.value = typesRes.value.map(t => ({\n value: t.value,\n label: t.label,\n color: t.color,\n }))\n }\n\n if (projectsRes.status === 'fulfilled' && projectsRes.value?.projects && Array.isArray(projectsRes.value.projects)) {\n projects.value = projectsRes.value.projects.map(p => ({\n value: p.name,\n label: p.name,\n }))\n }\n }\n\n async function loadMore(): Promise<void> {\n if (!hasMore.value || isLoading.value) return\n page.value++\n await fetchExperiments()\n }\n\n function reset(): void {\n page.value = 0\n experiments.value = []\n total.value = 0\n fetchExperiments()\n }\n\n function select(experiment: ExperimentSummary): void {\n selectedExperiment.value = experiment\n }\n\n function clear(): void {\n selectedExperiment.value = null\n filters.search = undefined\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.datePreset = undefined\n sortKey.value = 'created_at:desc'\n page.value = 0\n request.clearError()\n }\n\n // Group experiments by project (client-side)\n const groupedByProject = computed<[string, ExperimentSummary[]][]>(() => {\n const groups = new Map<string, ExperimentSummary[]>()\n for (const exp of experiments.value) {\n const key = exp.project_name ?? exp.project ?? 'No project'\n const list = groups.get(key)\n if (list) {\n list.push(exp)\n } else {\n groups.set(key, [exp])\n }\n }\n // Sort alphabetically, \"No project\" last\n return [...groups.entries()].sort(([a], [b]) => {\n if (a === 'No project') return 1\n if (b === 'No project') return -1\n return a.localeCompare(b)\n })\n })\n\n const searchWatch = useDebouncedWatch(\n () => filters.search,\n () => {\n page.value = 0\n fetchExperiments()\n },\n { delay: 300 },\n )\n\n // Immediate watch on discrete filters and sort (cancel any pending search debounce)\n watch(\n () => [filters.status, filters.project, filters.experimentType, filters.datePreset, sortKey.value],\n () => {\n searchWatch.cancel()\n page.value = 0\n fetchExperiments()\n },\n )\n\n if (immediate) {\n fetchExperiments()\n }\n\n return {\n experiments,\n total,\n selectedExperiment,\n filters,\n isLoading,\n error,\n lastLoadedAt,\n page,\n hasMore,\n sortKey,\n experimentTypes,\n projects,\n groupedByProject,\n fetch: fetchExperiments,\n loadMore,\n reset,\n select,\n clear,\n fetchFilterOptions,\n }\n}\n"],"mappings":"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"}