@morscherlab/mint-sdk 1.1.0 → 1.1.2
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/{EmptyState-DgBRoYAy.js → EmptyState-LMPS3keb.js} +3 -193
- package/dist/EmptyState-LMPS3keb.js.map +1 -0
- package/dist/{ExperimentPopover-BFw3QX8L.js → ExperimentPopover-D6_RGzTY.js} +113 -35
- package/dist/ExperimentPopover-D6_RGzTY.js.map +1 -0
- package/dist/{ExperimentSelectorModal-BBWXC7KW.js → ExperimentSelectorModal-DcQ38zua.js} +9 -11
- package/dist/ExperimentSelectorModal-DcQ38zua.js.map +1 -0
- package/dist/{SettingsModal-Dpg-X1OM.js → SettingsModal-Ccq2z-Ms.js} +9 -6
- package/dist/SettingsModal-Ccq2z-Ms.js.map +1 -0
- package/dist/__stories__/experiment-helpers.d.ts +3 -0
- package/dist/__tests__/stores/experiment.test.d.ts +1 -0
- package/dist/{analysisArtifactTypes-ejqR8MX3.js → analysisArtifactTypes-DQ0MjwHZ.js} +1 -1
- package/dist/{analysisArtifactTypes-ejqR8MX3.js.map → analysisArtifactTypes-DQ0MjwHZ.js.map} +1 -1
- package/dist/components/ExperimentPopover.vue.d.ts +5 -0
- package/dist/components/ExperimentSelectorModal.vue.d.ts +1 -5
- package/dist/components/PluginWorkspaceView.props.d.ts +1 -4
- package/dist/components/PluginWorkspaceView.vue.d.ts +0 -1
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +2 -2
- package/dist/components/index.js +7 -7
- package/dist/{components-Ci_BTx00.js → components-CV5zuWa8.js} +24 -29
- package/dist/components-CV5zuWa8.js.map +1 -0
- package/dist/composables/experiment-utils.d.ts +10 -0
- package/dist/composables/index.d.ts +2 -2
- package/dist/composables/index.js +11 -10
- package/dist/composables/useAppExperiment.d.ts +29 -17
- package/dist/composables/usePlatformContext.d.ts +3 -3
- package/dist/{composables-C0VEJ73O.js → composables-CW8NRXLB.js} +5 -5
- package/dist/{composables-C0VEJ73O.js.map → composables-CW8NRXLB.js.map} +1 -1
- package/dist/experiment-Y9Iz7KYi.js +95 -0
- package/dist/experiment-Y9Iz7KYi.js.map +1 -0
- package/dist/{experiment-utils-CxkruZ3G.js → experiment-utils-7Yw-h0tD.js} +16 -2
- package/dist/{experiment-utils-CxkruZ3G.js.map → experiment-utils-7Yw-h0tD.js.map} +1 -1
- package/dist/index.js +16 -15
- package/dist/index.js.map +1 -1
- package/dist/install.js +7 -7
- package/dist/{plotly.min-BR53hja4.js → plotly.min-c2Sez60c.js} +1 -1
- package/dist/{plotly.min-BR53hja4.js.map → plotly.min-c2Sez60c.js.map} +1 -1
- package/dist/stores/auth.d.ts +6 -6
- package/dist/stores/experiment.d.ts +166 -0
- package/dist/stores/index.d.ts +1 -0
- package/dist/stores/index.js +3 -2
- package/dist/styles.css +188 -0
- package/dist/templates/index.js +4 -4
- package/dist/{templates-Cdj93S9C.js → templates-CJRFiWUX.js} +2 -2
- package/dist/{templates-Cdj93S9C.js.map → templates-CJRFiWUX.js.map} +1 -1
- package/dist/types/auth.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/platform.d.ts +1 -1
- package/dist/{auth-B87xM--s.js → useApi-CDBq6z23.js} +191 -2
- package/dist/useApi-CDBq6z23.js.map +1 -0
- package/dist/{useControlSchema-B_qgU4rW.js → useControlSchema-BEAgLyyx.js} +5 -4
- package/dist/{useControlSchema-B_qgU4rW.js.map → useControlSchema-BEAgLyyx.js.map} +1 -1
- package/dist/{useDropdownState-gVaMXtwW.js → useDropdownState-Be-ug_ci.js} +2 -2
- package/dist/{useDropdownState-gVaMXtwW.js.map → useDropdownState-Be-ug_ci.js.map} +1 -1
- package/dist/{useExperimentSelector-BuQZRgMt.js → useExperimentSelector-DFg_iEHI.js} +36 -16
- package/dist/useExperimentSelector-DFg_iEHI.js.map +1 -0
- package/dist/{useFormBuilder-D1G5J5Z5.js → useFormBuilder-CR9pBApW.js} +2 -2
- package/dist/{useFormBuilder-D1G5J5Z5.js.map → useFormBuilder-CR9pBApW.js.map} +1 -1
- package/dist/{useProtocolTemplates-DeaOmMaw.js → useProtocolTemplates-DXeIHMwV.js} +38 -42
- package/dist/useProtocolTemplates-DXeIHMwV.js.map +1 -0
- package/package.json +1 -1
- package/src/__stories__/experiment-helpers.ts +12 -9
- package/src/__tests__/components/AppTopBar.test.ts +10 -6
- package/src/__tests__/components/ExperimentPopover.test.ts +64 -0
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +86 -41
- package/src/__tests__/components/PluginWorkspaceView.test.ts +103 -19
- package/src/__tests__/components/SettingsModal.test.ts +29 -1
- package/src/__tests__/composables/experiment-utils.test.ts +33 -0
- package/src/__tests__/composables/useAppExperiment.test.ts +331 -92
- package/src/__tests__/composables/useAuth.test.ts +2 -2
- package/src/__tests__/composables/useExperimentSelector.test.ts +71 -0
- package/src/__tests__/composables/usePlatformContext.test.ts +5 -1
- package/src/__tests__/stores/experiment.test.ts +300 -0
- package/src/components/AppTopBar.vue +0 -2
- package/src/components/ExperimentPopover.story.vue +59 -0
- package/src/components/ExperimentPopover.vue +96 -6
- package/src/components/ExperimentSelectorModal.story.vue +6 -16
- package/src/components/ExperimentSelectorModal.vue +8 -4
- package/src/components/PluginWorkspaceView.props.ts +1 -4
- package/src/components/PluginWorkspaceView.story.vue +12 -5
- package/src/components/PluginWorkspaceView.vue +0 -2
- package/src/components/SettingsModal.vue +6 -1
- package/src/composables/experiment-utils.ts +22 -0
- package/src/composables/index.ts +1 -2
- package/src/composables/useAppExperiment.ts +57 -67
- package/src/composables/useAuth.ts +1 -1
- package/src/composables/useExperimentSelector.ts +45 -13
- package/src/stores/experiment.ts +120 -0
- package/src/stores/index.ts +1 -0
- package/src/styles/components/experiment-popover.css +105 -0
- package/src/types/auth.ts +1 -1
- package/src/types/platform.ts +1 -1
- package/dist/EmptyState-DgBRoYAy.js.map +0 -1
- package/dist/ExperimentPopover-BFw3QX8L.js.map +0 -1
- package/dist/ExperimentSelectorModal-BBWXC7KW.js.map +0 -1
- package/dist/SettingsModal-Dpg-X1OM.js.map +0 -1
- package/dist/auth-B87xM--s.js.map +0 -1
- package/dist/components-Ci_BTx00.js.map +0 -1
- package/dist/useExperimentSelector-BuQZRgMt.js.map +0 -1
- package/dist/useProtocolTemplates-DeaOmMaw.js.map +0 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, nextTick, onMounted } from 'vue'
|
|
2
|
+
import { ref, computed, nextTick, onMounted } from 'vue'
|
|
3
3
|
import ExperimentSelectorModal from './ExperimentSelectorModal.vue'
|
|
4
4
|
import ExperimentCodeBadge from './ExperimentCodeBadge.vue'
|
|
5
|
+
import { useExperimentStore } from '../stores/experiment'
|
|
5
6
|
import type { ExperimentSummary, ExperimentStatus, ExperimentSortField } from '../types'
|
|
6
7
|
|
|
7
8
|
// -- Mock data: realistic cell biology experiments --
|
|
@@ -247,15 +248,11 @@ const isOpenEmpty = ref(false)
|
|
|
247
248
|
const isOpenGrouped = ref(false)
|
|
248
249
|
const isOpenWithSelection = ref(false)
|
|
249
250
|
const isOpenAdvanced = ref(false)
|
|
250
|
-
const selectedExperiment = ref<ExperimentSummary | null>(null)
|
|
251
251
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
function handleDeselect() {
|
|
257
|
-
selectedExperiment.value = null
|
|
258
|
-
}
|
|
252
|
+
// The modal commits to the shared store rather than emitting; stories read the
|
|
253
|
+
// selection back from the same place a plugin would.
|
|
254
|
+
const experimentStore = useExperimentStore()
|
|
255
|
+
const selectedExperiment = computed(() => experimentStore.current)
|
|
259
256
|
</script>
|
|
260
257
|
|
|
261
258
|
<template>
|
|
@@ -290,8 +287,6 @@ function handleDeselect() {
|
|
|
290
287
|
:experiment-type="state.experimentType || undefined"
|
|
291
288
|
:title="state.title"
|
|
292
289
|
:current-experiment-id="selectedExperiment?.id ?? null"
|
|
293
|
-
@select="handleSelect"
|
|
294
|
-
@deselect="handleDeselect"
|
|
295
290
|
/>
|
|
296
291
|
</div>
|
|
297
292
|
</template>
|
|
@@ -319,7 +314,6 @@ function handleDeselect() {
|
|
|
319
314
|
v-model="isOpenAdvanced"
|
|
320
315
|
title="Select Experiment"
|
|
321
316
|
:show-filters="true"
|
|
322
|
-
@select="handleSelect"
|
|
323
317
|
/>
|
|
324
318
|
</div>
|
|
325
319
|
</Variant>
|
|
@@ -340,7 +334,6 @@ function handleDeselect() {
|
|
|
340
334
|
v-model="isOpenGrouped"
|
|
341
335
|
title="Select Experiment"
|
|
342
336
|
:group-by-project="true"
|
|
343
|
-
@select="handleSelect"
|
|
344
337
|
/>
|
|
345
338
|
</div>
|
|
346
339
|
</Variant>
|
|
@@ -361,8 +354,6 @@ function handleDeselect() {
|
|
|
361
354
|
v-model="isOpenWithSelection"
|
|
362
355
|
title="Select Experiment"
|
|
363
356
|
:current-experiment-id="1"
|
|
364
|
-
@select="handleSelect"
|
|
365
|
-
@deselect="handleDeselect"
|
|
366
357
|
/>
|
|
367
358
|
</div>
|
|
368
359
|
</Variant>
|
|
@@ -383,7 +374,6 @@ function handleDeselect() {
|
|
|
383
374
|
v-model="isOpenEmpty"
|
|
384
375
|
experiment-type="nonexistent_type"
|
|
385
376
|
title="Select Experiment"
|
|
386
|
-
@select="handleSelect"
|
|
387
377
|
/>
|
|
388
378
|
</div>
|
|
389
379
|
</Variant>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
|
4
4
|
import type { ModalSize, ExperimentSummary, ExperimentFilters } from '../types'
|
|
5
5
|
import { useExperimentSelector } from '../composables/useExperimentSelector'
|
|
6
|
+
import { useExperimentStore } from '../stores/experiment'
|
|
6
7
|
import {
|
|
7
8
|
formatExperimentDate,
|
|
8
9
|
formatExperimentStatus,
|
|
@@ -38,12 +39,15 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
38
39
|
showFilters: false,
|
|
39
40
|
})
|
|
40
41
|
|
|
42
|
+
// Selection is committed to the shared experiment store rather than emitted.
|
|
43
|
+
// A single owner of "the current experiment" is what stops callers holding —
|
|
44
|
+
// or inventing — a record of their own.
|
|
41
45
|
const emit = defineEmits<{
|
|
42
46
|
'update:modelValue': [value: boolean]
|
|
43
|
-
select: [experiment: ExperimentSummary]
|
|
44
|
-
deselect: []
|
|
45
47
|
}>()
|
|
46
48
|
|
|
49
|
+
const experimentStore = useExperimentStore()
|
|
50
|
+
|
|
47
51
|
const {
|
|
48
52
|
experiments,
|
|
49
53
|
filters,
|
|
@@ -113,12 +117,12 @@ function handleSortChange(value: string | number) {
|
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
function handleSelect(experiment: ExperimentSummary) {
|
|
116
|
-
|
|
120
|
+
experimentStore.select(experiment)
|
|
117
121
|
emit('update:modelValue', false)
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
function handleDeselect() {
|
|
121
|
-
|
|
125
|
+
experimentStore.clear()
|
|
122
126
|
emit('update:modelValue', false)
|
|
123
127
|
}
|
|
124
128
|
|
|
@@ -19,7 +19,6 @@ import type {
|
|
|
19
19
|
ControlSchema,
|
|
20
20
|
ControlWorkspaceOptions,
|
|
21
21
|
} from '../composables/useControlSchema'
|
|
22
|
-
import type { AppExperimentRecord } from '../composables/useAppExperiment'
|
|
23
22
|
|
|
24
23
|
export type PluginWorkspaceSidebarVariant = 'default' | 'analysis'
|
|
25
24
|
|
|
@@ -72,10 +71,8 @@ export interface PluginWorkspaceViewProps {
|
|
|
72
71
|
userInitial?: string
|
|
73
72
|
/** Classic profile email. */
|
|
74
73
|
userEmail?: string
|
|
75
|
-
/** Enable the built-in AppTopBar experiment popover and selector flow. */
|
|
74
|
+
/** Enable the built-in AppTopBar experiment popover and selector flow. The current experiment comes from the shared experiment store, not from a prop. Keep this comment on one line — the docs extractor drops multi-line JSDoc. */
|
|
76
75
|
experimentShell?: boolean
|
|
77
|
-
/** Current experiment mirrored into the AppTopBar experiment chip when experimentShell is enabled. */
|
|
78
|
-
experiment?: AppExperimentRecord | null
|
|
79
76
|
/** Save handler used by the AppTopBar experiment chip. Return a message to flash success. */
|
|
80
77
|
experimentSave?: () => string | null | Promise<string | null>
|
|
81
78
|
/** Disable the experiment save action. */
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { ref } from 'vue'
|
|
3
3
|
import type { ExperimentSummary, PillNavItem, SidebarToolSection } from '../types'
|
|
4
4
|
import { defineDoseDesignControlModel } from '../composables/useControlSchema'
|
|
5
|
-
import { ExperimentProvider
|
|
5
|
+
import { ExperimentProvider } from '../__stories__/experiment-helpers'
|
|
6
6
|
import PluginWorkspaceView from './PluginWorkspaceView.vue'
|
|
7
7
|
import AlertBox from './AlertBox.vue'
|
|
8
8
|
import BaseButton from './BaseButton.vue'
|
|
@@ -70,7 +70,9 @@ const doseDesignModel = defineDoseDesignControlModel({
|
|
|
70
70
|
},
|
|
71
71
|
})
|
|
72
72
|
const doseDesignValues = ref({})
|
|
73
|
-
|
|
73
|
+
// Read-only echo of the shell's selection events. The workspace itself reads
|
|
74
|
+
// the current experiment from the shared store, not from a prop.
|
|
75
|
+
const lastExperimentEvent = ref<string>('')
|
|
74
76
|
|
|
75
77
|
const state = ref({
|
|
76
78
|
title: 'Plugin Workspace',
|
|
@@ -83,11 +85,11 @@ const state = ref({
|
|
|
83
85
|
})
|
|
84
86
|
|
|
85
87
|
function handleExperimentSelect(experiment: ExperimentSummary) {
|
|
86
|
-
|
|
88
|
+
lastExperimentEvent.value = `selected: ${experiment.name}`
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
function handleExperimentDetach() {
|
|
90
|
-
|
|
92
|
+
lastExperimentEvent.value = 'detached'
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
async function saveDoseDesign() {
|
|
@@ -242,7 +244,6 @@ async function saveDoseDesign() {
|
|
|
242
244
|
sidebar-title="Design Controls"
|
|
243
245
|
sidebar-subtitle="Plate and dose setup"
|
|
244
246
|
experiment-shell
|
|
245
|
-
:experiment="shellExperiment"
|
|
246
247
|
:experiment-save="saveDoseDesign"
|
|
247
248
|
show-settings
|
|
248
249
|
show-theme-toggle
|
|
@@ -269,6 +270,12 @@ async function saveDoseDesign() {
|
|
|
269
270
|
</div>
|
|
270
271
|
</template>
|
|
271
272
|
</PluginWorkspaceView>
|
|
273
|
+
<p
|
|
274
|
+
v-if="lastExperimentEvent"
|
|
275
|
+
style="padding: 0.75rem 1rem; margin: 0; font-family: 'Fira Code', monospace; font-size: 0.75rem; color: var(--text-muted);"
|
|
276
|
+
>
|
|
277
|
+
Last shell event: {{ lastExperimentEvent }}
|
|
278
|
+
</p>
|
|
272
279
|
</div>
|
|
273
280
|
</ExperimentProvider>
|
|
274
281
|
</Variant>
|
|
@@ -52,7 +52,6 @@ const props = withDefaults(defineProps<PluginWorkspaceViewProps>(), {
|
|
|
52
52
|
userInitial: undefined,
|
|
53
53
|
userEmail: undefined,
|
|
54
54
|
experimentShell: false,
|
|
55
|
-
experiment: undefined,
|
|
56
55
|
experimentSave: undefined,
|
|
57
56
|
experimentSaveDisabled: false,
|
|
58
57
|
experimentSaveDisabledMessage: undefined,
|
|
@@ -144,7 +143,6 @@ const slots = useSlots()
|
|
|
144
143
|
|
|
145
144
|
if (props.experimentShell) {
|
|
146
145
|
useAppExperiment({
|
|
147
|
-
experiment: computed(() => props.experiment),
|
|
148
146
|
onSelect: experiment => emit('experiment-select', experiment),
|
|
149
147
|
onSave: props.experimentSave,
|
|
150
148
|
onDetach: () => emit('experiment-detach'),
|
|
@@ -191,7 +191,12 @@ watch(
|
|
|
191
191
|
watch(
|
|
192
192
|
() => ({ ...builder.form.data }),
|
|
193
193
|
(data) => {
|
|
194
|
-
if (settingsSchema.value)
|
|
194
|
+
if (settingsSchema.value) {
|
|
195
|
+
emit('update:values', {
|
|
196
|
+
...resolvedValues.value,
|
|
197
|
+
...data,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
195
200
|
},
|
|
196
201
|
{ deep: true },
|
|
197
202
|
)
|
|
@@ -67,6 +67,28 @@ export function resolveExperimentCode(experiment: ExperimentCodeSource): string
|
|
|
67
67
|
return undefined
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Mirrors vue-router's LocationQuery, whose array members are `string | null`.
|
|
71
|
+
// Declaring `string[]` here made the helper reject the very type every caller
|
|
72
|
+
// passes it, so keep this assignable from LocationQuery without importing
|
|
73
|
+
// vue-router (the SDK does not depend on it).
|
|
74
|
+
type RouteQueryValue = string | null | undefined | readonly (string | null | undefined)[]
|
|
75
|
+
type RouteQueryLike = Record<string, RouteQueryValue>
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Reads the experiment id a deep link was opened with.
|
|
79
|
+
*
|
|
80
|
+
* Returns an id and never a label: a display name belongs to the platform, so
|
|
81
|
+
* callers must resolve it (see `useExperimentStore().selectById`) rather than
|
|
82
|
+
* derive one from the query or the id itself.
|
|
83
|
+
*/
|
|
84
|
+
export function experimentIdFromRoute(query: RouteQueryLike): number | null {
|
|
85
|
+
const raw = query.experiment_id ?? query.experimentId
|
|
86
|
+
const value = Array.isArray(raw) ? raw.find(item => typeof item === 'string') : raw
|
|
87
|
+
if (typeof value !== 'string' || !value) return null
|
|
88
|
+
const id = Number(value)
|
|
89
|
+
return Number.isInteger(id) && id > 0 ? id : null
|
|
90
|
+
}
|
|
91
|
+
|
|
70
92
|
export const DATE_PRESET_OPTIONS: SelectOption<string>[] = [
|
|
71
93
|
{ value: '', label: 'Any time' },
|
|
72
94
|
{ value: 'last_7_days', label: 'Last 7 days' },
|
package/src/composables/index.ts
CHANGED
|
@@ -169,6 +169,7 @@ export {
|
|
|
169
169
|
formatExperimentStatus,
|
|
170
170
|
getExperimentStatusVariant,
|
|
171
171
|
resolveExperimentCode,
|
|
172
|
+
experimentIdFromRoute,
|
|
172
173
|
datePresetToISO,
|
|
173
174
|
EXPERIMENT_STATUS_OPTIONS,
|
|
174
175
|
EXPERIMENT_STATUS_VARIANT_MAP,
|
|
@@ -203,8 +204,6 @@ export {
|
|
|
203
204
|
export {
|
|
204
205
|
useAppExperiment,
|
|
205
206
|
APP_EXPERIMENT_KEY,
|
|
206
|
-
type AppExperimentRecord,
|
|
207
|
-
type AppExperimentSource,
|
|
208
207
|
type UseAppExperimentOptions,
|
|
209
208
|
type UseAppExperimentReturn,
|
|
210
209
|
type AppExperimentState,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ref,
|
|
3
3
|
computed,
|
|
4
|
-
readonly,
|
|
5
4
|
provide,
|
|
6
5
|
toValue,
|
|
7
6
|
watch,
|
|
@@ -11,21 +10,14 @@ import {
|
|
|
11
10
|
type InjectionKey,
|
|
12
11
|
} from 'vue'
|
|
13
12
|
import type { ExperimentSummary, ExperimentStatus } from '../types'
|
|
13
|
+
import { useExperimentStore } from '../stores/experiment'
|
|
14
14
|
import { resolveExperimentCode } from './experiment-utils'
|
|
15
15
|
|
|
16
|
-
export type AppExperimentRecord = Pick<ExperimentSummary, 'id' | 'name' | 'status' | 'experiment_code'>
|
|
17
|
-
|
|
18
|
-
export type AppExperimentSource =
|
|
19
|
-
| AppExperimentRecord
|
|
20
|
-
| null
|
|
21
|
-
| undefined
|
|
22
|
-
| Ref<AppExperimentRecord | null | undefined>
|
|
23
|
-
| ComputedRef<AppExperimentRecord | null | undefined>
|
|
24
|
-
| (() => AppExperimentRecord | null | undefined)
|
|
25
|
-
|
|
26
16
|
export interface UseAppExperimentOptions {
|
|
27
|
-
/**
|
|
28
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Called when the shared store adopts an experiment — a modal selection or a
|
|
19
|
+
* resolved deep link. The record always comes from the platform.
|
|
20
|
+
*/
|
|
29
21
|
onSelect?: (experiment: ExperimentSummary) => void | Promise<void>
|
|
30
22
|
onSave?: () => string | null | Promise<string | null>
|
|
31
23
|
onDetach?: () => void
|
|
@@ -37,6 +29,10 @@ export interface AppExperimentPopoverBinding {
|
|
|
37
29
|
experimentName?: string
|
|
38
30
|
experimentCode?: string
|
|
39
31
|
experimentStatus?: ExperimentStatus
|
|
32
|
+
/** An experiment is bound but its record has not arrived yet. */
|
|
33
|
+
isResolving: boolean
|
|
34
|
+
/** Resolution failed; the binding is kept so the user can retry or detach. */
|
|
35
|
+
error?: string
|
|
40
36
|
showSave: boolean
|
|
41
37
|
showDetach: boolean
|
|
42
38
|
saveDisabled: boolean
|
|
@@ -51,26 +47,27 @@ export interface AppExperimentSelectorModalBinding {
|
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
export interface UseAppExperimentReturn {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
50
|
+
experimentName: ComputedRef<string | undefined>
|
|
51
|
+
experimentCode: ComputedRef<string | undefined>
|
|
52
|
+
experimentStatus: ComputedRef<ExperimentStatus | undefined>
|
|
53
|
+
experimentId: ComputedRef<number | null>
|
|
54
|
+
isResolving: ComputedRef<boolean>
|
|
55
|
+
error: ComputedRef<string | null>
|
|
60
56
|
popover: ComputedRef<AppExperimentPopoverBinding>
|
|
61
57
|
selectorModal: ComputedRef<AppExperimentSelectorModalBinding>
|
|
62
58
|
openModal: () => void
|
|
63
59
|
closeModal: () => void
|
|
64
|
-
handleSelect: (experiment: ExperimentSummary) => void
|
|
65
60
|
handleSave: () => Promise<void>
|
|
66
61
|
handleDetach: () => void
|
|
67
62
|
}
|
|
68
63
|
|
|
69
64
|
export interface AppExperimentState {
|
|
70
|
-
experimentName:
|
|
71
|
-
experimentCode:
|
|
72
|
-
experimentStatus:
|
|
73
|
-
experimentId:
|
|
65
|
+
experimentName: ComputedRef<string | undefined>
|
|
66
|
+
experimentCode: ComputedRef<string | undefined>
|
|
67
|
+
experimentStatus: ComputedRef<ExperimentStatus | undefined>
|
|
68
|
+
experimentId: ComputedRef<number | null>
|
|
69
|
+
isResolving: ComputedRef<boolean>
|
|
70
|
+
error: ComputedRef<string | null>
|
|
74
71
|
showModal: Ref<boolean>
|
|
75
72
|
saveLoading: Ref<boolean>
|
|
76
73
|
saveSuccessMessage: Ref<string | undefined>
|
|
@@ -82,25 +79,41 @@ export interface AppExperimentState {
|
|
|
82
79
|
popover: ComputedRef<AppExperimentPopoverBinding>
|
|
83
80
|
selectorModal: ComputedRef<AppExperimentSelectorModalBinding>
|
|
84
81
|
openModal: () => void
|
|
85
|
-
handleSelect: (experiment: ExperimentSummary) => void
|
|
86
82
|
handleSave: () => Promise<void>
|
|
87
83
|
handleDetach: () => void
|
|
88
84
|
}
|
|
89
85
|
|
|
90
86
|
export const APP_EXPERIMENT_KEY: InjectionKey<AppExperimentState> = Symbol('app-experiment')
|
|
91
87
|
|
|
92
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Projects the current experiment from the shared store into an AppTopBar app
|
|
90
|
+
* shell, and owns the shell-local concerns the store has no opinion on: the
|
|
91
|
+
* selector modal's open state and the save flow.
|
|
92
|
+
*
|
|
93
|
+
* The composable never writes a selection of its own. `ExperimentSelectorModal`
|
|
94
|
+
* commits to the store and this reads back, so a displayed name can only ever
|
|
95
|
+
* have come from the platform.
|
|
96
|
+
*/
|
|
93
97
|
export function useAppExperiment(options: UseAppExperimentOptions = {}): UseAppExperimentReturn {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
const experimentStatus = ref<ExperimentStatus | undefined>()
|
|
97
|
-
const experimentId = ref<number | null>(null)
|
|
98
|
+
const store = useExperimentStore()
|
|
99
|
+
|
|
98
100
|
const showModal = ref(false)
|
|
99
101
|
const saveLoading = ref(false)
|
|
100
102
|
const saveSuccessMessage = ref<string | undefined>()
|
|
101
103
|
|
|
102
104
|
let successTimer: ReturnType<typeof setTimeout> | null = null
|
|
103
105
|
|
|
106
|
+
const experimentName = computed(() => store.current?.name)
|
|
107
|
+
const experimentCode = computed(() =>
|
|
108
|
+
store.current ? resolveExperimentCode(store.current) : undefined,
|
|
109
|
+
)
|
|
110
|
+
const experimentStatus = computed(() => store.current?.status)
|
|
111
|
+
// Set while resolving and after a failed resolve, so the shell can tell
|
|
112
|
+
// "nothing selected" apart from "selected but not resolved".
|
|
113
|
+
const experimentId = computed(() => store.currentId)
|
|
114
|
+
const isResolving = computed(() => store.isResolving)
|
|
115
|
+
const error = computed(() => store.error)
|
|
116
|
+
|
|
104
117
|
const showSave = ref(!!options.onSave)
|
|
105
118
|
const showDetach = computed(() => experimentId.value !== null)
|
|
106
119
|
const saveDisabled = computed(() => toValue(options.saveDisabled) ?? false)
|
|
@@ -109,6 +122,8 @@ export function useAppExperiment(options: UseAppExperimentOptions = {}): UseAppE
|
|
|
109
122
|
experimentName: experimentName.value,
|
|
110
123
|
experimentCode: experimentCode.value,
|
|
111
124
|
experimentStatus: experimentStatus.value,
|
|
125
|
+
isResolving: isResolving.value,
|
|
126
|
+
error: error.value ?? undefined,
|
|
112
127
|
showSave: showSave.value,
|
|
113
128
|
showDetach: showDetach.value,
|
|
114
129
|
saveDisabled: saveDisabled.value,
|
|
@@ -121,31 +136,12 @@ export function useAppExperiment(options: UseAppExperimentOptions = {}): UseAppE
|
|
|
121
136
|
currentExperimentId: experimentId.value,
|
|
122
137
|
}))
|
|
123
138
|
|
|
124
|
-
|
|
125
|
-
experimentId.value = experiment.id
|
|
126
|
-
experimentName.value = experiment.name
|
|
127
|
-
experimentCode.value = resolveExperimentCode(experiment)
|
|
128
|
-
experimentStatus.value = experiment.status
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function clear() {
|
|
132
|
-
experimentId.value = null
|
|
133
|
-
experimentName.value = undefined
|
|
134
|
-
experimentCode.value = undefined
|
|
135
|
-
experimentStatus.value = undefined
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (options.experiment !== undefined) {
|
|
139
|
+
if (options.onSelect) {
|
|
139
140
|
watch(
|
|
140
|
-
() =>
|
|
141
|
+
() => store.current,
|
|
141
142
|
(experiment) => {
|
|
142
|
-
if (experiment)
|
|
143
|
-
set(experiment)
|
|
144
|
-
} else {
|
|
145
|
-
clear()
|
|
146
|
-
}
|
|
143
|
+
if (experiment) void options.onSelect?.(experiment)
|
|
147
144
|
},
|
|
148
|
-
{ immediate: true },
|
|
149
145
|
)
|
|
150
146
|
}
|
|
151
147
|
|
|
@@ -157,12 +153,6 @@ export function useAppExperiment(options: UseAppExperimentOptions = {}): UseAppE
|
|
|
157
153
|
showModal.value = false
|
|
158
154
|
}
|
|
159
155
|
|
|
160
|
-
function handleSelect(experiment: ExperimentSummary) {
|
|
161
|
-
set(experiment)
|
|
162
|
-
showModal.value = false
|
|
163
|
-
options.onSelect?.(experiment)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
156
|
async function handleSave() {
|
|
167
157
|
if (!options.onSave || saveLoading.value) return
|
|
168
158
|
saveLoading.value = true
|
|
@@ -182,7 +172,7 @@ export function useAppExperiment(options: UseAppExperimentOptions = {}): UseAppE
|
|
|
182
172
|
}
|
|
183
173
|
|
|
184
174
|
function handleDetach() {
|
|
185
|
-
clear()
|
|
175
|
+
store.clear()
|
|
186
176
|
options.onDetach?.()
|
|
187
177
|
}
|
|
188
178
|
|
|
@@ -195,6 +185,8 @@ export function useAppExperiment(options: UseAppExperimentOptions = {}): UseAppE
|
|
|
195
185
|
experimentCode,
|
|
196
186
|
experimentStatus,
|
|
197
187
|
experimentId,
|
|
188
|
+
isResolving,
|
|
189
|
+
error,
|
|
198
190
|
showModal,
|
|
199
191
|
saveLoading,
|
|
200
192
|
saveSuccessMessage,
|
|
@@ -206,7 +198,6 @@ export function useAppExperiment(options: UseAppExperimentOptions = {}): UseAppE
|
|
|
206
198
|
selectorModal,
|
|
207
199
|
openModal,
|
|
208
200
|
closeModal,
|
|
209
|
-
handleSelect,
|
|
210
201
|
handleSave,
|
|
211
202
|
handleDetach,
|
|
212
203
|
}
|
|
@@ -214,17 +205,16 @@ export function useAppExperiment(options: UseAppExperimentOptions = {}): UseAppE
|
|
|
214
205
|
provide(APP_EXPERIMENT_KEY, state)
|
|
215
206
|
|
|
216
207
|
return {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
208
|
+
experimentName,
|
|
209
|
+
experimentCode,
|
|
210
|
+
experimentStatus,
|
|
211
|
+
experimentId,
|
|
212
|
+
isResolving,
|
|
213
|
+
error,
|
|
223
214
|
popover,
|
|
224
215
|
selectorModal,
|
|
225
216
|
openModal,
|
|
226
217
|
closeModal,
|
|
227
|
-
handleSelect,
|
|
228
218
|
handleSave,
|
|
229
219
|
handleDetach,
|
|
230
220
|
}
|
|
@@ -97,6 +97,7 @@ export function useExperimentSelector(
|
|
|
97
97
|
const error = request.error
|
|
98
98
|
const lastLoadedAt = request.lastLoadedAt
|
|
99
99
|
const page = ref(0)
|
|
100
|
+
let fetchGeneration = 0
|
|
100
101
|
|
|
101
102
|
// Sort: combined key like "created_at:desc"
|
|
102
103
|
const sortKey = ref<string>('created_at:desc')
|
|
@@ -201,9 +202,11 @@ export function useExperimentSelector(
|
|
|
201
202
|
return await api.get<ExperimentListResponse>(buildExperimentListUrl(typeFilter, skip, pageLimit))
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
async function fetchScopedExperimentPage(
|
|
205
|
+
async function fetchScopedExperimentPage(
|
|
206
|
+
requestedPage: number,
|
|
207
|
+
): Promise<ExperimentListResponse> {
|
|
205
208
|
const scope = resolveExperimentTypeScope()
|
|
206
|
-
const pageSkip =
|
|
209
|
+
const pageSkip = requestedPage * limit
|
|
207
210
|
|
|
208
211
|
if (scope.kind === 'blocked') {
|
|
209
212
|
return { experiments: [], total: 0 }
|
|
@@ -229,24 +232,39 @@ export function useExperimentSelector(
|
|
|
229
232
|
}
|
|
230
233
|
|
|
231
234
|
async function fetchExperiments(): Promise<void> {
|
|
235
|
+
const generation = ++fetchGeneration
|
|
236
|
+
const requestedPage = page.value
|
|
237
|
+
isLoading.value = true
|
|
238
|
+
request.clearError()
|
|
232
239
|
try {
|
|
233
|
-
await
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
} catch {
|
|
243
|
-
if (
|
|
240
|
+
const data = await fetchScopedExperimentPage(requestedPage)
|
|
241
|
+
if (generation !== fetchGeneration) return
|
|
242
|
+
if (requestedPage === 0) {
|
|
243
|
+
experiments.value = data.experiments
|
|
244
|
+
} else {
|
|
245
|
+
experiments.value = [...experiments.value, ...data.experiments]
|
|
246
|
+
}
|
|
247
|
+
total.value = data.total
|
|
248
|
+
request.markLoaded()
|
|
249
|
+
} catch (err) {
|
|
250
|
+
if (generation !== fetchGeneration) return
|
|
251
|
+
request.setError(err, 'Failed to fetch experiments')
|
|
252
|
+
if (requestedPage === 0) {
|
|
244
253
|
experiments.value = []
|
|
245
254
|
total.value = 0
|
|
246
255
|
}
|
|
256
|
+
} finally {
|
|
257
|
+
if (generation === fetchGeneration) {
|
|
258
|
+
isLoading.value = false
|
|
259
|
+
}
|
|
247
260
|
}
|
|
248
261
|
}
|
|
249
262
|
|
|
263
|
+
function invalidateFetch(): void {
|
|
264
|
+
fetchGeneration += 1
|
|
265
|
+
isLoading.value = false
|
|
266
|
+
}
|
|
267
|
+
|
|
250
268
|
async function fetchFilterOptions(): Promise<void> {
|
|
251
269
|
if (filterOptionsFetched) return
|
|
252
270
|
filterOptionsFetched = true
|
|
@@ -298,6 +316,7 @@ export function useExperimentSelector(
|
|
|
298
316
|
}
|
|
299
317
|
|
|
300
318
|
function clear(): void {
|
|
319
|
+
invalidateFetch()
|
|
301
320
|
selectedExperiment.value = null
|
|
302
321
|
filters.search = undefined
|
|
303
322
|
filters.status = undefined
|
|
@@ -329,6 +348,19 @@ export function useExperimentSelector(
|
|
|
329
348
|
})
|
|
330
349
|
})
|
|
331
350
|
|
|
351
|
+
watch(
|
|
352
|
+
() => [
|
|
353
|
+
filters.search,
|
|
354
|
+
filters.status,
|
|
355
|
+
filters.project,
|
|
356
|
+
filters.experimentType,
|
|
357
|
+
filters.datePreset,
|
|
358
|
+
sortKey.value,
|
|
359
|
+
],
|
|
360
|
+
() => invalidateFetch(),
|
|
361
|
+
{ flush: 'sync' },
|
|
362
|
+
)
|
|
363
|
+
|
|
332
364
|
const searchWatch = useDebouncedWatch(
|
|
333
365
|
() => filters.search,
|
|
334
366
|
() => {
|