@morscherlab/mld-sdk 0.9.1 → 0.9.3
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/dist/composables/useApi.js +3 -0
- package/dist/composables/useApi.js.map +1 -1
- package/dist/composables/useExperimentSelector.js +11 -8
- package/dist/composables/useExperimentSelector.js.map +1 -1
- package/dist/composables/usePluginConfig.js +1 -1
- package/dist/composables/usePluginConfig.js.map +1 -1
- package/package.json +1 -1
- package/src/composables/useApi.ts +5 -0
- package/src/composables/useExperimentSelector.ts +12 -7
- package/src/composables/usePluginConfig.ts +1 -1
|
@@ -17,6 +17,9 @@ function useApi(options = {}) {
|
|
|
17
17
|
const settingsStore = useSettingsStore();
|
|
18
18
|
const authStore = useAuthStore();
|
|
19
19
|
const apiClient = getApiClient();
|
|
20
|
+
if (!authStore.isInitialized) {
|
|
21
|
+
authStore.initialize();
|
|
22
|
+
}
|
|
20
23
|
if (!interceptorAttached) {
|
|
21
24
|
apiClient.interceptors.request.use((config) => {
|
|
22
25
|
config.baseURL = options.baseUrl ?? settingsStore.getApiBaseUrl();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useApi.js","sources":["../../src/composables/useApi.ts"],"sourcesContent":["import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'\nimport { useSettingsStore } from '../stores/settings'\nimport { useAuthStore } from '../stores/auth'\n\nlet apiClientInstance: AxiosInstance | null = null\nlet interceptorAttached = false\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\nexport function useApi(options: ApiClientOptions = {}): UseApiReturn {\n const settingsStore = useSettingsStore()\n const authStore = useAuthStore()\n const apiClient = getApiClient()\n\n // Attach interceptor only once\n if (!interceptorAttached) {\n apiClient.interceptors.request.use((config) => {\n // Set base URL from settings or options\n config.baseURL = options.baseUrl ?? settingsStore.getApiBaseUrl()\n config.timeout = options.timeout ?? settingsStore.requestTimeout\n\n // Add Authorization header if token exists and auth is not explicitly disabled\n if (options.withAuth !== false && authStore.token) {\n config.headers.Authorization = `Bearer ${authStore.token}`\n }\n\n return config\n })\n interceptorAttached = true\n }\n\n // Generic request methods\n async function get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.get<T>(url, 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>(url, data, 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>(url, data, 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>(url, data, 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>(url, 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>(url, formData, {\n headers: { 'Content-Type': 'multipart/form-data' },\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(url, { 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 link.click()\n }\n\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 = options.baseUrl ?? settingsStore.getApiBaseUrl()\n return `${baseUrl}${path}`\n }\n\n // WebSocket URL builder\n function buildWsUrl(path: string): string {\n return `${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"],"names":[],"mappings":";;;AAIA,IAAI,oBAA0C;AAC9C,IAAI,sBAAsB;AAE1B,SAAS,eAA8B;AACrC,MAAI,CAAC,mBAAmB;AACtB,wBAAoB,MAAM,OAAO;AAAA,MAC/B,SAAS;AAAA,QACP,gBAAgB;AAAA,MAAA;AAAA,IAClB,CACD;AAAA,EACH;AACA,SAAO;AACT;AAqBO,SAAS,OAAO,UAA4B,IAAkB;AACnE,QAAM,gBAAgB,iBAAA;AACtB,QAAM,YAAY,aAAA;AAClB,QAAM,YAAY,aAAA;AAGlB,MAAI,CAAC,qBAAqB;AACxB,cAAU,aAAa,QAAQ,IAAI,CAAC,WAAW;AAE7C,aAAO,UAAU,QAAQ,WAAW,cAAc,cAAA;AAClD,aAAO,UAAU,QAAQ,WAAW,cAAc;AAGlD,UAAI,QAAQ,aAAa,SAAS,UAAU,OAAO;AACjD,eAAO,QAAQ,gBAAgB,UAAU,UAAU,KAAK;AAAA,MAC1D;AAEA,aAAO;AAAA,IACT,CAAC;AACD,0BAAsB;AAAA,EACxB;AAGA,iBAAe,IAAO,KAAa,QAAyC;AAC1E,UAAM,WAAW,MAAM,UAAU,IAAO,KAAK,MAAM;AACnD,WAAO,SAAS;AAAA,EAClB;AAEA,iBAAe,KAAQ,KAAa,MAAgB,QAAyC;AAC3F,UAAM,WAAW,MAAM,UAAU,KAAQ,KAAK,MAAM,MAAM;AAC1D,WAAO,SAAS;AAAA,EAClB;AAEA,iBAAe,IAAO,KAAa,MAAgB,QAAyC;AAC1F,UAAM,WAAW,MAAM,UAAU,IAAO,KAAK,MAAM,MAAM;AACzD,WAAO,SAAS;AAAA,EAClB;AAEA,iBAAe,MAAS,KAAa,MAAgB,QAAyC;AAC5F,UAAM,WAAW,MAAM,UAAU,MAAS,KAAK,MAAM,MAAM;AAC3D,WAAO,SAAS;AAAA,EAClB;AAEA,iBAAe,IAAO,KAAa,QAAyC;AAC1E,UAAM,WAAW,MAAM,UAAU,OAAU,KAAK,MAAM;AACtD,WAAO,SAAS;AAAA,EAClB;AAGA,iBAAe,OAAU,KAAa,MAAY,YAAY,QAAQ,gBAAsD;AAC1H,UAAM,WAAW,IAAI,SAAA;AACrB,aAAS,OAAO,WAAW,IAAI;AAE/B,QAAI,gBAAgB;AAClB,aAAO,QAAQ,cAAc,EAAE,QAAQ,CAAC,CAAC,KAAK,KAAK,MAAM;AACvD,YAAI,UAAU,UAAa,UAAU,MAAM;AACzC,mBAAS,OAAO,KAAK,OAAO,UAAU,WAAW,KAAK,UAAU,KAAK,IAAI,OAAO,KAAK,CAAC;AAAA,QACxF;AAAA,MACF,CAAC;AAAA,IACH;AAEA,UAAM,WAAW,MAAM,UAAU,KAAQ,KAAK,UAAU;AAAA,MACtD,SAAS,EAAE,gBAAgB,sBAAA;AAAA,IAAsB,CAClD;AACD,WAAO,SAAS;AAAA,EAClB;AAGA,iBAAe,SAAS,KAAa,UAAoC;AACvE,UAAM,WAAW,MAAM,UAAU,IAAI,KAAK,EAAE,cAAc,QAAQ;AAClE,UAAM,OAAO,IAAI,KAAK,CAAC,SAAS,IAAI,CAAC;AACrC,UAAM,UAAU,IAAI,gBAAgB,IAAI;AAGxC,QAAI,UAAU;AACZ,YAAM,OAAO,SAAS,cAAc,GAAG;AACvC,WAAK,OAAO;AACZ,WAAK,WAAW;AAChB,WAAK,MAAA;AAAA,IACP;AAEA,WAAO;AAAA,EACT;AAGA,WAAS,SAAS,MAAsB;AACtC,UAAM,UAAU,QAAQ,WAAW,cAAc,cAAA;AACjD,WAAO,GAAG,OAAO,GAAG,IAAI;AAAA,EAC1B;AAGA,WAAS,WAAW,MAAsB;AACxC,WAAO,GAAG,cAAc,aAAA,CAAc,GAAG,IAAI;AAAA,EAC/C;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"useApi.js","sources":["../../src/composables/useApi.ts"],"sourcesContent":["import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'\nimport { useSettingsStore } from '../stores/settings'\nimport { useAuthStore } from '../stores/auth'\n\nlet apiClientInstance: AxiosInstance | null = null\nlet interceptorAttached = false\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\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 interceptor only once\n if (!interceptorAttached) {\n apiClient.interceptors.request.use((config) => {\n // Set base URL from settings or options\n config.baseURL = options.baseUrl ?? settingsStore.getApiBaseUrl()\n config.timeout = options.timeout ?? settingsStore.requestTimeout\n\n // Add Authorization header if token exists and auth is not explicitly disabled\n if (options.withAuth !== false && authStore.token) {\n config.headers.Authorization = `Bearer ${authStore.token}`\n }\n\n return config\n })\n interceptorAttached = true\n }\n\n // Generic request methods\n async function get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.get<T>(url, 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>(url, data, 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>(url, data, 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>(url, data, 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>(url, 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>(url, formData, {\n headers: { 'Content-Type': 'multipart/form-data' },\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(url, { 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 link.click()\n }\n\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 = options.baseUrl ?? settingsStore.getApiBaseUrl()\n return `${baseUrl}${path}`\n }\n\n // WebSocket URL builder\n function buildWsUrl(path: string): string {\n return `${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"],"names":[],"mappings":"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"}
|
|
@@ -20,15 +20,13 @@ function useExperimentSelector(options = {}) {
|
|
|
20
20
|
const page = ref(0);
|
|
21
21
|
const hasMore = computed(() => experiments.value.length < total.value);
|
|
22
22
|
async function fetchExperiments() {
|
|
23
|
+
var _a;
|
|
23
24
|
isLoading.value = true;
|
|
24
25
|
error.value = null;
|
|
25
26
|
try {
|
|
26
27
|
const params = new URLSearchParams();
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const types = (_a = getPlatformContext()) == null ? void 0 : _a.allowedExperimentTypes;
|
|
30
|
-
return (types == null ? void 0 : types.length) === 1 ? types[0] : void 0;
|
|
31
|
-
})();
|
|
28
|
+
const allowedTypes = (_a = getPlatformContext()) == null ? void 0 : _a.allowedExperimentTypes;
|
|
29
|
+
const effectiveType = experimentType ?? ((allowedTypes == null ? void 0 : allowedTypes.length) === 1 ? allowedTypes[0] : void 0);
|
|
32
30
|
if (effectiveType) params.set("experiment_type", effectiveType);
|
|
33
31
|
if (filters.status) params.set("status", filters.status);
|
|
34
32
|
if (filters.search) params.set("search", filters.search);
|
|
@@ -39,12 +37,17 @@ function useExperimentSelector(options = {}) {
|
|
|
39
37
|
const base = platformBase ?? "/api";
|
|
40
38
|
const url = `${base}/experiments${query ? `?${query}` : ""}`;
|
|
41
39
|
const data = await api.get(url);
|
|
40
|
+
let filtered = data.experiments;
|
|
41
|
+
if (!effectiveType && allowedTypes && allowedTypes.length > 1) {
|
|
42
|
+
const typeSet = new Set(allowedTypes);
|
|
43
|
+
filtered = filtered.filter((e) => typeSet.has(e.experiment_type));
|
|
44
|
+
}
|
|
42
45
|
if (page.value === 0) {
|
|
43
|
-
experiments.value =
|
|
46
|
+
experiments.value = filtered;
|
|
44
47
|
} else {
|
|
45
|
-
experiments.value = [...experiments.value, ...
|
|
48
|
+
experiments.value = [...experiments.value, ...filtered];
|
|
46
49
|
}
|
|
47
|
-
total.value = data.total;
|
|
50
|
+
total.value = effectiveType || !(allowedTypes == null ? void 0 : allowedTypes.length) ? data.total : filtered.length;
|
|
48
51
|
} catch (e) {
|
|
49
52
|
error.value = e instanceof Error ? e.message : "Failed to fetch experiments";
|
|
50
53
|
if (page.value === 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useExperimentSelector.js","sources":["../../src/composables/useExperimentSelector.ts"],"sourcesContent":["import { ref, reactive, computed, watch, onScopeDispose, type Ref, type ComputedRef } from 'vue'\nimport { useApi } from './useApi'\nimport type { ExperimentSummary, ExperimentListResponse, ExperimentFilters, PlatformContext } from '../types'\n\nfunction getPlatformContext(): PlatformContext | undefined {\n if (typeof window === 'undefined') return undefined\n return (window as unknown as { __MLD_PLATFORM__?: PlatformContext }).__MLD_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 experiments: Ref<ExperimentSummary[]>\n total: Ref<number>\n selectedExperiment: Ref<ExperimentSummary | null>\n filters: ExperimentFilters\n isLoading: Ref<boolean>\n error: Ref<string | null>\n page: Ref<number>\n hasMore: ComputedRef<boolean>\n fetch: () => Promise<void>\n loadMore: () => Promise<void>\n reset: () => void\n select: (experiment: ExperimentSummary) => void\n clear: () => void\n}\n\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 isLoading = ref(false)\n const error = ref<string | null>(null)\n const page = ref(0)\n\n const hasMore = computed(() => experiments.value.length < total.value)\n\n async function fetchExperiments(): Promise<void> {\n isLoading.value = true\n error.value = null\n try {\n const params = new URLSearchParams()\n // Priority: explicit option > platform context (single type) > no filter\n const
|
|
1
|
+
{"version":3,"file":"useExperimentSelector.js","sources":["../../src/composables/useExperimentSelector.ts"],"sourcesContent":["import { ref, reactive, computed, watch, onScopeDispose, type Ref, type ComputedRef } from 'vue'\nimport { useApi } from './useApi'\nimport type { ExperimentSummary, ExperimentListResponse, ExperimentFilters, PlatformContext } from '../types'\n\nfunction getPlatformContext(): PlatformContext | undefined {\n if (typeof window === 'undefined') return undefined\n return (window as unknown as { __MLD_PLATFORM__?: PlatformContext }).__MLD_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 experiments: Ref<ExperimentSummary[]>\n total: Ref<number>\n selectedExperiment: Ref<ExperimentSummary | null>\n filters: ExperimentFilters\n isLoading: Ref<boolean>\n error: Ref<string | null>\n page: Ref<number>\n hasMore: ComputedRef<boolean>\n fetch: () => Promise<void>\n loadMore: () => Promise<void>\n reset: () => void\n select: (experiment: ExperimentSummary) => void\n clear: () => void\n}\n\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 isLoading = ref(false)\n const error = ref<string | null>(null)\n const page = ref(0)\n\n const hasMore = computed(() => experiments.value.length < total.value)\n\n async function fetchExperiments(): Promise<void> {\n isLoading.value = true\n error.value = null\n try {\n const params = new URLSearchParams()\n // Priority: explicit option > platform context (single type) > no filter\n const allowedTypes = getPlatformContext()?.allowedExperimentTypes\n const effectiveType = experimentType\n ?? (allowedTypes?.length === 1 ? allowedTypes[0] : 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 params.set('limit', String(limit))\n params.set('skip', String(page.value * limit))\n\n const query = params.toString()\n // Use absolute platform URL in integrated mode to bypass plugin's baseURL\n const base = platformBase ?? '/api'\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 total.value = effectiveType || !allowedTypes?.length ? data.total : filtered.length\n } catch (e) {\n error.value = e instanceof Error ? e.message : 'Failed to fetch experiments'\n if (page.value === 0) {\n experiments.value = []\n total.value = 0\n }\n } finally {\n isLoading.value = false\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 page.value = 0\n }\n\n const filters: ExperimentFilters = reactive({\n search: undefined,\n status: undefined,\n project: undefined,\n })\n\n // Debounced watch on search filter\n let debounceTimer: ReturnType<typeof setTimeout> | null = null\n watch(\n () => filters.search,\n () => {\n if (debounceTimer) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n page.value = 0\n fetchExperiments()\n }, 300)\n },\n )\n\n // Immediate watch on status/project filters (no debounce needed)\n watch(\n () => [filters.status, filters.project],\n () => {\n page.value = 0\n fetchExperiments()\n },\n )\n\n onScopeDispose(() => {\n if (debounceTimer) clearTimeout(debounceTimer)\n })\n\n if (immediate) {\n fetchExperiments()\n }\n\n return {\n experiments,\n total,\n selectedExperiment,\n filters,\n isLoading,\n error,\n page,\n hasMore,\n fetch: fetchExperiments,\n loadMore,\n reset,\n select,\n clear,\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePluginConfig.js","sources":["../../src/composables/usePluginConfig.ts"],"sourcesContent":["import { ref, computed, onMounted, type Ref, type ComputedRef } from 'vue'\nimport { useApi } from './useApi'\nimport { usePlatformContext } from './usePlatformContext'\n\nexport interface UsePluginConfigReturn {\n config: Ref<Record<string, unknown>>\n isLoading: Ref<boolean>\n isSaving: Ref<boolean>\n error: Ref<string | null>\n isDirty: ComputedRef<boolean>\n load: () => Promise<void>\n save: () => Promise<boolean>\n reset: () => void\n}\n\nexport function usePluginConfig(pluginName?: string): UsePluginConfigReturn {\n const api = useApi()\n const { plugin } = usePlatformContext()\n\n const resolvedName = computed(() => pluginName ?? plugin.value?.name ?? '')\n\n const config = ref<Record<string, unknown>>({})\n const savedConfig = ref<Record<string, unknown>>({})\n const isLoading = ref(false)\n const isSaving = ref(false)\n const error = ref<string | null>(null)\n\n const isDirty = computed(() => {\n return JSON.stringify(config.value) !== JSON.stringify(savedConfig.value)\n })\n\n async function load(): Promise<void> {\n const name = resolvedName.value\n if (!name) return\n\n isLoading.value = true\n error.value = null\n try {\n const response = await api.get<{ plugin_name: string; config: Record<string, unknown> }>(\n `/api/plugins/${encodeURIComponent(name)}/config`,\n )\n config.value = { ...response.config }\n savedConfig.value = { ...response.config }\n } catch (e) {\n error.value = e instanceof Error ? e.message : 'Failed to load plugin config'\n } finally {\n isLoading.value = false\n }\n }\n\n async function save(): Promise<boolean> {\n const name = resolvedName.value\n if (!name) return false\n\n isSaving.value = true\n error.value = null\n try {\n const response = await api.
|
|
1
|
+
{"version":3,"file":"usePluginConfig.js","sources":["../../src/composables/usePluginConfig.ts"],"sourcesContent":["import { ref, computed, onMounted, type Ref, type ComputedRef } from 'vue'\nimport { useApi } from './useApi'\nimport { usePlatformContext } from './usePlatformContext'\n\nexport interface UsePluginConfigReturn {\n config: Ref<Record<string, unknown>>\n isLoading: Ref<boolean>\n isSaving: Ref<boolean>\n error: Ref<string | null>\n isDirty: ComputedRef<boolean>\n load: () => Promise<void>\n save: () => Promise<boolean>\n reset: () => void\n}\n\nexport function usePluginConfig(pluginName?: string): UsePluginConfigReturn {\n const api = useApi()\n const { plugin } = usePlatformContext()\n\n const resolvedName = computed(() => pluginName ?? plugin.value?.name ?? '')\n\n const config = ref<Record<string, unknown>>({})\n const savedConfig = ref<Record<string, unknown>>({})\n const isLoading = ref(false)\n const isSaving = ref(false)\n const error = ref<string | null>(null)\n\n const isDirty = computed(() => {\n return JSON.stringify(config.value) !== JSON.stringify(savedConfig.value)\n })\n\n async function load(): Promise<void> {\n const name = resolvedName.value\n if (!name) return\n\n isLoading.value = true\n error.value = null\n try {\n const response = await api.get<{ plugin_name: string; config: Record<string, unknown> }>(\n `/api/plugins/${encodeURIComponent(name)}/config`,\n )\n config.value = { ...response.config }\n savedConfig.value = { ...response.config }\n } catch (e) {\n error.value = e instanceof Error ? e.message : 'Failed to load plugin config'\n } finally {\n isLoading.value = false\n }\n }\n\n async function save(): Promise<boolean> {\n const name = resolvedName.value\n if (!name) return false\n\n isSaving.value = true\n error.value = null\n try {\n const response = await api.patch<{ plugin_name: string; config: Record<string, unknown> }>(\n `/api/plugins/${encodeURIComponent(name)}/config`,\n { config: config.value },\n )\n config.value = { ...response.config }\n savedConfig.value = { ...response.config }\n return true\n } catch (e) {\n error.value = e instanceof Error ? e.message : 'Failed to save plugin config'\n return false\n } finally {\n isSaving.value = false\n }\n }\n\n function reset(): void {\n config.value = { ...savedConfig.value }\n error.value = null\n }\n\n onMounted(() => {\n load()\n })\n\n return {\n config,\n isLoading,\n isSaving,\n error,\n isDirty,\n load,\n save,\n reset,\n }\n}\n"],"names":[],"mappings":";;;AAeO,SAAS,gBAAgB,YAA4C;AAC1E,QAAM,MAAM,OAAA;AACZ,QAAM,EAAE,OAAA,IAAW,mBAAA;AAEnB,QAAM,eAAe,SAAS,MAAA;;AAAM,2BAAc,YAAO,UAAP,mBAAc,SAAQ;AAAA,GAAE;AAE1E,QAAM,SAAS,IAA6B,EAAE;AAC9C,QAAM,cAAc,IAA6B,EAAE;AACnD,QAAM,YAAY,IAAI,KAAK;AAC3B,QAAM,WAAW,IAAI,KAAK;AAC1B,QAAM,QAAQ,IAAmB,IAAI;AAErC,QAAM,UAAU,SAAS,MAAM;AAC7B,WAAO,KAAK,UAAU,OAAO,KAAK,MAAM,KAAK,UAAU,YAAY,KAAK;AAAA,EAC1E,CAAC;AAED,iBAAe,OAAsB;AACnC,UAAM,OAAO,aAAa;AAC1B,QAAI,CAAC,KAAM;AAEX,cAAU,QAAQ;AAClB,UAAM,QAAQ;AACd,QAAI;AACF,YAAM,WAAW,MAAM,IAAI;AAAA,QACzB,gBAAgB,mBAAmB,IAAI,CAAC;AAAA,MAAA;AAE1C,aAAO,QAAQ,EAAE,GAAG,SAAS,OAAA;AAC7B,kBAAY,QAAQ,EAAE,GAAG,SAAS,OAAA;AAAA,IACpC,SAAS,GAAG;AACV,YAAM,QAAQ,aAAa,QAAQ,EAAE,UAAU;AAAA,IACjD,UAAA;AACE,gBAAU,QAAQ;AAAA,IACpB;AAAA,EACF;AAEA,iBAAe,OAAyB;AACtC,UAAM,OAAO,aAAa;AAC1B,QAAI,CAAC,KAAM,QAAO;AAElB,aAAS,QAAQ;AACjB,UAAM,QAAQ;AACd,QAAI;AACF,YAAM,WAAW,MAAM,IAAI;AAAA,QACzB,gBAAgB,mBAAmB,IAAI,CAAC;AAAA,QACxC,EAAE,QAAQ,OAAO,MAAA;AAAA,MAAM;AAEzB,aAAO,QAAQ,EAAE,GAAG,SAAS,OAAA;AAC7B,kBAAY,QAAQ,EAAE,GAAG,SAAS,OAAA;AAClC,aAAO;AAAA,IACT,SAAS,GAAG;AACV,YAAM,QAAQ,aAAa,QAAQ,EAAE,UAAU;AAC/C,aAAO;AAAA,IACT,UAAA;AACE,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF;AAEA,WAAS,QAAc;AACrB,WAAO,QAAQ,EAAE,GAAG,YAAY,MAAA;AAChC,UAAM,QAAQ;AAAA,EAChB;AAEA,YAAU,MAAM;AACd,SAAA;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
|
package/package.json
CHANGED
|
@@ -40,6 +40,11 @@ export function useApi(options: ApiClientOptions = {}): UseApiReturn {
|
|
|
40
40
|
const authStore = useAuthStore()
|
|
41
41
|
const apiClient = getApiClient()
|
|
42
42
|
|
|
43
|
+
// Ensure auth store is initialized (reads token from localStorage)
|
|
44
|
+
if (!authStore.isInitialized) {
|
|
45
|
+
authStore.initialize()
|
|
46
|
+
}
|
|
47
|
+
|
|
43
48
|
// Attach interceptor only once
|
|
44
49
|
if (!interceptorAttached) {
|
|
45
50
|
apiClient.interceptors.request.use((config) => {
|
|
@@ -56,11 +56,9 @@ export function useExperimentSelector(
|
|
|
56
56
|
try {
|
|
57
57
|
const params = new URLSearchParams()
|
|
58
58
|
// Priority: explicit option > platform context (single type) > no filter
|
|
59
|
+
const allowedTypes = getPlatformContext()?.allowedExperimentTypes
|
|
59
60
|
const effectiveType = experimentType
|
|
60
|
-
?? (
|
|
61
|
-
const types = getPlatformContext()?.allowedExperimentTypes
|
|
62
|
-
return types?.length === 1 ? types[0] : undefined
|
|
63
|
-
})()
|
|
61
|
+
?? (allowedTypes?.length === 1 ? allowedTypes[0] : undefined)
|
|
64
62
|
if (effectiveType) params.set('experiment_type', effectiveType)
|
|
65
63
|
if (filters.status) params.set('status', filters.status)
|
|
66
64
|
if (filters.search) params.set('search', filters.search)
|
|
@@ -74,12 +72,19 @@ export function useExperimentSelector(
|
|
|
74
72
|
const url = `${base}/experiments${query ? `?${query}` : ''}`
|
|
75
73
|
const data = await api.get<ExperimentListResponse>(url)
|
|
76
74
|
|
|
75
|
+
// Client-side filter for multiple allowed types (backend only supports single type)
|
|
76
|
+
let filtered = data.experiments
|
|
77
|
+
if (!effectiveType && allowedTypes && allowedTypes.length > 1) {
|
|
78
|
+
const typeSet = new Set(allowedTypes)
|
|
79
|
+
filtered = filtered.filter(e => typeSet.has(e.experiment_type))
|
|
80
|
+
}
|
|
81
|
+
|
|
77
82
|
if (page.value === 0) {
|
|
78
|
-
experiments.value =
|
|
83
|
+
experiments.value = filtered
|
|
79
84
|
} else {
|
|
80
|
-
experiments.value = [...experiments.value, ...
|
|
85
|
+
experiments.value = [...experiments.value, ...filtered]
|
|
81
86
|
}
|
|
82
|
-
total.value = data.total
|
|
87
|
+
total.value = effectiveType || !allowedTypes?.length ? data.total : filtered.length
|
|
83
88
|
} catch (e) {
|
|
84
89
|
error.value = e instanceof Error ? e.message : 'Failed to fetch experiments'
|
|
85
90
|
if (page.value === 0) {
|
|
@@ -55,7 +55,7 @@ export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
|
|
|
55
55
|
isSaving.value = true
|
|
56
56
|
error.value = null
|
|
57
57
|
try {
|
|
58
|
-
const response = await api.
|
|
58
|
+
const response = await api.patch<{ plugin_name: string; config: Record<string, unknown> }>(
|
|
59
59
|
`/api/plugins/${encodeURIComponent(name)}/config`,
|
|
60
60
|
{ config: config.value },
|
|
61
61
|
)
|