@morscherlab/mld-sdk 0.9.6 → 0.9.8
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/components/ExperimentPopover.vue.d.ts +4 -0
- package/dist/components/ExperimentPopover.vue.js +31 -17
- package/dist/components/ExperimentPopover.vue.js.map +1 -1
- package/dist/components/FormulaInput.vue.js +24 -18
- package/dist/components/FormulaInput.vue.js.map +1 -1
- package/dist/components/MoleculeInput.vue.js +15 -6
- package/dist/components/MoleculeInput.vue.js.map +1 -1
- package/dist/components/PlateMapEditor.vue.js +1 -1
- package/dist/components/PlateMapEditor.vue.js.map +1 -1
- package/dist/composables/useAuth.js +26 -25
- package/dist/composables/useAuth.js.map +1 -1
- package/dist/composables/useAutoGroup.js +7 -32
- package/dist/composables/useAutoGroup.js.map +1 -1
- package/dist/composables/useForm.js +1 -1
- package/dist/composables/useForm.js.map +1 -1
- package/dist/composables/useWellPlateEditor.d.ts +1 -0
- package/dist/composables/useWellPlateEditor.js +21 -10
- package/dist/composables/useWellPlateEditor.js.map +1 -1
- package/dist/stores/settings.js +17 -30
- package/dist/stores/settings.js.map +1 -1
- package/dist/styles.css +68 -14
- package/package.json +1 -1
- package/src/components/ExperimentPopover.vue +16 -3
- package/src/components/FormulaInput.vue +17 -16
- package/src/components/MoleculeInput.vue +29 -14
- package/src/components/PlateMapEditor.vue +1 -1
- package/src/composables/useAuth.ts +29 -31
- package/src/composables/useAutoGroup.ts +7 -33
- package/src/composables/useForm.ts +1 -1
- package/src/composables/useWellPlateEditor.ts +22 -10
- package/src/stores/settings.ts +22 -38
- package/src/styles/components/experiment-popover.css +25 -1
- package/src/styles/components/formula-input.css +13 -6
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import axios from 'axios'
|
|
2
|
-
import { ref, onMounted, onUnmounted, watch, type Ref } from 'vue'
|
|
2
|
+
import { ref, onMounted, onUnmounted, watch, getCurrentInstance, type Ref } from 'vue'
|
|
3
3
|
import { useAuthStore } from '../stores/auth'
|
|
4
4
|
import { useSettingsStore } from '../stores/settings'
|
|
5
5
|
import type { AuthConfig, UserInfo, LoginResponse, TokenVerifyResponse, UpdateProfileRequest } from '../types'
|
|
@@ -66,8 +66,6 @@ export function useAuth(): UseAuthReturn {
|
|
|
66
66
|
const authStore = useAuthStore()
|
|
67
67
|
const settingsStore = useSettingsStore()
|
|
68
68
|
|
|
69
|
-
// Track refresh timer (module-level to prevent timer multiplication)
|
|
70
|
-
const refreshTimerId = ref<number | null>(_refreshTimerId)
|
|
71
69
|
const isRefreshing = ref(false)
|
|
72
70
|
|
|
73
71
|
function getApiBaseUrl(): string {
|
|
@@ -281,7 +279,7 @@ export function useAuth(): UseAuthReturn {
|
|
|
281
279
|
|
|
282
280
|
// Schedule refresh
|
|
283
281
|
const delay = refreshAt - now
|
|
284
|
-
|
|
282
|
+
_refreshTimerId = window.setTimeout(() => {
|
|
285
283
|
refreshToken()
|
|
286
284
|
}, delay)
|
|
287
285
|
}
|
|
@@ -290,9 +288,9 @@ export function useAuth(): UseAuthReturn {
|
|
|
290
288
|
* Stop automatic token refresh.
|
|
291
289
|
*/
|
|
292
290
|
function stopTokenRefresh(): void {
|
|
293
|
-
if (
|
|
294
|
-
window.clearTimeout(
|
|
295
|
-
|
|
291
|
+
if (_refreshTimerId !== null) {
|
|
292
|
+
window.clearTimeout(_refreshTimerId)
|
|
293
|
+
_refreshTimerId = null
|
|
296
294
|
}
|
|
297
295
|
}
|
|
298
296
|
|
|
@@ -382,34 +380,34 @@ export function useAuth(): UseAuthReturn {
|
|
|
382
380
|
}
|
|
383
381
|
}
|
|
384
382
|
|
|
385
|
-
// Set up periodic check as safety net
|
|
383
|
+
// Set up periodic check as safety net (only inside component setup)
|
|
386
384
|
let checkInterval: number | null = null
|
|
387
385
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
386
|
+
if (getCurrentInstance()) {
|
|
387
|
+
onMounted(() => {
|
|
388
|
+
checkInterval = window.setInterval(checkAndRefreshIfNeeded, TOKEN_REFRESH_CHECK_INTERVAL_MS)
|
|
389
|
+
})
|
|
392
390
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
checkInterval = null
|
|
399
|
-
}
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
// Watch for token changes to reschedule refresh
|
|
403
|
-
watch(
|
|
404
|
-
() => authStore.tokenExpires,
|
|
405
|
-
(newExpires) => {
|
|
406
|
-
if (newExpires) {
|
|
407
|
-
scheduleTokenRefresh()
|
|
408
|
-
} else {
|
|
409
|
-
stopTokenRefresh()
|
|
391
|
+
onUnmounted(() => {
|
|
392
|
+
stopTokenRefresh()
|
|
393
|
+
if (checkInterval !== null) {
|
|
394
|
+
window.clearInterval(checkInterval)
|
|
395
|
+
checkInterval = null
|
|
410
396
|
}
|
|
411
|
-
}
|
|
412
|
-
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
// Watch for token changes to reschedule refresh
|
|
400
|
+
watch(
|
|
401
|
+
() => authStore.tokenExpires,
|
|
402
|
+
(newExpires) => {
|
|
403
|
+
if (newExpires) {
|
|
404
|
+
scheduleTokenRefresh()
|
|
405
|
+
} else {
|
|
406
|
+
stopTokenRefresh()
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
)
|
|
410
|
+
}
|
|
413
411
|
|
|
414
412
|
return {
|
|
415
413
|
// Core auth methods
|
|
@@ -331,24 +331,9 @@ export function useAutoGroup() {
|
|
|
331
331
|
}))
|
|
332
332
|
})
|
|
333
333
|
|
|
334
|
-
const
|
|
334
|
+
const _computedResult = computed(() => {
|
|
335
335
|
if (effectiveColumns.value.length === 0 || enabledFields.value.size === 0) {
|
|
336
|
-
return []
|
|
337
|
-
}
|
|
338
|
-
const result = computeGroups(
|
|
339
|
-
samples.value,
|
|
340
|
-
effectiveColumns.value,
|
|
341
|
-
enabledFields.value,
|
|
342
|
-
outlierActions.value,
|
|
343
|
-
delimiter.value,
|
|
344
|
-
minFieldCount.value,
|
|
345
|
-
)
|
|
346
|
-
return result.groups
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
const metadata = computed(() => {
|
|
350
|
-
if (effectiveColumns.value.length === 0 || enabledFields.value.size === 0) {
|
|
351
|
-
return []
|
|
336
|
+
return { groups: [] as SampleGroup[], metadata: [] as MetadataRow[], excludedSamples: [] as string[] }
|
|
352
337
|
}
|
|
353
338
|
return computeGroups(
|
|
354
339
|
samples.value,
|
|
@@ -357,25 +342,14 @@ export function useAutoGroup() {
|
|
|
357
342
|
outlierActions.value,
|
|
358
343
|
delimiter.value,
|
|
359
344
|
minFieldCount.value,
|
|
360
|
-
)
|
|
345
|
+
)
|
|
361
346
|
})
|
|
362
347
|
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
effectiveColumns.value,
|
|
367
|
-
enabledFields.value,
|
|
368
|
-
outlierActions.value,
|
|
369
|
-
delimiter.value,
|
|
370
|
-
minFieldCount.value,
|
|
371
|
-
).excludedSamples
|
|
372
|
-
})
|
|
348
|
+
const groups = computed(() => _computedResult.value.groups)
|
|
349
|
+
const metadata = computed(() => _computedResult.value.metadata)
|
|
350
|
+
const excludedSamples = computed(() => _computedResult.value.excludedSamples)
|
|
373
351
|
|
|
374
|
-
const result = computed<AutoGroupResult>(() =>
|
|
375
|
-
groups: groups.value,
|
|
376
|
-
metadata: metadata.value,
|
|
377
|
-
excludedSamples: excludedSamples.value,
|
|
378
|
-
}))
|
|
352
|
+
const result = computed<AutoGroupResult>(() => _computedResult.value)
|
|
379
353
|
|
|
380
354
|
function parseInput() {
|
|
381
355
|
if (inputMode.value === 'csv' && csvData.value) {
|
|
@@ -354,7 +354,7 @@ export function useForm<T extends Record<string, unknown>>(
|
|
|
354
354
|
function reset(values?: Partial<T>): void {
|
|
355
355
|
const resetValues = values ? { ..._initialValues, ...values } : _initialValues
|
|
356
356
|
for (const key of Object.keys(data)) {
|
|
357
|
-
;(data as Record<string, unknown>)[key] = resetValues[key as keyof T]
|
|
357
|
+
;(data as Record<string, unknown>)[key] = structuredClone(resetValues[key as keyof T])
|
|
358
358
|
errors[key] = null
|
|
359
359
|
touched[key] = false
|
|
360
360
|
dirty[key] = false
|
|
@@ -52,6 +52,7 @@ export interface UseWellPlateEditorReturn {
|
|
|
52
52
|
redo: () => void
|
|
53
53
|
exportData: (format: 'json' | 'csv') => string
|
|
54
54
|
importData: (data: string, format: 'json' | 'csv') => boolean
|
|
55
|
+
loadState: (state: Partial<PlateMapEditorState>) => void
|
|
55
56
|
reset: () => void
|
|
56
57
|
}
|
|
57
58
|
|
|
@@ -103,14 +104,10 @@ export function useWellPlateEditor(
|
|
|
103
104
|
const canUndo = computed(() => historyIndex.value >= 0)
|
|
104
105
|
const canRedo = computed(() => historyIndex.value < history.value.length - 1)
|
|
105
106
|
|
|
106
|
-
function deepClone<T>(obj: T): T {
|
|
107
|
-
return structuredClone(obj)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
107
|
function saveToHistory() {
|
|
111
108
|
const entry: HistoryEntry = {
|
|
112
|
-
plates:
|
|
113
|
-
samples:
|
|
109
|
+
plates: structuredClone(internalState.value.plates),
|
|
110
|
+
samples: structuredClone(internalState.value.samples),
|
|
114
111
|
}
|
|
115
112
|
|
|
116
113
|
if (historyIndex.value < history.value.length - 1) {
|
|
@@ -257,8 +254,8 @@ export function useWellPlateEditor(
|
|
|
257
254
|
const entry = history.value[historyIndex.value]
|
|
258
255
|
historyIndex.value--
|
|
259
256
|
|
|
260
|
-
internalState.value.plates =
|
|
261
|
-
internalState.value.samples =
|
|
257
|
+
internalState.value.plates = structuredClone(entry.plates)
|
|
258
|
+
internalState.value.samples = structuredClone(entry.samples)
|
|
262
259
|
|
|
263
260
|
const activePlateExists = internalState.value.plates.some(p => p.id === internalState.value.activePlateId)
|
|
264
261
|
if (!activePlateExists) {
|
|
@@ -273,8 +270,8 @@ export function useWellPlateEditor(
|
|
|
273
270
|
historyIndex.value++
|
|
274
271
|
const entry = history.value[historyIndex.value]
|
|
275
272
|
|
|
276
|
-
internalState.value.plates =
|
|
277
|
-
internalState.value.samples =
|
|
273
|
+
internalState.value.plates = structuredClone(entry.plates)
|
|
274
|
+
internalState.value.samples = structuredClone(entry.samples)
|
|
278
275
|
internalState.value.selectedWells = []
|
|
279
276
|
}
|
|
280
277
|
|
|
@@ -368,6 +365,20 @@ export function useWellPlateEditor(
|
|
|
368
365
|
}
|
|
369
366
|
}
|
|
370
367
|
|
|
368
|
+
function loadState(state: Partial<PlateMapEditorState>) {
|
|
369
|
+
saveToHistory()
|
|
370
|
+
if (state.plates && state.plates.length > 0) {
|
|
371
|
+
internalState.value.plates = structuredClone(state.plates)
|
|
372
|
+
internalState.value.activePlateId = state.activePlateId ?? state.plates[0].id
|
|
373
|
+
}
|
|
374
|
+
if (state.samples) {
|
|
375
|
+
internalState.value.samples = structuredClone(state.samples)
|
|
376
|
+
}
|
|
377
|
+
internalState.value.selectedWells = state.selectedWells ?? []
|
|
378
|
+
internalState.value.activeSampleId = state.activeSampleId
|
|
379
|
+
updateSampleCounts()
|
|
380
|
+
}
|
|
381
|
+
|
|
371
382
|
function reset() {
|
|
372
383
|
const plate = createEmptyPlate('Plate 1', defaultFormat)
|
|
373
384
|
internalState.value = {
|
|
@@ -403,6 +414,7 @@ export function useWellPlateEditor(
|
|
|
403
414
|
redo,
|
|
404
415
|
exportData,
|
|
405
416
|
importData,
|
|
417
|
+
loadState,
|
|
406
418
|
reset,
|
|
407
419
|
}
|
|
408
420
|
}
|
package/src/stores/settings.ts
CHANGED
|
@@ -33,8 +33,12 @@ function getDefaultServerHost(): string {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function getDefaultServerPort(): number {
|
|
36
|
-
if (typeof window !== 'undefined'
|
|
37
|
-
|
|
36
|
+
if (typeof window !== 'undefined') {
|
|
37
|
+
if (window.location.port) {
|
|
38
|
+
return parseInt(window.location.port, 10)
|
|
39
|
+
}
|
|
40
|
+
// Standard ports: 443 for HTTPS, 80 for HTTP (browser omits from location.port)
|
|
41
|
+
return window.location.protocol === 'https:' ? 443 : 80
|
|
38
42
|
}
|
|
39
43
|
return 8000
|
|
40
44
|
}
|
|
@@ -92,34 +96,26 @@ export const useSettingsStore = defineStore('mld-settings', () => {
|
|
|
92
96
|
// API prefix - can be configured via env variable VITE_API_PREFIX
|
|
93
97
|
const apiPrefix: string = (import.meta.env?.VITE_API_PREFIX as string | undefined) ?? '/api'
|
|
94
98
|
|
|
95
|
-
function
|
|
99
|
+
function _isSameOrigin(): { same: boolean; currentHost: string; currentPort: string } {
|
|
96
100
|
const currentHost = typeof window !== 'undefined' ? window.location.hostname : 'localhost'
|
|
97
101
|
const currentPort = typeof window !== 'undefined' ? window.location.port : '8000'
|
|
102
|
+
const effectivePort = currentPort || (window.location.protocol === 'https:' ? '443' : '80')
|
|
103
|
+
const same =
|
|
104
|
+
(serverHost.value === currentHost && String(serverPort.value) === effectivePort) ||
|
|
105
|
+
(serverHost.value === 'localhost' && currentHost !== 'localhost')
|
|
106
|
+
return { same, currentHost, currentPort }
|
|
107
|
+
}
|
|
98
108
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (serverHost.value === 'localhost' && currentHost !== 'localhost') {
|
|
104
|
-
return apiPrefix
|
|
105
|
-
}
|
|
106
|
-
|
|
109
|
+
function getApiBaseUrl(): string {
|
|
110
|
+
const { same } = _isSameOrigin()
|
|
111
|
+
if (same) return apiPrefix
|
|
107
112
|
return `http://${serverHost.value}:${serverPort.value}${apiPrefix}`
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
function getWsBaseUrl(): string {
|
|
111
|
-
const
|
|
112
|
-
const currentPort = typeof window !== 'undefined' ? window.location.port : '8000'
|
|
116
|
+
const { same, currentHost, currentPort } = _isSameOrigin()
|
|
113
117
|
const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
114
|
-
|
|
115
|
-
if (serverHost.value === currentHost && String(serverPort.value) === currentPort) {
|
|
116
|
-
return `${protocol}//${currentHost}:${currentPort}${apiPrefix}`
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (serverHost.value === 'localhost' && currentHost !== 'localhost') {
|
|
120
|
-
return `${protocol}//${currentHost}:${currentPort}${apiPrefix}`
|
|
121
|
-
}
|
|
122
|
-
|
|
118
|
+
if (same) return `${protocol}//${currentHost}${currentPort ? ':' + currentPort : ''}${apiPrefix}`
|
|
123
119
|
return `ws://${serverHost.value}:${serverPort.value}${apiPrefix}`
|
|
124
120
|
}
|
|
125
121
|
|
|
@@ -138,19 +134,10 @@ export const useSettingsStore = defineStore('mld-settings', () => {
|
|
|
138
134
|
}
|
|
139
135
|
|
|
140
136
|
function applyTheme() {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
} else {
|
|
146
|
-
isDark = theme.value === 'dark'
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (isDark) {
|
|
150
|
-
document.documentElement.classList.add('dark')
|
|
151
|
-
} else {
|
|
152
|
-
document.documentElement.classList.remove('dark')
|
|
153
|
-
}
|
|
137
|
+
const dark = theme.value === 'system'
|
|
138
|
+
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
139
|
+
: theme.value === 'dark'
|
|
140
|
+
document.documentElement.classList.toggle('dark', dark)
|
|
154
141
|
}
|
|
155
142
|
|
|
156
143
|
watch(theme, () => {
|
|
@@ -206,7 +193,6 @@ export const useSettingsStore = defineStore('mld-settings', () => {
|
|
|
206
193
|
}
|
|
207
194
|
|
|
208
195
|
return {
|
|
209
|
-
// State
|
|
210
196
|
serverHost,
|
|
211
197
|
serverPort,
|
|
212
198
|
requestTimeout,
|
|
@@ -215,8 +201,6 @@ export const useSettingsStore = defineStore('mld-settings', () => {
|
|
|
215
201
|
theme,
|
|
216
202
|
colorPalette,
|
|
217
203
|
tableDensity,
|
|
218
|
-
|
|
219
|
-
// Actions
|
|
220
204
|
initialize,
|
|
221
205
|
applyTheme,
|
|
222
206
|
persistSettings,
|
|
@@ -181,8 +181,15 @@
|
|
|
181
181
|
color: var(--text-muted, var(--mld-text-muted, #94a3b8));
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
.mld-experiment-
|
|
184
|
+
.mld-experiment-popover__card-actions {
|
|
185
|
+
display: flex;
|
|
186
|
+
flex-direction: column;
|
|
187
|
+
align-items: flex-end;
|
|
188
|
+
gap: 0.125rem;
|
|
185
189
|
flex-shrink: 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.mld-experiment-popover__change-btn {
|
|
186
193
|
padding: 0.25rem 0.375rem;
|
|
187
194
|
border: none;
|
|
188
195
|
background: transparent;
|
|
@@ -198,6 +205,23 @@
|
|
|
198
205
|
background: rgba(99, 102, 241, 0.08);
|
|
199
206
|
}
|
|
200
207
|
|
|
208
|
+
.mld-experiment-popover__detach-btn {
|
|
209
|
+
padding: 0.25rem 0.375rem;
|
|
210
|
+
border: none;
|
|
211
|
+
background: transparent;
|
|
212
|
+
border-radius: 0.25rem;
|
|
213
|
+
color: var(--text-muted, var(--mld-text-muted, #94a3b8));
|
|
214
|
+
font-size: 0.6875rem;
|
|
215
|
+
font-weight: 400;
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
transition: color 0.15s ease, background-color 0.15s ease;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.mld-experiment-popover__detach-btn:hover {
|
|
221
|
+
color: #ef4444;
|
|
222
|
+
background: rgba(239, 68, 68, 0.08);
|
|
223
|
+
}
|
|
224
|
+
|
|
201
225
|
/* Divider */
|
|
202
226
|
.mld-experiment-popover__divider {
|
|
203
227
|
height: 1px;
|
|
@@ -65,22 +65,29 @@
|
|
|
65
65
|
background-color: var(--bg-tertiary);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/* Preview */
|
|
68
|
+
/* Preview row */
|
|
69
69
|
.mld-formula-input__preview {
|
|
70
|
-
padding: 0.
|
|
70
|
+
padding: 0.25rem 0.75rem;
|
|
71
71
|
background-color: var(--bg-tertiary);
|
|
72
72
|
border-top: 1px solid var(--border-color);
|
|
73
|
-
|
|
74
|
-
min-height: 1.75rem;
|
|
73
|
+
min-height: 1.5rem;
|
|
75
74
|
display: flex;
|
|
76
75
|
align-items: center;
|
|
76
|
+
justify-content: space-between;
|
|
77
|
+
gap: 0.75rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.mld-formula-input__preview-formula {
|
|
81
|
+
font-size: 1rem;
|
|
82
|
+
line-height: 1.4;
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
/* Molecular weight display */
|
|
80
86
|
.mld-formula-input__mw {
|
|
81
|
-
font-size: 0.
|
|
87
|
+
font-size: 0.6875rem;
|
|
82
88
|
color: var(--text-muted);
|
|
83
|
-
|
|
89
|
+
white-space: nowrap;
|
|
90
|
+
flex-shrink: 0;
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
.mld-formula-input__mw::before {
|