@morscherlab/mint-sdk 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/BaseModal-B9UA8Y_I.js +165 -0
  2. package/dist/BaseModal-B9UA8Y_I.js.map +1 -0
  3. package/dist/BaseSelect-DksaKYq_.js +176 -0
  4. package/dist/BaseSelect-DksaKYq_.js.map +1 -0
  5. package/dist/ExperimentPopover-CCYB1oWp.js +361 -0
  6. package/dist/ExperimentPopover-CCYB1oWp.js.map +1 -0
  7. package/dist/ExperimentPopover-D0bg_fqM.js +3 -0
  8. package/dist/ExperimentSelectorModal-B_kPbXcg.js +4 -0
  9. package/dist/ExperimentSelectorModal-wm7yUdAr.js +720 -0
  10. package/dist/ExperimentSelectorModal-wm7yUdAr.js.map +1 -0
  11. package/dist/SettingsModal-L7Ejny45.js +5 -0
  12. package/dist/SettingsModal-LEKI6Ebl.js +521 -0
  13. package/dist/SettingsModal-LEKI6Ebl.js.map +1 -0
  14. package/dist/{auth-BulIv_km.js → auth-D9q2GIcv.js} +3 -80
  15. package/dist/auth-D9q2GIcv.js.map +1 -0
  16. package/dist/components/DataFrame.vue.d.ts +3 -0
  17. package/dist/components/ExperimentDataViewer.vue.d.ts +2 -0
  18. package/dist/components/PluginWorkspaceView.vue.d.ts +2 -2
  19. package/dist/components/index.js +7 -2
  20. package/dist/{components-DtX3LDLq.js → components-CdjRzHI2.js} +533 -2025
  21. package/dist/components-CdjRzHI2.js.map +1 -0
  22. package/dist/composables/index.js +9 -3
  23. package/dist/composables/usePluginClient.d.ts +2 -1
  24. package/dist/{composables-wNt7VtkF.js → composables-DJgqPrlR.js} +7 -12
  25. package/dist/{composables-wNt7VtkF.js.map → composables-DJgqPrlR.js.map} +1 -1
  26. package/dist/experiment-utils-hGXMHlAc.js +109 -0
  27. package/dist/experiment-utils-hGXMHlAc.js.map +1 -0
  28. package/dist/index.js +16 -5
  29. package/dist/index.js.map +1 -1
  30. package/dist/install.js +7 -2
  31. package/dist/install.js.map +1 -1
  32. package/dist/permissions.js +81 -0
  33. package/dist/permissions.js.map +1 -0
  34. package/dist/stores/index.js +1 -1
  35. package/dist/styles.css +3233 -3185
  36. package/dist/templates/index.js +3 -1
  37. package/dist/templates-Do43ZIMb.js +5065 -0
  38. package/dist/templates-Do43ZIMb.js.map +1 -0
  39. package/dist/{templates-DSbHJC4v.js → useControlSchema-0n8Bcftq.js} +10 -5335
  40. package/dist/useControlSchema-0n8Bcftq.js.map +1 -0
  41. package/dist/useDropdownState-Ben4DnjJ.js +47 -0
  42. package/dist/useDropdownState-Ben4DnjJ.js.map +1 -0
  43. package/dist/useEventListener-CfVkP9Xz.js +57 -0
  44. package/dist/useEventListener-CfVkP9Xz.js.map +1 -0
  45. package/dist/useExperimentSelector-BpZklTbV.js +469 -0
  46. package/dist/useExperimentSelector-BpZklTbV.js.map +1 -0
  47. package/dist/useFormBuilder-COfYWDuC.js +729 -0
  48. package/dist/useFormBuilder-COfYWDuC.js.map +1 -0
  49. package/dist/{useProtocolTemplates-DwBhEPPU.js → useProtocolTemplates-TUQO_F3n.js} +8 -1298
  50. package/dist/useProtocolTemplates-TUQO_F3n.js.map +1 -0
  51. package/dist/utils/pluginIcon.d.ts +29 -2
  52. package/package.json +5 -1
  53. package/src/__tests__/components/DataFrame.test.ts +37 -0
  54. package/src/__tests__/components/PluginIcon.test.ts +77 -0
  55. package/src/__tests__/composables/usePluginClient.test.ts +11 -10
  56. package/src/components/AppTopBar.vue +7 -6
  57. package/src/components/DataFrame.vue +27 -2
  58. package/src/components/ExperimentDataViewer.vue +5 -1
  59. package/src/components/PluginIcon.story.vue +31 -1
  60. package/src/components/PluginIcon.vue +94 -4
  61. package/src/composables/usePluginClient.ts +3 -12
  62. package/src/styles/components/dataframe.css +26 -0
  63. package/src/styles/components/plugin-icon.css +5 -0
  64. package/src/utils/pluginIcon.ts +159 -2
  65. package/dist/auth-BulIv_km.js.map +0 -1
  66. package/dist/components-DtX3LDLq.js.map +0 -1
  67. package/dist/templates-DSbHJC4v.js.map +0 -1
  68. package/dist/useProtocolTemplates-DwBhEPPU.js.map +0 -1
