@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.
- package/dist/BaseModal-B9UA8Y_I.js +165 -0
- package/dist/BaseModal-B9UA8Y_I.js.map +1 -0
- package/dist/BaseSelect-DksaKYq_.js +176 -0
- package/dist/BaseSelect-DksaKYq_.js.map +1 -0
- package/dist/ExperimentPopover-CCYB1oWp.js +361 -0
- package/dist/ExperimentPopover-CCYB1oWp.js.map +1 -0
- package/dist/ExperimentPopover-D0bg_fqM.js +3 -0
- package/dist/ExperimentSelectorModal-B_kPbXcg.js +4 -0
- package/dist/ExperimentSelectorModal-wm7yUdAr.js +720 -0
- package/dist/ExperimentSelectorModal-wm7yUdAr.js.map +1 -0
- package/dist/SettingsModal-L7Ejny45.js +5 -0
- package/dist/SettingsModal-LEKI6Ebl.js +521 -0
- package/dist/SettingsModal-LEKI6Ebl.js.map +1 -0
- package/dist/{auth-BulIv_km.js → auth-D9q2GIcv.js} +3 -80
- package/dist/auth-D9q2GIcv.js.map +1 -0
- package/dist/components/DataFrame.vue.d.ts +3 -0
- package/dist/components/ExperimentDataViewer.vue.d.ts +2 -0
- package/dist/components/PluginWorkspaceView.vue.d.ts +2 -2
- package/dist/components/index.js +7 -2
- package/dist/{components-DtX3LDLq.js → components-CdjRzHI2.js} +533 -2025
- package/dist/components-CdjRzHI2.js.map +1 -0
- package/dist/composables/index.js +9 -3
- package/dist/composables/usePluginClient.d.ts +2 -1
- package/dist/{composables-wNt7VtkF.js → composables-DJgqPrlR.js} +7 -12
- package/dist/{composables-wNt7VtkF.js.map → composables-DJgqPrlR.js.map} +1 -1
- package/dist/experiment-utils-hGXMHlAc.js +109 -0
- package/dist/experiment-utils-hGXMHlAc.js.map +1 -0
- package/dist/index.js +16 -5
- package/dist/index.js.map +1 -1
- package/dist/install.js +7 -2
- package/dist/install.js.map +1 -1
- package/dist/permissions.js +81 -0
- package/dist/permissions.js.map +1 -0
- package/dist/stores/index.js +1 -1
- package/dist/styles.css +3233 -3185
- package/dist/templates/index.js +3 -1
- package/dist/templates-Do43ZIMb.js +5065 -0
- package/dist/templates-Do43ZIMb.js.map +1 -0
- package/dist/{templates-DSbHJC4v.js → useControlSchema-0n8Bcftq.js} +10 -5335
- package/dist/useControlSchema-0n8Bcftq.js.map +1 -0
- package/dist/useDropdownState-Ben4DnjJ.js +47 -0
- package/dist/useDropdownState-Ben4DnjJ.js.map +1 -0
- package/dist/useEventListener-CfVkP9Xz.js +57 -0
- package/dist/useEventListener-CfVkP9Xz.js.map +1 -0
- package/dist/useExperimentSelector-BpZklTbV.js +469 -0
- package/dist/useExperimentSelector-BpZklTbV.js.map +1 -0
- package/dist/useFormBuilder-COfYWDuC.js +729 -0
- package/dist/useFormBuilder-COfYWDuC.js.map +1 -0
- package/dist/{useProtocolTemplates-DwBhEPPU.js → useProtocolTemplates-TUQO_F3n.js} +8 -1298
- package/dist/useProtocolTemplates-TUQO_F3n.js.map +1 -0
- package/dist/utils/pluginIcon.d.ts +29 -2
- package/package.json +5 -1
- package/src/__tests__/components/DataFrame.test.ts +37 -0
- package/src/__tests__/components/PluginIcon.test.ts +77 -0
- package/src/__tests__/composables/usePluginClient.test.ts +11 -10
- package/src/components/AppTopBar.vue +7 -6
- package/src/components/DataFrame.vue +27 -2
- package/src/components/ExperimentDataViewer.vue +5 -1
- package/src/components/PluginIcon.story.vue +31 -1
- package/src/components/PluginIcon.vue +94 -4
- package/src/composables/usePluginClient.ts +3 -12
- package/src/styles/components/dataframe.css +26 -0
- package/src/styles/components/plugin-icon.css +5 -0
- package/src/utils/pluginIcon.ts +159 -2
- package/dist/auth-BulIv_km.js.map +0 -1
- package/dist/components-DtX3LDLq.js.map +0 -1
- package/dist/templates-DSbHJC4v.js.map +0 -1
- package/dist/useProtocolTemplates-DwBhEPPU.js.map +0 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
1
|
+
import { A as useConcentrationUnits, u as useControlWorkspace } from "./useControlSchema-0n8Bcftq.js";
|
|
2
|
+
import { r as useSettingsStore } from "./auth-D9q2GIcv.js";
|
|
3
|
+
import { u as resolveExperimentCode } from "./experiment-utils-hGXMHlAc.js";
|
|
4
|
+
import { i as useApi, r as useRequestSyncState } from "./useExperimentSelector-BpZklTbV.js";
|
|
5
|
+
import { B as toBioTemplateComponentProps, F as getBioTemplateComponentProps$1, H as toBioTemplateComponentPropsById, Jt as getBioTemplatePresetInfo, L as toBioTemplateComponentBindings, P as getBioTemplateComponentBindings, Qt as getBioTemplatePackInfo, R as toBioTemplateComponentBindingsById, U as toBioTemplateComponentSnippets, V as toBioTemplateComponentPropsByComponent$1, W as toBioTemplateComponentUsage, d as createBioTemplatePresetCollectionFromControls, dn as extractTemplateCollection, i as createBioTemplateControlToolkit, p as createBioTemplatePackCollection, sn as createTemplateCollection, u as createBioTemplatePresetCollection, un as ensureTemplateFromCollection, z as toBioTemplateComponentImports } from "./templates-Do43ZIMb.js";
|
|
6
|
+
import { computed, effectScope, getCurrentScope, inject, onScopeDispose, onUnmounted, provide, reactive, readonly, ref, shallowRef, toRaw, toValue, watch } from "vue";
|
|
5
7
|
//#region src/composables/useSortedItems.ts
|
|
6
8
|
/** Shared sorting for SDK tables and lists with stable empty-value handling. */
|
|
7
9
|
function useSortedItems(options) {
|
|
@@ -144,1298 +146,6 @@ function useTheme() {
|
|
|
144
146
|
};
|
|
145
147
|
}
|
|
146
148
|
//#endregion
|
|
147
|
-
//#region src/composables/useFormValidation.ts
|
|
148
|
-
var validators = {
|
|
149
|
-
required: (value, message = "This field is required") => {
|
|
150
|
-
if (value === null || value === void 0 || value === "") return message;
|
|
151
|
-
if (Array.isArray(value) && value.length === 0) return message;
|
|
152
|
-
return null;
|
|
153
|
-
},
|
|
154
|
-
minLength: (value, min, message) => {
|
|
155
|
-
if (typeof value !== "string") return null;
|
|
156
|
-
if (value.length < min) return message || `Must be at least ${min} characters`;
|
|
157
|
-
return null;
|
|
158
|
-
},
|
|
159
|
-
maxLength: (value, max, message) => {
|
|
160
|
-
if (typeof value !== "string") return null;
|
|
161
|
-
if (value.length > max) return message || `Must be at most ${max} characters`;
|
|
162
|
-
return null;
|
|
163
|
-
},
|
|
164
|
-
min: (value, min, message) => {
|
|
165
|
-
if (typeof value !== "number") return null;
|
|
166
|
-
if (value < min) return message || `Must be at least ${min}`;
|
|
167
|
-
return null;
|
|
168
|
-
},
|
|
169
|
-
max: (value, max, message) => {
|
|
170
|
-
if (typeof value !== "number") return null;
|
|
171
|
-
if (value > max) return message || `Must be at most ${max}`;
|
|
172
|
-
return null;
|
|
173
|
-
},
|
|
174
|
-
pattern: (value, pattern, message) => {
|
|
175
|
-
if (typeof value !== "string") return null;
|
|
176
|
-
if (!pattern.test(value)) return message || "Invalid format";
|
|
177
|
-
return null;
|
|
178
|
-
},
|
|
179
|
-
email: (value, message = "Invalid email address") => {
|
|
180
|
-
if (typeof value !== "string" || !value) return null;
|
|
181
|
-
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return message;
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
function validateFieldValue(value, fieldRules, formData) {
|
|
186
|
-
if (!fieldRules) return null;
|
|
187
|
-
if (fieldRules.required) {
|
|
188
|
-
const message = typeof fieldRules.required === "string" ? fieldRules.required : void 0;
|
|
189
|
-
const error = validators.required(value, message);
|
|
190
|
-
if (error) return error;
|
|
191
|
-
}
|
|
192
|
-
if (value === null || value === void 0 || value === "") return null;
|
|
193
|
-
if (fieldRules.minLength !== void 0) {
|
|
194
|
-
const config = typeof fieldRules.minLength === "number" ? {
|
|
195
|
-
value: fieldRules.minLength,
|
|
196
|
-
message: void 0
|
|
197
|
-
} : fieldRules.minLength;
|
|
198
|
-
const error = validators.minLength(value, config.value, config.message);
|
|
199
|
-
if (error) return error;
|
|
200
|
-
}
|
|
201
|
-
if (fieldRules.maxLength !== void 0) {
|
|
202
|
-
const config = typeof fieldRules.maxLength === "number" ? {
|
|
203
|
-
value: fieldRules.maxLength,
|
|
204
|
-
message: void 0
|
|
205
|
-
} : fieldRules.maxLength;
|
|
206
|
-
const error = validators.maxLength(value, config.value, config.message);
|
|
207
|
-
if (error) return error;
|
|
208
|
-
}
|
|
209
|
-
if (fieldRules.min !== void 0) {
|
|
210
|
-
const config = typeof fieldRules.min === "number" ? {
|
|
211
|
-
value: fieldRules.min,
|
|
212
|
-
message: void 0
|
|
213
|
-
} : fieldRules.min;
|
|
214
|
-
const error = validators.min(value, config.value, config.message);
|
|
215
|
-
if (error) return error;
|
|
216
|
-
}
|
|
217
|
-
if (fieldRules.max !== void 0) {
|
|
218
|
-
const config = typeof fieldRules.max === "number" ? {
|
|
219
|
-
value: fieldRules.max,
|
|
220
|
-
message: void 0
|
|
221
|
-
} : fieldRules.max;
|
|
222
|
-
const error = validators.max(value, config.value, config.message);
|
|
223
|
-
if (error) return error;
|
|
224
|
-
}
|
|
225
|
-
if (fieldRules.pattern !== void 0) {
|
|
226
|
-
const config = fieldRules.pattern instanceof RegExp ? {
|
|
227
|
-
value: fieldRules.pattern,
|
|
228
|
-
message: void 0
|
|
229
|
-
} : fieldRules.pattern;
|
|
230
|
-
const error = validators.pattern(value, config.value, config.message);
|
|
231
|
-
if (error) return error;
|
|
232
|
-
}
|
|
233
|
-
if (fieldRules.email) {
|
|
234
|
-
const message = typeof fieldRules.email === "string" ? fieldRules.email : void 0;
|
|
235
|
-
const error = validators.email(value, message);
|
|
236
|
-
if (error) return error;
|
|
237
|
-
}
|
|
238
|
-
if (fieldRules.custom) {
|
|
239
|
-
const customRules = Array.isArray(fieldRules.custom) ? fieldRules.custom : [fieldRules.custom];
|
|
240
|
-
for (const rule of customRules) {
|
|
241
|
-
const error = rule(value, formData);
|
|
242
|
-
if (error) return error;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
//#endregion
|
|
248
|
-
//#region src/composables/useForm.ts
|
|
249
|
-
/**
|
|
250
|
-
* Form state management composable with validation.
|
|
251
|
-
*
|
|
252
|
-
* @param initialValues - Initial form values
|
|
253
|
-
* @param rules - Validation rules for each field
|
|
254
|
-
*
|
|
255
|
-
* @example
|
|
256
|
-
* ```typescript
|
|
257
|
-
* const { data, errors, isValid, handleSubmit, getFieldProps } = useForm(
|
|
258
|
-
* { email: '', password: '' },
|
|
259
|
-
* {
|
|
260
|
-
* email: { required: true, email: true },
|
|
261
|
-
* password: { required: true, minLength: 8 },
|
|
262
|
-
* }
|
|
263
|
-
* )
|
|
264
|
-
*
|
|
265
|
-
* // In template
|
|
266
|
-
* <BaseInput v-bind="getFieldProps('email')" label="Email" />
|
|
267
|
-
* <BaseInput v-bind="getFieldProps('password')" type="password" label="Password" />
|
|
268
|
-
* <BaseButton @click="handleSubmit(onSubmit)" :disabled="!isValid">Submit</BaseButton>
|
|
269
|
-
* ```
|
|
270
|
-
*/
|
|
271
|
-
/** Reactive form state with field-level validation, dirty tracking, and submit handling. */
|
|
272
|
-
function useForm(initialValues, rules = {}) {
|
|
273
|
-
const cloneableInitialValues = deepToRaw(initialValues);
|
|
274
|
-
let _initialValues = structuredClone(cloneableInitialValues);
|
|
275
|
-
const data = reactive(structuredClone(cloneableInitialValues));
|
|
276
|
-
const errors = reactive(Object.keys(initialValues).reduce((acc, key) => {
|
|
277
|
-
acc[key] = null;
|
|
278
|
-
return acc;
|
|
279
|
-
}, {}));
|
|
280
|
-
const touched = reactive(Object.keys(initialValues).reduce((acc, key) => {
|
|
281
|
-
acc[key] = false;
|
|
282
|
-
return acc;
|
|
283
|
-
}, {}));
|
|
284
|
-
const dirty = reactive(Object.keys(initialValues).reduce((acc, key) => {
|
|
285
|
-
acc[key] = false;
|
|
286
|
-
return acc;
|
|
287
|
-
}, {}));
|
|
288
|
-
const isSubmitting = ref(false);
|
|
289
|
-
watch(() => ({ ...data }), (newData) => {
|
|
290
|
-
for (const key of Object.keys(newData)) dirty[key] = newData[key] !== _initialValues[key];
|
|
291
|
-
}, { deep: true });
|
|
292
|
-
function validateField(field) {
|
|
293
|
-
const value = data[field];
|
|
294
|
-
const fieldRules = rules[field];
|
|
295
|
-
const error = validateFieldValue(value, fieldRules, data);
|
|
296
|
-
errors[field] = error;
|
|
297
|
-
return error === null;
|
|
298
|
-
}
|
|
299
|
-
function validate() {
|
|
300
|
-
let isAllValid = true;
|
|
301
|
-
for (const field of Object.keys(data)) if (!validateField(field)) isAllValid = false;
|
|
302
|
-
return isAllValid;
|
|
303
|
-
}
|
|
304
|
-
const isValid = computed(() => {
|
|
305
|
-
return Object.values(errors).every((error) => error === null);
|
|
306
|
-
});
|
|
307
|
-
const isDirty = computed(() => {
|
|
308
|
-
return Object.values(dirty).some((d) => d);
|
|
309
|
-
});
|
|
310
|
-
function setFieldValue(field, value) {
|
|
311
|
-
data[field] = value;
|
|
312
|
-
if (touched[field]) validateField(field);
|
|
313
|
-
}
|
|
314
|
-
function setFieldError(field, error) {
|
|
315
|
-
errors[field] = error;
|
|
316
|
-
}
|
|
317
|
-
function setFieldTouched(field, isTouched = true) {
|
|
318
|
-
touched[field] = isTouched;
|
|
319
|
-
if (isTouched) validateField(field);
|
|
320
|
-
}
|
|
321
|
-
function reset(values) {
|
|
322
|
-
const resetValues = values ? {
|
|
323
|
-
..._initialValues,
|
|
324
|
-
...values
|
|
325
|
-
} : _initialValues;
|
|
326
|
-
for (const key of Object.keys(data)) {
|
|
327
|
-
data[key] = structuredClone(deepToRaw(resetValues[key]));
|
|
328
|
-
errors[key] = null;
|
|
329
|
-
touched[key] = false;
|
|
330
|
-
dirty[key] = false;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
function replaceState(values) {
|
|
334
|
-
const nextValues = structuredClone(deepToRaw(values));
|
|
335
|
-
_initialValues = structuredClone(nextValues);
|
|
336
|
-
for (const key of Object.keys(data)) {
|
|
337
|
-
if (key in nextValues) continue;
|
|
338
|
-
delete data[key];
|
|
339
|
-
delete errors[key];
|
|
340
|
-
delete touched[key];
|
|
341
|
-
delete dirty[key];
|
|
342
|
-
}
|
|
343
|
-
for (const key of Object.keys(nextValues)) {
|
|
344
|
-
data[key] = structuredClone(deepToRaw(nextValues[key]));
|
|
345
|
-
errors[key] = null;
|
|
346
|
-
touched[key] = false;
|
|
347
|
-
dirty[key] = false;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
function handleSubmit(onSubmit) {
|
|
351
|
-
return async (e) => {
|
|
352
|
-
e?.preventDefault();
|
|
353
|
-
for (const field of Object.keys(data)) touched[field] = true;
|
|
354
|
-
if (!validate()) return;
|
|
355
|
-
isSubmitting.value = true;
|
|
356
|
-
try {
|
|
357
|
-
await onSubmit(data);
|
|
358
|
-
} finally {
|
|
359
|
-
isSubmitting.value = false;
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
function getFieldProps(field) {
|
|
364
|
-
const fieldStr = field;
|
|
365
|
-
return {
|
|
366
|
-
modelValue: data[field],
|
|
367
|
-
"onUpdate:modelValue": (value) => setFieldValue(field, value),
|
|
368
|
-
onBlur: () => setFieldTouched(fieldStr),
|
|
369
|
-
error: touched[fieldStr] ? errors[fieldStr] : null
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
return {
|
|
373
|
-
data,
|
|
374
|
-
errors,
|
|
375
|
-
touched,
|
|
376
|
-
dirty,
|
|
377
|
-
isValid,
|
|
378
|
-
isDirty,
|
|
379
|
-
isSubmitting,
|
|
380
|
-
setFieldValue,
|
|
381
|
-
setFieldError,
|
|
382
|
-
setFieldTouched,
|
|
383
|
-
validateField,
|
|
384
|
-
validate,
|
|
385
|
-
reset,
|
|
386
|
-
replaceState,
|
|
387
|
-
handleSubmit,
|
|
388
|
-
getFieldProps
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
function deepToRaw(value) {
|
|
392
|
-
const raw = toRaw(value);
|
|
393
|
-
if (Array.isArray(raw)) return raw.map((item) => deepToRaw(item));
|
|
394
|
-
if (isPlainRecord(raw)) return Object.fromEntries(Object.entries(raw).map(([key, item]) => [key, deepToRaw(item)]));
|
|
395
|
-
return raw;
|
|
396
|
-
}
|
|
397
|
-
function isPlainRecord(value) {
|
|
398
|
-
return Object.prototype.toString.call(value) === "[object Object]";
|
|
399
|
-
}
|
|
400
|
-
//#endregion
|
|
401
|
-
//#region src/composables/formBuilderSchema.ts
|
|
402
|
-
/**
|
|
403
|
-
* Evaluate a JSON-serializable field condition against the current form data.
|
|
404
|
-
*
|
|
405
|
-
* Supports logical operators (`and`, `or`, `not`) and comparison operators
|
|
406
|
-
* (`eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `notIn`, `truthy`, `falsy`,
|
|
407
|
-
* `contains`). Returns `true` if the condition passes.
|
|
408
|
-
*/
|
|
409
|
-
function evaluateCondition(condition, data) {
|
|
410
|
-
if ("and" in condition) return condition.and.every((c) => evaluateCondition(c, data));
|
|
411
|
-
if ("or" in condition) return condition.or.some((c) => evaluateCondition(c, data));
|
|
412
|
-
if ("not" in condition) return !evaluateCondition(condition.not, data);
|
|
413
|
-
const value = data[condition.field];
|
|
414
|
-
if ("eq" in condition) return value === condition.eq;
|
|
415
|
-
if ("neq" in condition) return value !== condition.neq;
|
|
416
|
-
if ("gt" in condition) return typeof value === "number" && value > condition.gt;
|
|
417
|
-
if ("lt" in condition) return typeof value === "number" && value < condition.lt;
|
|
418
|
-
if ("gte" in condition) return typeof value === "number" && value >= condition.gte;
|
|
419
|
-
if ("lte" in condition) return typeof value === "number" && value <= condition.lte;
|
|
420
|
-
if ("in" in condition) return condition.in.includes(value);
|
|
421
|
-
if ("notIn" in condition) return !condition.notIn.includes(value);
|
|
422
|
-
if ("truthy" in condition) return !!value;
|
|
423
|
-
if ("falsy" in condition) return !value;
|
|
424
|
-
if ("contains" in condition) {
|
|
425
|
-
if (typeof value === "string") return value.includes(condition.contains);
|
|
426
|
-
if (Array.isArray(value)) return value.includes(condition.contains);
|
|
427
|
-
return false;
|
|
428
|
-
}
|
|
429
|
-
return true;
|
|
430
|
-
}
|
|
431
|
-
/** Return all sections across steps (wizard) or directly from a flat schema. */
|
|
432
|
-
function collectSections(schema) {
|
|
433
|
-
return schema.steps ? schema.steps.flatMap((step) => step.sections) : schema.sections;
|
|
434
|
-
}
|
|
435
|
-
/** Return all field schemas in schema order, flattening sections and steps. */
|
|
436
|
-
function flattenFields(schema) {
|
|
437
|
-
return collectSections(schema).flatMap((section) => section.fields);
|
|
438
|
-
}
|
|
439
|
-
/** Convert a JSON-safe `FieldValidation` descriptor to a runtime `FieldRules` object. */
|
|
440
|
-
function convertValidation(v) {
|
|
441
|
-
const rules = {};
|
|
442
|
-
if (v.required !== void 0) rules.required = v.required;
|
|
443
|
-
if (v.minLength !== void 0) rules.minLength = v.minLength;
|
|
444
|
-
if (v.maxLength !== void 0) rules.maxLength = v.maxLength;
|
|
445
|
-
if (v.min !== void 0) rules.min = v.min;
|
|
446
|
-
if (v.max !== void 0) rules.max = v.max;
|
|
447
|
-
if (v.email !== void 0) rules.email = v.email;
|
|
448
|
-
if (v.pattern !== void 0) rules.pattern = typeof v.pattern === "string" ? new RegExp(v.pattern) : {
|
|
449
|
-
value: new RegExp(v.pattern.value),
|
|
450
|
-
message: v.pattern.message
|
|
451
|
-
};
|
|
452
|
-
return rules;
|
|
453
|
-
}
|
|
454
|
-
function buildInitialValues(fields, initialData) {
|
|
455
|
-
const initialValues = {};
|
|
456
|
-
for (const field of fields) {
|
|
457
|
-
const key = field.name;
|
|
458
|
-
if (initialData && hasOwnKey(initialData, key)) initialValues[key] = initialData[key];
|
|
459
|
-
else if (field.defaultValue !== void 0) initialValues[key] = field.defaultValue;
|
|
460
|
-
else initialValues[key] = getTypeDefault(field.type);
|
|
461
|
-
}
|
|
462
|
-
return initialValues;
|
|
463
|
-
}
|
|
464
|
-
function buildRules(fields, enhancements) {
|
|
465
|
-
const rules = {};
|
|
466
|
-
for (const field of fields) {
|
|
467
|
-
const base = field.validation ? convertValidation(field.validation) : {};
|
|
468
|
-
const enhancement = enhancements?.fields?.[field.name];
|
|
469
|
-
const customValidators = [];
|
|
470
|
-
if (enhancement?.validate) {
|
|
471
|
-
const fn = enhancement.validate;
|
|
472
|
-
customValidators.push((value, formData) => fn(value, formData));
|
|
473
|
-
}
|
|
474
|
-
if (customValidators.length > 0) base.custom = customValidators;
|
|
475
|
-
if (Object.keys(base).length > 0) rules[field.name] = base;
|
|
476
|
-
}
|
|
477
|
-
return rules;
|
|
478
|
-
}
|
|
479
|
-
function replaceArray(target, values) {
|
|
480
|
-
target.splice(0, target.length, ...values);
|
|
481
|
-
}
|
|
482
|
-
function replaceRecord(target, source) {
|
|
483
|
-
for (const key of Object.keys(target)) delete target[key];
|
|
484
|
-
Object.assign(target, source);
|
|
485
|
-
}
|
|
486
|
-
function hasOwnKey(source, key) {
|
|
487
|
-
return Object.prototype.hasOwnProperty.call(source, key);
|
|
488
|
-
}
|
|
489
|
-
//#endregion
|
|
490
|
-
//#region src/composables/useFormBuilder.ts
|
|
491
|
-
/**
|
|
492
|
-
* Drive a `FormSchema` as reactive form state.
|
|
493
|
-
*
|
|
494
|
-
* Builds initial values from schema defaults and `initialData`, derives
|
|
495
|
-
* validation rules from `FieldValidation` descriptors and enhancement
|
|
496
|
-
* validators, evaluates `FieldCondition` expressions for field/section
|
|
497
|
-
* visibility, and wires wizard step navigation when `schema.steps` is set.
|
|
498
|
-
*
|
|
499
|
-
* @param schema - Declarative form or wizard schema.
|
|
500
|
-
* @param initialData - Values that override schema defaults.
|
|
501
|
-
* @param enhancements - TypeScript-only callbacks (dynamic options, validators,
|
|
502
|
-
* submit handler, transform, field-change watcher).
|
|
503
|
-
*/
|
|
504
|
-
/** Drives a declarative FormSchema as reactive state with conditional fields, wizard steps, and validation. */
|
|
505
|
-
function useFormBuilder(schema, initialData, enhancements) {
|
|
506
|
-
const currentSchema = shallowRef(schema);
|
|
507
|
-
const fields = reactive(flattenFields(schema));
|
|
508
|
-
const sections = reactive(collectSections(schema));
|
|
509
|
-
const rules = reactive(buildRules(fields, enhancements));
|
|
510
|
-
const form = useForm(buildInitialValues(fields, initialData), rules);
|
|
511
|
-
function isFieldVisible(name) {
|
|
512
|
-
const enhancement = enhancements?.fields?.[name];
|
|
513
|
-
if (enhancement?.visible) return enhancement.visible(form.data);
|
|
514
|
-
const field = fields.find((f) => f.name === name);
|
|
515
|
-
if (field?.condition) return evaluateCondition(field.condition, form.data);
|
|
516
|
-
return true;
|
|
517
|
-
}
|
|
518
|
-
function isSectionVisible(id) {
|
|
519
|
-
const section = sections.find((s) => s.id === id);
|
|
520
|
-
if (section?.condition) return evaluateCondition(section.condition, form.data);
|
|
521
|
-
return true;
|
|
522
|
-
}
|
|
523
|
-
function getResolvedFieldProps(field) {
|
|
524
|
-
const entry = getFieldRegistryEntry(field.type);
|
|
525
|
-
const formProps = form.getFieldProps(field.name);
|
|
526
|
-
const merged = {
|
|
527
|
-
...entry.defaults,
|
|
528
|
-
...field.props ?? {}
|
|
529
|
-
};
|
|
530
|
-
const enhancement = enhancements?.fields?.[field.name];
|
|
531
|
-
if (enhancement?.props) Object.assign(merged, enhancement.props(form.data));
|
|
532
|
-
const options = getFieldOptions(field.name);
|
|
533
|
-
if (options) merged.options = options;
|
|
534
|
-
if (entry.vModel) {
|
|
535
|
-
merged.modelValue = formProps.modelValue;
|
|
536
|
-
merged["onUpdate:modelValue"] = formProps["onUpdate:modelValue"];
|
|
537
|
-
}
|
|
538
|
-
merged.onBlur = formProps.onBlur;
|
|
539
|
-
if (formProps.error) merged.error = true;
|
|
540
|
-
if (field.placeholder) merged.placeholder = field.placeholder;
|
|
541
|
-
if (field.size) merged.size = field.size;
|
|
542
|
-
if (field.disabled) merged.disabled = true;
|
|
543
|
-
if (field.readonly) merged.readonly = true;
|
|
544
|
-
if (field.type === "radio" && !merged.name) merged.name = field.name;
|
|
545
|
-
return merged;
|
|
546
|
-
}
|
|
547
|
-
function getFieldOptions(name) {
|
|
548
|
-
const enhancement = enhancements?.fields?.[name];
|
|
549
|
-
if (enhancement?.options) return enhancement.options(form.data);
|
|
550
|
-
const field = fields.find((f) => f.name === name);
|
|
551
|
-
if (field?.props?.options) return field.props.options;
|
|
552
|
-
}
|
|
553
|
-
const currentStep = ref(0);
|
|
554
|
-
const isCurrentStepValid = computed(() => {
|
|
555
|
-
if (!currentSchema.value.steps) return form.isValid.value;
|
|
556
|
-
const step = currentSchema.value.steps[currentStep.value];
|
|
557
|
-
if (!step) return true;
|
|
558
|
-
for (const section of step.sections) {
|
|
559
|
-
if (!isSectionVisible(section.id)) continue;
|
|
560
|
-
for (const field of section.fields) {
|
|
561
|
-
if (!isFieldVisible(field.name)) continue;
|
|
562
|
-
if (!form.validateField(field.name)) return false;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
return true;
|
|
566
|
-
});
|
|
567
|
-
function goNext() {
|
|
568
|
-
if (!currentSchema.value.steps) return false;
|
|
569
|
-
const step = currentSchema.value.steps[currentStep.value];
|
|
570
|
-
if (!step) return false;
|
|
571
|
-
let valid = true;
|
|
572
|
-
for (const section of step.sections) {
|
|
573
|
-
if (!isSectionVisible(section.id)) continue;
|
|
574
|
-
for (const field of section.fields) {
|
|
575
|
-
if (!isFieldVisible(field.name)) continue;
|
|
576
|
-
form.setFieldTouched(field.name, true);
|
|
577
|
-
if (!form.validateField(field.name)) valid = false;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
if (!valid) return false;
|
|
581
|
-
if (currentStep.value < currentSchema.value.steps.length - 1) currentStep.value++;
|
|
582
|
-
return true;
|
|
583
|
-
}
|
|
584
|
-
function goBack() {
|
|
585
|
-
if (currentStep.value > 0) currentStep.value--;
|
|
586
|
-
}
|
|
587
|
-
function goToStep(index) {
|
|
588
|
-
if (!currentSchema.value.steps) return;
|
|
589
|
-
if (index >= 0 && index < currentSchema.value.steps.length) currentStep.value = index;
|
|
590
|
-
}
|
|
591
|
-
function validate() {
|
|
592
|
-
let allValid = true;
|
|
593
|
-
for (const field of fields) {
|
|
594
|
-
if (!isFieldVisible(field.name)) continue;
|
|
595
|
-
form.setFieldTouched(field.name, true);
|
|
596
|
-
if (!form.validateField(field.name)) allValid = false;
|
|
597
|
-
}
|
|
598
|
-
return allValid;
|
|
599
|
-
}
|
|
600
|
-
async function submit() {
|
|
601
|
-
if (!validate()) return;
|
|
602
|
-
let submitData = {};
|
|
603
|
-
for (const field of fields) if (isFieldVisible(field.name)) submitData[field.name] = form.data[field.name];
|
|
604
|
-
if (enhancements?.transform) submitData = enhancements.transform(submitData);
|
|
605
|
-
if (enhancements?.onSubmit) {
|
|
606
|
-
form.isSubmitting.value = true;
|
|
607
|
-
try {
|
|
608
|
-
await enhancements.onSubmit(submitData);
|
|
609
|
-
} finally {
|
|
610
|
-
form.isSubmitting.value = false;
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
function reset(values) {
|
|
615
|
-
form.reset(values);
|
|
616
|
-
currentStep.value = 0;
|
|
617
|
-
}
|
|
618
|
-
function updateSchema(nextSchema, nextData) {
|
|
619
|
-
currentSchema.value = nextSchema;
|
|
620
|
-
const nextFields = flattenFields(nextSchema);
|
|
621
|
-
replaceArray(fields, nextFields);
|
|
622
|
-
replaceArray(sections, collectSections(nextSchema));
|
|
623
|
-
replaceRecord(rules, buildRules(nextFields, enhancements));
|
|
624
|
-
form.replaceState(buildInitialValues(nextFields, nextData));
|
|
625
|
-
currentStep.value = 0;
|
|
626
|
-
}
|
|
627
|
-
if (enhancements?.onFieldChange) {
|
|
628
|
-
const callback = enhancements.onFieldChange;
|
|
629
|
-
watch(() => ({ ...form.data }), (newData, oldData) => {
|
|
630
|
-
if (!oldData) return;
|
|
631
|
-
for (const key of Object.keys(newData)) if (newData[key] !== oldData[key]) callback(key, newData[key], newData);
|
|
632
|
-
}, { deep: true });
|
|
633
|
-
}
|
|
634
|
-
return {
|
|
635
|
-
form,
|
|
636
|
-
rules,
|
|
637
|
-
isFieldVisible,
|
|
638
|
-
isSectionVisible,
|
|
639
|
-
fields,
|
|
640
|
-
getResolvedFieldProps,
|
|
641
|
-
getFieldOptions,
|
|
642
|
-
currentStep,
|
|
643
|
-
isCurrentStepValid,
|
|
644
|
-
goNext,
|
|
645
|
-
goBack,
|
|
646
|
-
goToStep,
|
|
647
|
-
validate,
|
|
648
|
-
reset,
|
|
649
|
-
submit,
|
|
650
|
-
updateSchema
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
//#endregion
|
|
654
|
-
//#region src/composables/usePlatformContext.ts
|
|
655
|
-
var DEFAULT_CONTEXT = {
|
|
656
|
-
isIntegrated: false,
|
|
657
|
-
theme: "system"
|
|
658
|
-
};
|
|
659
|
-
var platformContext = ref({ ...DEFAULT_CONTEXT });
|
|
660
|
-
var inferredOrigins = /* @__PURE__ */ new Set();
|
|
661
|
-
var allowedOrigins = /* @__PURE__ */ new Set();
|
|
662
|
-
var allowAnyOrigin = false;
|
|
663
|
-
var initialized = false;
|
|
664
|
-
var listenerCount = 0;
|
|
665
|
-
var nextConsumerId = 0;
|
|
666
|
-
var currentHandler = null;
|
|
667
|
-
var consumerOrigins = /* @__PURE__ */ new Map();
|
|
668
|
-
var allowAnyOriginConsumers = /* @__PURE__ */ new Set();
|
|
669
|
-
/**
|
|
670
|
-
* Derive origin from URL (protocol + host)
|
|
671
|
-
*/
|
|
672
|
-
function getOriginFromUrl(url) {
|
|
673
|
-
try {
|
|
674
|
-
return new URL(url).origin;
|
|
675
|
-
} catch {
|
|
676
|
-
return null;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
function normalizeAllowedOrigins(origins) {
|
|
680
|
-
const normalized = /* @__PURE__ */ new Set();
|
|
681
|
-
if (!origins) return normalized;
|
|
682
|
-
for (const origin of origins) normalized.add(getOriginFromUrl(origin) || origin);
|
|
683
|
-
return normalized;
|
|
684
|
-
}
|
|
685
|
-
function recomputeOriginPolicy() {
|
|
686
|
-
allowedOrigins = new Set(inferredOrigins);
|
|
687
|
-
for (const origins of consumerOrigins.values()) for (const origin of origins) allowedOrigins.add(origin);
|
|
688
|
-
allowAnyOrigin = allowAnyOriginConsumers.size > 0;
|
|
689
|
-
}
|
|
690
|
-
function resetPlatformContextState() {
|
|
691
|
-
inferredOrigins = /* @__PURE__ */ new Set();
|
|
692
|
-
allowedOrigins = /* @__PURE__ */ new Set();
|
|
693
|
-
allowAnyOrigin = false;
|
|
694
|
-
initialized = false;
|
|
695
|
-
listenerCount = 0;
|
|
696
|
-
currentHandler = null;
|
|
697
|
-
consumerOrigins.clear();
|
|
698
|
-
allowAnyOriginConsumers.clear();
|
|
699
|
-
platformContext.value = { ...DEFAULT_CONTEXT };
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Check if an origin is allowed for postMessage communication
|
|
703
|
-
*/
|
|
704
|
-
function isOriginAllowed(origin) {
|
|
705
|
-
if (allowAnyOrigin) {
|
|
706
|
-
console.warn("[MINT SDK] postMessage origin validation disabled - only use in development");
|
|
707
|
-
return true;
|
|
708
|
-
}
|
|
709
|
-
if (origin === window.location.origin) return true;
|
|
710
|
-
return allowedOrigins.has(origin);
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* Platform context composable for plugin integration with MINT Platform.
|
|
714
|
-
*
|
|
715
|
-
* Provides secure communication with the parent platform via postMessage.
|
|
716
|
-
*
|
|
717
|
-
* @param options - Configuration options
|
|
718
|
-
* @param options.allowedOrigins - List of allowed origins for postMessage
|
|
719
|
-
* @param options.allowAnyOrigin - Allow any origin (UNSAFE, development only)
|
|
720
|
-
*
|
|
721
|
-
* @example
|
|
722
|
-
* ```typescript
|
|
723
|
-
* // Basic usage - derives origin from platform injection
|
|
724
|
-
* const { isIntegrated, user, theme } = usePlatformContext()
|
|
725
|
-
*
|
|
726
|
-
* // With explicit allowed origins
|
|
727
|
-
* const { isIntegrated } = usePlatformContext({
|
|
728
|
-
* allowedOrigins: ['https://mint.example.com']
|
|
729
|
-
* })
|
|
730
|
-
*
|
|
731
|
-
* // Development mode (UNSAFE)
|
|
732
|
-
* const { isIntegrated } = usePlatformContext({
|
|
733
|
-
* allowAnyOrigin: import.meta.env.DEV
|
|
734
|
-
* })
|
|
735
|
-
* ```
|
|
736
|
-
*/
|
|
737
|
-
/** Connects a plugin to the MINT platform via postMessage, exposing user, theme, and experiment context. */
|
|
738
|
-
function usePlatformContext(options = {}) {
|
|
739
|
-
const consumerId = ++nextConsumerId;
|
|
740
|
-
const instanceOrigins = normalizeAllowedOrigins(options.allowedOrigins);
|
|
741
|
-
const instanceAllowAnyOrigin = options.allowAnyOrigin === true;
|
|
742
|
-
function detectPlatform() {
|
|
743
|
-
const detectedOrigins = /* @__PURE__ */ new Set();
|
|
744
|
-
const platformData = window.__MINT_PLATFORM__;
|
|
745
|
-
if (platformData) {
|
|
746
|
-
platformContext.value = {
|
|
747
|
-
...platformData,
|
|
748
|
-
isIntegrated: true
|
|
749
|
-
};
|
|
750
|
-
if (platformData.platformOrigin) detectedOrigins.add(platformData.platformOrigin);
|
|
751
|
-
else if (platformData.platformApiUrl) {
|
|
752
|
-
const origin = getOriginFromUrl(platformData.platformApiUrl);
|
|
753
|
-
if (origin) detectedOrigins.add(origin);
|
|
754
|
-
}
|
|
755
|
-
} else {
|
|
756
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
757
|
-
const hasPluginParam = urlParams.has("mint-plugin");
|
|
758
|
-
const platformOrigin = urlParams.get("mint-origin");
|
|
759
|
-
if (platformOrigin) {
|
|
760
|
-
const origin = getOriginFromUrl(platformOrigin);
|
|
761
|
-
if (origin) detectedOrigins.add(origin);
|
|
762
|
-
}
|
|
763
|
-
platformContext.value = {
|
|
764
|
-
isIntegrated: hasPluginParam,
|
|
765
|
-
theme: (() => {
|
|
766
|
-
try {
|
|
767
|
-
const s = localStorage.getItem("mint-settings");
|
|
768
|
-
if (s) return JSON.parse(s).theme || "system";
|
|
769
|
-
} catch {}
|
|
770
|
-
return "system";
|
|
771
|
-
})(),
|
|
772
|
-
platformOrigin: platformOrigin || void 0
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
inferredOrigins = detectedOrigins;
|
|
776
|
-
}
|
|
777
|
-
function handlePlatformMessage(event) {
|
|
778
|
-
if (event.source !== window.parent) return;
|
|
779
|
-
if (!isOriginAllowed(event.origin)) {
|
|
780
|
-
console.warn(`[MINT SDK] Rejected postMessage from untrusted origin: ${event.origin}`);
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
try {
|
|
784
|
-
const platformEvent = event.data;
|
|
785
|
-
if (!platformEvent.type?.startsWith("mint:")) return;
|
|
786
|
-
switch (platformEvent.type) {
|
|
787
|
-
case "mint:theme-changed":
|
|
788
|
-
platformContext.value.theme = platformEvent.payload;
|
|
789
|
-
break;
|
|
790
|
-
case "mint:user-changed":
|
|
791
|
-
platformContext.value.user = platformEvent.payload;
|
|
792
|
-
break;
|
|
793
|
-
}
|
|
794
|
-
} catch {}
|
|
795
|
-
}
|
|
796
|
-
/**
|
|
797
|
-
* Send a message to the parent platform.
|
|
798
|
-
* Uses validated target origin for security.
|
|
799
|
-
*/
|
|
800
|
-
function sendToPlatform(event) {
|
|
801
|
-
if (!platformContext.value.isIntegrated || window.parent === window) return;
|
|
802
|
-
let targetOrigin;
|
|
803
|
-
if (platformContext.value.platformOrigin) targetOrigin = platformContext.value.platformOrigin;
|
|
804
|
-
else if (allowedOrigins.size > 0) targetOrigin = allowedOrigins.values().next().value;
|
|
805
|
-
else if (allowAnyOrigin) {
|
|
806
|
-
targetOrigin = "*";
|
|
807
|
-
console.warn("[MINT SDK] Using wildcard origin for postMessage - only use in development");
|
|
808
|
-
} else {
|
|
809
|
-
console.warn("[MINT SDK] Cannot send postMessage: no platform origin configured");
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
window.parent.postMessage(event, targetOrigin);
|
|
813
|
-
}
|
|
814
|
-
/**
|
|
815
|
-
* Request navigation to a path in the platform.
|
|
816
|
-
*/
|
|
817
|
-
function navigate(path) {
|
|
818
|
-
sendToPlatform({
|
|
819
|
-
type: "mint:navigate",
|
|
820
|
-
payload: path
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
/**
|
|
824
|
-
* Show a notification in the platform.
|
|
825
|
-
*/
|
|
826
|
-
function notify(message, type = "info") {
|
|
827
|
-
sendToPlatform({
|
|
828
|
-
type: "mint:notification",
|
|
829
|
-
payload: {
|
|
830
|
-
message,
|
|
831
|
-
type
|
|
832
|
-
}
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
onMounted(() => {
|
|
836
|
-
consumerOrigins.set(consumerId, instanceOrigins);
|
|
837
|
-
if (instanceAllowAnyOrigin) allowAnyOriginConsumers.add(consumerId);
|
|
838
|
-
if (!initialized) {
|
|
839
|
-
detectPlatform();
|
|
840
|
-
currentHandler = handlePlatformMessage;
|
|
841
|
-
window.addEventListener("message", handlePlatformMessage);
|
|
842
|
-
initialized = true;
|
|
843
|
-
}
|
|
844
|
-
listenerCount++;
|
|
845
|
-
recomputeOriginPolicy();
|
|
846
|
-
});
|
|
847
|
-
onUnmounted(() => {
|
|
848
|
-
consumerOrigins.delete(consumerId);
|
|
849
|
-
allowAnyOriginConsumers.delete(consumerId);
|
|
850
|
-
listenerCount = Math.max(0, listenerCount - 1);
|
|
851
|
-
if (listenerCount === 0) {
|
|
852
|
-
if (currentHandler) window.removeEventListener("message", currentHandler);
|
|
853
|
-
resetPlatformContextState();
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
recomputeOriginPolicy();
|
|
857
|
-
});
|
|
858
|
-
return {
|
|
859
|
-
context: platformContext,
|
|
860
|
-
isIntegrated: computed(() => platformContext.value.isIntegrated),
|
|
861
|
-
plugin: computed(() => platformContext.value.plugin),
|
|
862
|
-
user: computed(() => platformContext.value.user),
|
|
863
|
-
theme: computed(() => platformContext.value.theme),
|
|
864
|
-
features: computed(() => platformContext.value.features),
|
|
865
|
-
navigate,
|
|
866
|
-
notify,
|
|
867
|
-
sendToPlatform
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
//#endregion
|
|
871
|
-
//#region src/composables/experiment-utils.ts
|
|
872
|
-
function formatExperimentDate(dateStr) {
|
|
873
|
-
try {
|
|
874
|
-
return new Date(dateStr).toLocaleDateString(void 0, {
|
|
875
|
-
year: "numeric",
|
|
876
|
-
month: "short",
|
|
877
|
-
day: "numeric"
|
|
878
|
-
});
|
|
879
|
-
} catch {
|
|
880
|
-
return dateStr;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
function datePresetToISO(preset) {
|
|
884
|
-
const now = /* @__PURE__ */ new Date();
|
|
885
|
-
const days = preset === "last_7_days" ? 7 : preset === "last_30_days" ? 30 : 90;
|
|
886
|
-
return (/* @__PURE__ */ new Date(now.getTime() - days * 864e5)).toISOString();
|
|
887
|
-
}
|
|
888
|
-
var EXPERIMENT_STATUS_OPTIONS = [
|
|
889
|
-
{
|
|
890
|
-
value: "",
|
|
891
|
-
label: "All statuses"
|
|
892
|
-
},
|
|
893
|
-
{
|
|
894
|
-
value: "planned",
|
|
895
|
-
label: "Planned"
|
|
896
|
-
},
|
|
897
|
-
{
|
|
898
|
-
value: "ongoing",
|
|
899
|
-
label: "Ongoing"
|
|
900
|
-
},
|
|
901
|
-
{
|
|
902
|
-
value: "completed",
|
|
903
|
-
label: "Completed"
|
|
904
|
-
},
|
|
905
|
-
{
|
|
906
|
-
value: "cancelled",
|
|
907
|
-
label: "Cancelled"
|
|
908
|
-
}
|
|
909
|
-
];
|
|
910
|
-
var EXPERIMENT_STATUS_VARIANT_MAP = {
|
|
911
|
-
planned: "default",
|
|
912
|
-
ongoing: "primary",
|
|
913
|
-
completed: "success",
|
|
914
|
-
cancelled: "error"
|
|
915
|
-
};
|
|
916
|
-
var EXPERIMENT_STATUS_LABELS = {
|
|
917
|
-
planned: "Planned",
|
|
918
|
-
ongoing: "Ongoing",
|
|
919
|
-
completed: "Completed",
|
|
920
|
-
cancelled: "Cancelled"
|
|
921
|
-
};
|
|
922
|
-
function formatExperimentStatus(status) {
|
|
923
|
-
if (!status) return "";
|
|
924
|
-
if (status in EXPERIMENT_STATUS_LABELS) return EXPERIMENT_STATUS_LABELS[status];
|
|
925
|
-
const label = String(status).replace(/[-_]+/g, " ").trim();
|
|
926
|
-
return label ? label.replace(/^\w/, (c) => c.toUpperCase()) : "";
|
|
927
|
-
}
|
|
928
|
-
function getExperimentStatusVariant(status) {
|
|
929
|
-
if (status && status in EXPERIMENT_STATUS_VARIANT_MAP) return EXPERIMENT_STATUS_VARIANT_MAP[status];
|
|
930
|
-
return "default";
|
|
931
|
-
}
|
|
932
|
-
function resolveExperimentCode(experiment) {
|
|
933
|
-
if (experiment.experiment_code) return experiment.experiment_code;
|
|
934
|
-
return experiment.id != null ? `EXP-${experiment.id}` : void 0;
|
|
935
|
-
}
|
|
936
|
-
var DATE_PRESET_OPTIONS = [
|
|
937
|
-
{
|
|
938
|
-
value: "",
|
|
939
|
-
label: "Any time"
|
|
940
|
-
},
|
|
941
|
-
{
|
|
942
|
-
value: "last_7_days",
|
|
943
|
-
label: "Last 7 days"
|
|
944
|
-
},
|
|
945
|
-
{
|
|
946
|
-
value: "last_30_days",
|
|
947
|
-
label: "Last 30 days"
|
|
948
|
-
},
|
|
949
|
-
{
|
|
950
|
-
value: "last_90_days",
|
|
951
|
-
label: "Last 90 days"
|
|
952
|
-
}
|
|
953
|
-
];
|
|
954
|
-
var SORT_OPTIONS = [
|
|
955
|
-
{
|
|
956
|
-
value: "created_at:desc",
|
|
957
|
-
label: "Newest first"
|
|
958
|
-
},
|
|
959
|
-
{
|
|
960
|
-
value: "created_at:asc",
|
|
961
|
-
label: "Oldest first"
|
|
962
|
-
},
|
|
963
|
-
{
|
|
964
|
-
value: "updated_at:desc",
|
|
965
|
-
label: "Recently updated"
|
|
966
|
-
},
|
|
967
|
-
{
|
|
968
|
-
value: "name:asc",
|
|
969
|
-
label: "Name A–Z"
|
|
970
|
-
},
|
|
971
|
-
{
|
|
972
|
-
value: "name:desc",
|
|
973
|
-
label: "Name Z–A"
|
|
974
|
-
}
|
|
975
|
-
];
|
|
976
|
-
//#endregion
|
|
977
|
-
//#region src/composables/useApi.ts
|
|
978
|
-
var apiClientInstance = null;
|
|
979
|
-
var interceptorAttached = false;
|
|
980
|
-
function joinUrlPath(baseUrl, path) {
|
|
981
|
-
if (!path) return baseUrl;
|
|
982
|
-
if (path.startsWith("?") || path.startsWith("#")) return `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
983
|
-
const normalizedBase = baseUrl.replace(/\/+$/, "");
|
|
984
|
-
const normalizedPath = path.replace(/^\/+/, "/");
|
|
985
|
-
return `${normalizedBase}${normalizedPath.startsWith("/") ? normalizedPath : `/${normalizedPath}`}`;
|
|
986
|
-
}
|
|
987
|
-
function getBasePath(baseUrl) {
|
|
988
|
-
if (!baseUrl) return "/";
|
|
989
|
-
try {
|
|
990
|
-
const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
991
|
-
return new URL(baseUrl, origin).pathname.replace(/\/+$/, "") || "/";
|
|
992
|
-
} catch {
|
|
993
|
-
return baseUrl.replace(/^https?:\/\/[^/]+/i, "").replace(/\/+$/, "") || "/";
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
function normalizeRequestUrl(baseUrl, url) {
|
|
997
|
-
if (!url || /^https?:\/\//.test(url)) return url;
|
|
998
|
-
const basePath = getBasePath(baseUrl);
|
|
999
|
-
if (basePath === "/") return url;
|
|
1000
|
-
const normalizedUrl = url.startsWith("/") ? url : `/${url}`;
|
|
1001
|
-
if (normalizedUrl === basePath || normalizedUrl.startsWith(`${basePath}?`) || normalizedUrl.startsWith(`${basePath}#`)) return normalizedUrl.slice(basePath.length);
|
|
1002
|
-
if (normalizedUrl.startsWith(`${basePath}/`)) return normalizedUrl.slice(basePath.length) || "";
|
|
1003
|
-
return url;
|
|
1004
|
-
}
|
|
1005
|
-
function asMutableHeaders(headers) {
|
|
1006
|
-
return headers ? headers : null;
|
|
1007
|
-
}
|
|
1008
|
-
function hasAuthorizationHeader(headers) {
|
|
1009
|
-
const bag = asMutableHeaders(headers);
|
|
1010
|
-
if (!bag) return false;
|
|
1011
|
-
if (typeof bag.has === "function") return bag.has("Authorization");
|
|
1012
|
-
return Object.keys(bag).some((key) => key.toLowerCase() === "authorization");
|
|
1013
|
-
}
|
|
1014
|
-
function setAuthorizationHeader(headers, value) {
|
|
1015
|
-
const bag = asMutableHeaders(headers);
|
|
1016
|
-
if (!bag) return;
|
|
1017
|
-
if (typeof bag.set === "function") {
|
|
1018
|
-
bag.set("Authorization", value);
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
bag.Authorization = value;
|
|
1022
|
-
}
|
|
1023
|
-
function deleteAuthorizationHeader(headers) {
|
|
1024
|
-
const bag = asMutableHeaders(headers);
|
|
1025
|
-
if (!bag) return;
|
|
1026
|
-
if (typeof bag.delete === "function") {
|
|
1027
|
-
bag.delete("Authorization");
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
for (const key of Object.keys(bag)) if (key.toLowerCase() === "authorization") delete bag[key];
|
|
1031
|
-
}
|
|
1032
|
-
function getApiClient() {
|
|
1033
|
-
if (!apiClientInstance) apiClientInstance = axios.create({ headers: { "Content-Type": "application/json" } });
|
|
1034
|
-
return apiClientInstance;
|
|
1035
|
-
}
|
|
1036
|
-
/** Axios-based API client that injects the plugin base URL and JWT auth header on every request. */
|
|
1037
|
-
function useApi(options = {}) {
|
|
1038
|
-
const settingsStore = useSettingsStore();
|
|
1039
|
-
const authStore = useAuthStore();
|
|
1040
|
-
const apiClient = getApiClient();
|
|
1041
|
-
if (!authStore.isInitialized) authStore.initialize();
|
|
1042
|
-
if (!interceptorAttached) {
|
|
1043
|
-
apiClient.interceptors.request.use((config) => {
|
|
1044
|
-
const request = config;
|
|
1045
|
-
if (request._mintSkipAuth) {
|
|
1046
|
-
delete request._mintSkipAuth;
|
|
1047
|
-
deleteAuthorizationHeader(request.headers);
|
|
1048
|
-
return request;
|
|
1049
|
-
}
|
|
1050
|
-
const currentAuthStore = useAuthStore();
|
|
1051
|
-
if (currentAuthStore.token && config.headers && !hasAuthorizationHeader(config.headers)) setAuthorizationHeader(config.headers, `Bearer ${currentAuthStore.token}`);
|
|
1052
|
-
return config;
|
|
1053
|
-
});
|
|
1054
|
-
interceptorAttached = true;
|
|
1055
|
-
}
|
|
1056
|
-
function getBaseUrl() {
|
|
1057
|
-
return options.baseUrl ?? settingsStore.getApiBaseUrl();
|
|
1058
|
-
}
|
|
1059
|
-
function normalizeUrl(url) {
|
|
1060
|
-
return normalizeRequestUrl(getBaseUrl(), url);
|
|
1061
|
-
}
|
|
1062
|
-
function requestConfig(config) {
|
|
1063
|
-
const base = {
|
|
1064
|
-
baseURL: getBaseUrl(),
|
|
1065
|
-
timeout: options.timeout ?? settingsStore.requestTimeout,
|
|
1066
|
-
...config
|
|
1067
|
-
};
|
|
1068
|
-
if (options.withAuth === false) {
|
|
1069
|
-
base._mintSkipAuth = true;
|
|
1070
|
-
deleteAuthorizationHeader(base.headers);
|
|
1071
|
-
}
|
|
1072
|
-
return base;
|
|
1073
|
-
}
|
|
1074
|
-
async function get(url, config) {
|
|
1075
|
-
return (await apiClient.get(normalizeUrl(url), requestConfig(config))).data;
|
|
1076
|
-
}
|
|
1077
|
-
async function post(url, data, config) {
|
|
1078
|
-
return (await apiClient.post(normalizeUrl(url), data, requestConfig(config))).data;
|
|
1079
|
-
}
|
|
1080
|
-
async function put(url, data, config) {
|
|
1081
|
-
return (await apiClient.put(normalizeUrl(url), data, requestConfig(config))).data;
|
|
1082
|
-
}
|
|
1083
|
-
async function patch(url, data, config) {
|
|
1084
|
-
return (await apiClient.patch(normalizeUrl(url), data, requestConfig(config))).data;
|
|
1085
|
-
}
|
|
1086
|
-
async function del(url, config) {
|
|
1087
|
-
return (await apiClient.delete(normalizeUrl(url), requestConfig(config))).data;
|
|
1088
|
-
}
|
|
1089
|
-
async function upload(url, file, fieldName = "file", additionalData) {
|
|
1090
|
-
const formData = new FormData();
|
|
1091
|
-
formData.append(fieldName, file);
|
|
1092
|
-
if (additionalData) Object.entries(additionalData).forEach(([key, value]) => {
|
|
1093
|
-
if (value !== void 0 && value !== null) formData.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
1094
|
-
});
|
|
1095
|
-
return (await apiClient.post(normalizeUrl(url), formData, requestConfig({ headers: { "Content-Type": void 0 } }))).data;
|
|
1096
|
-
}
|
|
1097
|
-
async function download(url, filename) {
|
|
1098
|
-
const response = await apiClient.get(normalizeUrl(url), requestConfig({ responseType: "blob" }));
|
|
1099
|
-
const blob = new Blob([response.data]);
|
|
1100
|
-
const blobUrl = URL.createObjectURL(blob);
|
|
1101
|
-
if (filename) {
|
|
1102
|
-
const link = document.createElement("a");
|
|
1103
|
-
link.href = blobUrl;
|
|
1104
|
-
link.download = filename;
|
|
1105
|
-
document.body.appendChild(link);
|
|
1106
|
-
link.click();
|
|
1107
|
-
document.body.removeChild(link);
|
|
1108
|
-
setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
|
|
1109
|
-
return "";
|
|
1110
|
-
}
|
|
1111
|
-
return blobUrl;
|
|
1112
|
-
}
|
|
1113
|
-
function buildUrl(path) {
|
|
1114
|
-
const baseUrl = getBaseUrl();
|
|
1115
|
-
return joinUrlPath(baseUrl, normalizeRequestUrl(baseUrl, path));
|
|
1116
|
-
}
|
|
1117
|
-
function buildWsUrl(path) {
|
|
1118
|
-
return joinUrlPath(settingsStore.getWsBaseUrl(), path);
|
|
1119
|
-
}
|
|
1120
|
-
return {
|
|
1121
|
-
client: apiClient,
|
|
1122
|
-
get,
|
|
1123
|
-
post,
|
|
1124
|
-
put,
|
|
1125
|
-
patch,
|
|
1126
|
-
delete: del,
|
|
1127
|
-
upload,
|
|
1128
|
-
download,
|
|
1129
|
-
buildUrl,
|
|
1130
|
-
buildWsUrl
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1133
|
-
//#endregion
|
|
1134
|
-
//#region src/composables/useDebouncedWatch.ts
|
|
1135
|
-
/** Watch a Vue source with debounced callback execution and explicit cancel/flush controls. */
|
|
1136
|
-
function useDebouncedWatch(source, callback, options = {}) {
|
|
1137
|
-
const { delay = 300, ...watchOptions } = options;
|
|
1138
|
-
const isPending = ref(false);
|
|
1139
|
-
let timer = null;
|
|
1140
|
-
let hasLatestValue = false;
|
|
1141
|
-
let latestValue;
|
|
1142
|
-
let latestOldValue;
|
|
1143
|
-
let callbackCleanup = null;
|
|
1144
|
-
let stopped = false;
|
|
1145
|
-
function clearTimer() {
|
|
1146
|
-
if (timer) {
|
|
1147
|
-
clearTimeout(timer);
|
|
1148
|
-
timer = null;
|
|
1149
|
-
}
|
|
1150
|
-
isPending.value = false;
|
|
1151
|
-
}
|
|
1152
|
-
function runCallbackCleanup() {
|
|
1153
|
-
if (!callbackCleanup) return;
|
|
1154
|
-
callbackCleanup();
|
|
1155
|
-
callbackCleanup = null;
|
|
1156
|
-
}
|
|
1157
|
-
function cancel() {
|
|
1158
|
-
clearTimer();
|
|
1159
|
-
hasLatestValue = false;
|
|
1160
|
-
latestOldValue = void 0;
|
|
1161
|
-
}
|
|
1162
|
-
function flush() {
|
|
1163
|
-
if (!hasLatestValue) return;
|
|
1164
|
-
const value = latestValue;
|
|
1165
|
-
const oldValue = latestOldValue;
|
|
1166
|
-
cancel();
|
|
1167
|
-
runCallbackCleanup();
|
|
1168
|
-
callback(value, oldValue, (cleanup) => {
|
|
1169
|
-
callbackCleanup = cleanup;
|
|
1170
|
-
});
|
|
1171
|
-
}
|
|
1172
|
-
function schedule(value, oldValue) {
|
|
1173
|
-
cancel();
|
|
1174
|
-
hasLatestValue = true;
|
|
1175
|
-
latestValue = value;
|
|
1176
|
-
latestOldValue = oldValue;
|
|
1177
|
-
isPending.value = true;
|
|
1178
|
-
timer = setTimeout(flush, delay);
|
|
1179
|
-
}
|
|
1180
|
-
const stopWatch = watch(source, (value, oldValue, onCleanup) => {
|
|
1181
|
-
schedule(value, oldValue);
|
|
1182
|
-
onCleanup(() => {
|
|
1183
|
-
cancel();
|
|
1184
|
-
runCallbackCleanup();
|
|
1185
|
-
});
|
|
1186
|
-
}, watchOptions);
|
|
1187
|
-
function stop() {
|
|
1188
|
-
if (stopped) return;
|
|
1189
|
-
stopped = true;
|
|
1190
|
-
stopWatch();
|
|
1191
|
-
cancel();
|
|
1192
|
-
runCallbackCleanup();
|
|
1193
|
-
}
|
|
1194
|
-
onScopeDispose(stop);
|
|
1195
|
-
return {
|
|
1196
|
-
isPending,
|
|
1197
|
-
cancel,
|
|
1198
|
-
flush,
|
|
1199
|
-
stop
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1202
|
-
//#endregion
|
|
1203
|
-
//#region src/composables/useRequestSyncState.ts
|
|
1204
|
-
/** Shared loading/error/timestamp state for generated plugin request helpers. */
|
|
1205
|
-
function useRequestSyncState(defaultErrorMessage = "Request failed.") {
|
|
1206
|
-
const loading = ref(false);
|
|
1207
|
-
const error = ref(null);
|
|
1208
|
-
const lastLoadedAt = ref(null);
|
|
1209
|
-
const lastSavedAt = ref(null);
|
|
1210
|
-
const lastRunAt = ref(null);
|
|
1211
|
-
function clearError() {
|
|
1212
|
-
error.value = null;
|
|
1213
|
-
}
|
|
1214
|
-
function readErrorMessage(value, fallback = defaultErrorMessage) {
|
|
1215
|
-
if (value instanceof Error && value.message) return value.message;
|
|
1216
|
-
if (typeof value === "string" && value.trim()) return value;
|
|
1217
|
-
if (typeof value === "object" && value !== null && "message" in value && typeof value.message === "string" && value.message) return value.message;
|
|
1218
|
-
return fallback;
|
|
1219
|
-
}
|
|
1220
|
-
function setError(value, fallback) {
|
|
1221
|
-
const message = readErrorMessage(value, fallback);
|
|
1222
|
-
error.value = message;
|
|
1223
|
-
return message;
|
|
1224
|
-
}
|
|
1225
|
-
function markLoaded(date = /* @__PURE__ */ new Date()) {
|
|
1226
|
-
lastLoadedAt.value = date;
|
|
1227
|
-
}
|
|
1228
|
-
function markSaved(date = /* @__PURE__ */ new Date()) {
|
|
1229
|
-
lastSavedAt.value = date;
|
|
1230
|
-
}
|
|
1231
|
-
function markRun(date = /* @__PURE__ */ new Date()) {
|
|
1232
|
-
lastRunAt.value = date;
|
|
1233
|
-
}
|
|
1234
|
-
async function run(operation, options = {}) {
|
|
1235
|
-
loading.value = true;
|
|
1236
|
-
clearError();
|
|
1237
|
-
try {
|
|
1238
|
-
const response = await operation();
|
|
1239
|
-
if (options.success === "load") markLoaded();
|
|
1240
|
-
else if (options.success === "save") markSaved();
|
|
1241
|
-
else if (options.success === "run") markRun();
|
|
1242
|
-
return response;
|
|
1243
|
-
} catch (err) {
|
|
1244
|
-
setError(err, options.errorMessage);
|
|
1245
|
-
throw err;
|
|
1246
|
-
} finally {
|
|
1247
|
-
loading.value = false;
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
return {
|
|
1251
|
-
loading,
|
|
1252
|
-
error,
|
|
1253
|
-
lastLoadedAt,
|
|
1254
|
-
lastSavedAt,
|
|
1255
|
-
lastRunAt,
|
|
1256
|
-
clearError,
|
|
1257
|
-
readErrorMessage,
|
|
1258
|
-
setError,
|
|
1259
|
-
markLoaded,
|
|
1260
|
-
markSaved,
|
|
1261
|
-
markRun,
|
|
1262
|
-
run
|
|
1263
|
-
};
|
|
1264
|
-
}
|
|
1265
|
-
//#endregion
|
|
1266
|
-
//#region src/composables/useExperimentSelector.ts
|
|
1267
|
-
function getPlatformContext() {
|
|
1268
|
-
if (typeof window === "undefined") return void 0;
|
|
1269
|
-
return window.__MINT_PLATFORM__;
|
|
1270
|
-
}
|
|
1271
|
-
function getPlatformApiUrl() {
|
|
1272
|
-
return getPlatformContext()?.platformApiUrl;
|
|
1273
|
-
}
|
|
1274
|
-
/** Fetches a paginated, filtered experiment list from the platform API for picker and selector UIs. */
|
|
1275
|
-
function useExperimentSelector(options = {}) {
|
|
1276
|
-
const { limit = 100, immediate = false, experimentType, apiBaseUrl } = options;
|
|
1277
|
-
const platformBase = apiBaseUrl ?? getPlatformApiUrl();
|
|
1278
|
-
const api = useApi();
|
|
1279
|
-
const experiments = ref([]);
|
|
1280
|
-
const total = ref(0);
|
|
1281
|
-
const selectedExperiment = ref(null);
|
|
1282
|
-
const request = useRequestSyncState("Failed to fetch experiments");
|
|
1283
|
-
const isLoading = request.loading;
|
|
1284
|
-
const error = request.error;
|
|
1285
|
-
const lastLoadedAt = request.lastLoadedAt;
|
|
1286
|
-
const page = ref(0);
|
|
1287
|
-
const sortKey = ref("created_at:desc");
|
|
1288
|
-
const experimentTypes = ref([]);
|
|
1289
|
-
const projects = ref([]);
|
|
1290
|
-
let filterOptionsFetched = false;
|
|
1291
|
-
const hasMore = computed(() => experiments.value.length < total.value);
|
|
1292
|
-
const filters = reactive({
|
|
1293
|
-
search: void 0,
|
|
1294
|
-
status: void 0,
|
|
1295
|
-
project: void 0,
|
|
1296
|
-
experimentType: void 0,
|
|
1297
|
-
datePreset: void 0
|
|
1298
|
-
});
|
|
1299
|
-
function parseSortKey() {
|
|
1300
|
-
const [field, order] = sortKey.value.split(":");
|
|
1301
|
-
return {
|
|
1302
|
-
sortBy: field || "created_at",
|
|
1303
|
-
sortOrder: order || "desc"
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
async function fetchExperiments() {
|
|
1307
|
-
try {
|
|
1308
|
-
await request.run(async () => {
|
|
1309
|
-
const params = new URLSearchParams();
|
|
1310
|
-
const allowedTypes = getPlatformContext()?.allowedExperimentTypes;
|
|
1311
|
-
const effectiveType = experimentType ?? (allowedTypes?.length === 1 ? allowedTypes[0] : void 0) ?? filters.experimentType ?? void 0;
|
|
1312
|
-
if (effectiveType) params.set("experiment_type", effectiveType);
|
|
1313
|
-
if (filters.status) params.set("status", filters.status);
|
|
1314
|
-
if (filters.search) params.set("search", filters.search);
|
|
1315
|
-
if (filters.project) params.set("project", filters.project);
|
|
1316
|
-
const { sortBy, sortOrder } = parseSortKey();
|
|
1317
|
-
params.set("sort_by", sortBy);
|
|
1318
|
-
params.set("sort_order", sortOrder);
|
|
1319
|
-
if (filters.datePreset) params.set("created_after", datePresetToISO(filters.datePreset));
|
|
1320
|
-
params.set("limit", String(limit));
|
|
1321
|
-
params.set("skip", String(page.value * limit));
|
|
1322
|
-
const query = params.toString();
|
|
1323
|
-
const url = `${platformBase ?? ""}/experiments${query ? `?${query}` : ""}`;
|
|
1324
|
-
const data = await api.get(url);
|
|
1325
|
-
let filtered = data.experiments;
|
|
1326
|
-
if (!effectiveType && allowedTypes && allowedTypes.length > 1) {
|
|
1327
|
-
const typeSet = new Set(allowedTypes);
|
|
1328
|
-
filtered = filtered.filter((e) => typeSet.has(e.experiment_type));
|
|
1329
|
-
}
|
|
1330
|
-
if (page.value === 0) experiments.value = filtered;
|
|
1331
|
-
else experiments.value = [...experiments.value, ...filtered];
|
|
1332
|
-
if (!effectiveType && allowedTypes && allowedTypes.length > 1) if (data.experiments.length < limit) total.value = experiments.value.length;
|
|
1333
|
-
else total.value = experiments.value.length + 1;
|
|
1334
|
-
else total.value = data.total;
|
|
1335
|
-
}, {
|
|
1336
|
-
success: "load",
|
|
1337
|
-
errorMessage: "Failed to fetch experiments"
|
|
1338
|
-
});
|
|
1339
|
-
} catch {
|
|
1340
|
-
if (page.value === 0) {
|
|
1341
|
-
experiments.value = [];
|
|
1342
|
-
total.value = 0;
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
async function fetchFilterOptions() {
|
|
1347
|
-
if (filterOptionsFetched) return;
|
|
1348
|
-
filterOptionsFetched = true;
|
|
1349
|
-
const base = platformBase ?? "";
|
|
1350
|
-
const [typesRes, projectsRes] = await Promise.allSettled([api.get(`${base}/experiments/experiment-types`), api.get(`${base}/projects`)]);
|
|
1351
|
-
if (typesRes.status === "fulfilled" && Array.isArray(typesRes.value)) experimentTypes.value = typesRes.value.map((t) => ({
|
|
1352
|
-
value: t.value,
|
|
1353
|
-
label: t.label,
|
|
1354
|
-
color: t.color
|
|
1355
|
-
}));
|
|
1356
|
-
if (projectsRes.status === "fulfilled" && projectsRes.value?.projects && Array.isArray(projectsRes.value.projects)) projects.value = projectsRes.value.projects.map((p) => ({
|
|
1357
|
-
value: p.name,
|
|
1358
|
-
label: p.name
|
|
1359
|
-
}));
|
|
1360
|
-
}
|
|
1361
|
-
async function loadMore() {
|
|
1362
|
-
if (!hasMore.value || isLoading.value) return;
|
|
1363
|
-
page.value++;
|
|
1364
|
-
await fetchExperiments();
|
|
1365
|
-
}
|
|
1366
|
-
function reset() {
|
|
1367
|
-
page.value = 0;
|
|
1368
|
-
experiments.value = [];
|
|
1369
|
-
total.value = 0;
|
|
1370
|
-
fetchExperiments();
|
|
1371
|
-
}
|
|
1372
|
-
function select(experiment) {
|
|
1373
|
-
selectedExperiment.value = experiment;
|
|
1374
|
-
}
|
|
1375
|
-
function clear() {
|
|
1376
|
-
selectedExperiment.value = null;
|
|
1377
|
-
filters.search = void 0;
|
|
1378
|
-
filters.status = void 0;
|
|
1379
|
-
filters.project = void 0;
|
|
1380
|
-
filters.experimentType = void 0;
|
|
1381
|
-
filters.datePreset = void 0;
|
|
1382
|
-
sortKey.value = "created_at:desc";
|
|
1383
|
-
page.value = 0;
|
|
1384
|
-
request.clearError();
|
|
1385
|
-
}
|
|
1386
|
-
const groupedByProject = computed(() => {
|
|
1387
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1388
|
-
for (const exp of experiments.value) {
|
|
1389
|
-
const key = exp.project_name ?? exp.project ?? "No project";
|
|
1390
|
-
const list = groups.get(key);
|
|
1391
|
-
if (list) list.push(exp);
|
|
1392
|
-
else groups.set(key, [exp]);
|
|
1393
|
-
}
|
|
1394
|
-
return [...groups.entries()].sort(([a], [b]) => {
|
|
1395
|
-
if (a === "No project") return 1;
|
|
1396
|
-
if (b === "No project") return -1;
|
|
1397
|
-
return a.localeCompare(b);
|
|
1398
|
-
});
|
|
1399
|
-
});
|
|
1400
|
-
const searchWatch = useDebouncedWatch(() => filters.search, () => {
|
|
1401
|
-
page.value = 0;
|
|
1402
|
-
fetchExperiments();
|
|
1403
|
-
}, { delay: 300 });
|
|
1404
|
-
watch(() => [
|
|
1405
|
-
filters.status,
|
|
1406
|
-
filters.project,
|
|
1407
|
-
filters.experimentType,
|
|
1408
|
-
filters.datePreset,
|
|
1409
|
-
sortKey.value
|
|
1410
|
-
], () => {
|
|
1411
|
-
searchWatch.cancel();
|
|
1412
|
-
page.value = 0;
|
|
1413
|
-
fetchExperiments();
|
|
1414
|
-
});
|
|
1415
|
-
if (immediate) fetchExperiments();
|
|
1416
|
-
return {
|
|
1417
|
-
experiments,
|
|
1418
|
-
total,
|
|
1419
|
-
selectedExperiment,
|
|
1420
|
-
filters,
|
|
1421
|
-
isLoading,
|
|
1422
|
-
error,
|
|
1423
|
-
lastLoadedAt,
|
|
1424
|
-
page,
|
|
1425
|
-
hasMore,
|
|
1426
|
-
sortKey,
|
|
1427
|
-
experimentTypes,
|
|
1428
|
-
projects,
|
|
1429
|
-
groupedByProject,
|
|
1430
|
-
fetch: fetchExperiments,
|
|
1431
|
-
loadMore,
|
|
1432
|
-
reset,
|
|
1433
|
-
select,
|
|
1434
|
-
clear,
|
|
1435
|
-
fetchFilterOptions
|
|
1436
|
-
};
|
|
1437
|
-
}
|
|
1438
|
-
//#endregion
|
|
1439
149
|
//#region src/composables/useAppExperiment.ts
|
|
1440
150
|
var APP_EXPERIMENT_KEY = Symbol("app-experiment");
|
|
1441
151
|
/** Manages the current experiment selection, save flow, and detach action for a plugin's AppTopBar app shell. */
|
|
@@ -5422,6 +4132,6 @@ function useProtocolTemplates() {
|
|
|
5422
4132
|
};
|
|
5423
4133
|
}
|
|
5424
4134
|
//#endregion
|
|
5425
|
-
export {
|
|
4135
|
+
export { parseCSV as A, useTheme as B, useExpansionSet as C, hslToHex as D, hexToHsl as E, classKey as F, compareSortValues as G, candidateMatchesSearch as H, useWellPlateEditor as I, useSortedItems as K, useDoseCalculator as L, extractSampleOptionsFromDesignData as M, unwrapExperimentDesignData as N, extractSamplesFromDesignData as O, DEFAULT_COLORS as P, APP_EXPERIMENT_KEY as R, resolveCurrentExperimentId as S, deriveShade as T, normalizeSearchQuery as U, useToast as V, useTextSearch as W, useScheduleDrag as _, useReagentSeries as a, currentExperimentFromContext as b, useBioTemplatePresetWorkspace as c, getBioTemplateComponentProps as d, toBioTemplateComponentPropsByComponent as f, useExperimentSave as g, useTemplateCollection as h, generateDilutionSeries as i, extractSampleNamesFromDesignData as j, useAutoGroup as k, useBioTemplatePackWorkspace as l, useBioTemplateControls as m, DEFAULT_PRESETS as n, useGroupAssignment as o, useBioTemplateComponents as p, DEFAULT_UNITS as r, useRackEditor as s, useProtocolTemplates as t, useBioTemplateWorkspace as u, useExperimentSamples as v, useSampleGroups as w, getInjectedPlatformContext as x, useExperimentData as y, useAppExperiment as z };
|
|
5426
4136
|
|
|
5427
|
-
//# sourceMappingURL=useProtocolTemplates-
|
|
4137
|
+
//# sourceMappingURL=useProtocolTemplates-TUQO_F3n.js.map
|