@@ -1,7 +1,9 @@
1
- import { B as toBioTemplateComponentProps, F as getBioTemplateComponentProps$1, H as toBioTemplateComponentPropsById, Hn as useConcentrationUnits, Jt as getBioTemplatePresetInfo, L as toBioTemplateComponentBindings, P as getBioTemplateComponentBindings, Qt as getBioTemplatePackInfo, R as toBioTemplateComponentBindingsById, Rn as getFieldRegistryEntry, Sn as useControlWorkspace, 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, zn as getTypeDefault } from "./templates-DSbHJC4v.js";
2
- import { g as useSettingsStore, t as useAuthStore } from "./auth-BulIv_km.js";
3
- import { computed, effectScope, getCurrentScope, inject, onMounted, onScopeDispose, onUnmounted, provide, reactive, readonly, ref, shallowRef, toRaw, toValue, watch } from "vue";
4
- import axios from "axios";
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 { resolveExperimentCode as $, parseCSV as A, useExperimentSelector as B, useExpansionSet as C, hslToHex as D, hexToHsl as E, classKey as F, EXPERIMENT_STATUS_LABELS as G, useDebouncedWatch as H, useWellPlateEditor as I, SORT_OPTIONS as J, EXPERIMENT_STATUS_OPTIONS as K, useDoseCalculator as L, extractSampleOptionsFromDesignData as M, unwrapExperimentDesignData as N, extractSamplesFromDesignData as O, DEFAULT_COLORS as P, getExperimentStatusVariant as Q, APP_EXPERIMENT_KEY as R, resolveCurrentExperimentId as S, deriveShade as T, useApi as U, useRequestSyncState as V, DATE_PRESET_OPTIONS as W, formatExperimentDate as X, datePresetToISO as Y, formatExperimentStatus as Z, useScheduleDrag as _, useReagentSeries as a, useToast as at, currentExperimentFromContext as b, useBioTemplatePresetWorkspace as c, useTextSearch as ct, getBioTemplateComponentProps as d, usePlatformContext as et, toBioTemplateComponentPropsByComponent as f, useExperimentSave as g, useTemplateCollection as h, generateDilutionSeries as i, useTheme as it, extractSampleNamesFromDesignData as j, useAutoGroup as k, useBioTemplatePackWorkspace as l, compareSortValues as lt, useBioTemplateControls as m, DEFAULT_PRESETS as n, evaluateCondition as nt, useGroupAssignment as o, candidateMatchesSearch as ot, useBioTemplateComponents as p, EXPERIMENT_STATUS_VARIANT_MAP as q, DEFAULT_UNITS as r, useForm as rt, useRackEditor as s, normalizeSearchQuery as st, useProtocolTemplates as t, useFormBuilder as tt, useBioTemplateWorkspace as u, useSortedItems as ut, useExperimentSamples as v, useSampleGroups as w, getInjectedPlatformContext as x, useExperimentData as y, useAppExperiment as z };
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-DwBhEPPU.js.map
4137
+ //# sourceMappingURL=useProtocolTemplates-TUQO_F3n.js.map