@morscherlab/mint-sdk 1.0.0-beta.7 → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/composables/useProtocolTemplates.test.d.ts +1 -0
- package/dist/__tests__/stores/settings.test.d.ts +1 -0
- package/dist/{auth-QQj2kkze.js → auth-CBG3bWEc.js} +50 -20
- package/dist/auth-CBG3bWEc.js.map +1 -0
- package/dist/components/SettingsModal.vue.d.ts +5 -0
- package/dist/components/index.js +2 -2
- package/dist/{components-DihbSJjU.js → components-5KSfsVqf.js} +49 -29
- package/dist/components-5KSfsVqf.js.map +1 -0
- package/dist/composables/index.js +3 -3
- package/dist/{composables-BcgZ6diz.js → composables-D4Myb30a.js} +3 -3
- package/dist/{composables-BcgZ6diz.js.map → composables-D4Myb30a.js.map} +1 -1
- package/dist/index.js +5 -5
- package/dist/install.js +2 -2
- package/dist/stores/index.js +1 -1
- package/dist/styles.css +16 -0
- package/dist/templates/index.js +1 -1
- package/dist/{templates-Cyt0Suwf.js → templates-BSlxwV2c.js} +12 -8
- package/dist/templates-BSlxwV2c.js.map +1 -0
- package/dist/{useExperimentData-CM6Y0u5L.js → useExperimentData-BbbdI5xT.js} +97 -25
- package/dist/useExperimentData-BbbdI5xT.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/GroupAssigner.test.ts +18 -0
- package/src/__tests__/composables/useApi.test.ts +45 -0
- package/src/__tests__/composables/useAuth.test.ts +20 -0
- package/src/__tests__/composables/useProtocolTemplates.test.ts +64 -0
- package/src/__tests__/stores/settings.test.ts +78 -0
- package/src/components/AppAvatarMenu.vue +6 -3
- package/src/components/AppTopBar.vue +15 -10
- package/src/components/AuditTrail.vue +1 -1
- package/src/components/Calendar.vue +6 -2
- package/src/components/ConcentrationInput.vue +3 -2
- package/src/components/GroupAssigner.vue +8 -3
- package/src/components/NumberInput.vue +5 -3
- package/src/components/SampleHierarchyTree.vue +3 -2
- package/src/components/SettingsModal.vue +7 -0
- package/src/components/UnitInput.vue +6 -2
- package/src/components/WellPlate.vue +3 -3
- package/src/composables/useApi.ts +113 -16
- package/src/composables/useAutoGroup.ts +13 -8
- package/src/composables/useProtocolTemplates.ts +13 -1
- package/src/composables/useRackEditor.ts +3 -2
- package/src/stores/auth.ts +48 -23
- package/src/stores/settings.ts +10 -0
- package/src/styles/components/settings-modal.css +9 -0
- package/dist/auth-QQj2kkze.js.map +0 -1
- package/dist/components-DihbSJjU.js.map +0 -1
- package/dist/templates-Cyt0Suwf.js.map +0 -1
- package/dist/useExperimentData-CM6Y0u5L.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morscherlab/mint-sdk",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-rc.1",
|
|
4
4
|
"description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -27,4 +27,22 @@ describe('GroupAssigner', () => {
|
|
|
27
27
|
expect(wrapper.emitted('update:group1')?.[0]).toEqual([[]])
|
|
28
28
|
expect(wrapper.emitted('update:group2')).toBeUndefined()
|
|
29
29
|
})
|
|
30
|
+
|
|
31
|
+
it('does not throw when drag events omit dataTransfer', async () => {
|
|
32
|
+
const wrapper = mount(GroupAssigner, {
|
|
33
|
+
props: {
|
|
34
|
+
groups,
|
|
35
|
+
group1: [],
|
|
36
|
+
group2: [],
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
await expect(
|
|
41
|
+
wrapper.find('.mint-group-assigner__pill--unassigned').trigger('dragstart', { dataTransfer: null }),
|
|
42
|
+
).resolves.toBeUndefined()
|
|
43
|
+
|
|
44
|
+
await expect(
|
|
45
|
+
wrapper.find('.mint-group-assigner__zone').trigger('dragover', { dataTransfer: null }),
|
|
46
|
+
).resolves.toBeUndefined()
|
|
47
|
+
})
|
|
30
48
|
})
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
2
|
import { createPinia, setActivePinia } from 'pinia'
|
|
3
|
+
import type { InternalAxiosRequestConfig } from 'axios'
|
|
3
4
|
|
|
4
5
|
import { useApi } from '../../composables/useApi'
|
|
6
|
+
import { useAuthStore } from '../../stores/auth'
|
|
7
|
+
|
|
8
|
+
function readAuthorizationHeader(headers: InternalAxiosRequestConfig['headers']): unknown {
|
|
9
|
+
const bag = headers as Record<string, unknown> & { get?: (header: string) => unknown }
|
|
10
|
+
return (typeof bag.get === 'function' ? bag.get('Authorization') : undefined)
|
|
11
|
+
?? bag.Authorization
|
|
12
|
+
?? bag.authorization
|
|
13
|
+
}
|
|
5
14
|
|
|
6
15
|
describe('useApi URL helpers', () => {
|
|
7
16
|
beforeEach(() => {
|
|
@@ -9,6 +18,7 @@ describe('useApi URL helpers', () => {
|
|
|
9
18
|
})
|
|
10
19
|
|
|
11
20
|
afterEach(() => {
|
|
21
|
+
vi.restoreAllMocks()
|
|
12
22
|
vi.unstubAllEnvs()
|
|
13
23
|
})
|
|
14
24
|
|
|
@@ -27,4 +37,39 @@ describe('useApi URL helpers', () => {
|
|
|
27
37
|
expect(api.buildWsUrl('/events')).toMatch(/\/api\/events$/)
|
|
28
38
|
expect(api.buildWsUrl('/events')).not.toContain('/api//events')
|
|
29
39
|
})
|
|
40
|
+
|
|
41
|
+
it('normalizes paths that already include the configured API prefix', async () => {
|
|
42
|
+
const api = useApi({ baseUrl: '/api' })
|
|
43
|
+
const getSpy = vi.spyOn(api.client, 'get').mockResolvedValue({ data: { ok: true } })
|
|
44
|
+
|
|
45
|
+
expect(api.buildUrl('/api/projects')).toBe('/api/projects')
|
|
46
|
+
expect(api.buildUrl('/api')).toBe('/api')
|
|
47
|
+
expect(api.buildUrl('/api?active=true')).toBe('/api?active=true')
|
|
48
|
+
|
|
49
|
+
await expect(api.get<{ ok: boolean }>('/api/projects')).resolves.toEqual({ ok: true })
|
|
50
|
+
expect(getSpy).toHaveBeenCalledWith('/projects', expect.objectContaining({ baseURL: '/api' }))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('honors withAuth=false even when the SDK auth store has a token', async () => {
|
|
54
|
+
const authStore = useAuthStore()
|
|
55
|
+
authStore.setToken('secret-token', 3600)
|
|
56
|
+
const api = useApi({ baseUrl: '/api', withAuth: false })
|
|
57
|
+
|
|
58
|
+
let authorization: unknown
|
|
59
|
+
const result = await api.get<{ ok: boolean }>('/public', {
|
|
60
|
+
adapter: async (config) => {
|
|
61
|
+
authorization = readAuthorizationHeader(config.headers)
|
|
62
|
+
return {
|
|
63
|
+
data: { ok: true },
|
|
64
|
+
status: 200,
|
|
65
|
+
statusText: 'OK',
|
|
66
|
+
headers: {},
|
|
67
|
+
config,
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
expect(result).toEqual({ ok: true })
|
|
73
|
+
expect(authorization).toBeUndefined()
|
|
74
|
+
})
|
|
30
75
|
})
|
|
@@ -94,6 +94,26 @@ describe('useAuth', () => {
|
|
|
94
94
|
})
|
|
95
95
|
})
|
|
96
96
|
|
|
97
|
+
describe('storage fallback', () => {
|
|
98
|
+
it('keeps auth usable when localStorage operations throw', () => {
|
|
99
|
+
vi.stubGlobal('localStorage', {
|
|
100
|
+
getItem: vi.fn(() => { throw new Error('blocked') }),
|
|
101
|
+
setItem: vi.fn(() => { throw new Error('blocked') }),
|
|
102
|
+
removeItem: vi.fn(() => { throw new Error('blocked') }),
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const authStore = useAuthStore()
|
|
106
|
+
|
|
107
|
+
expect(() => authStore.initialize()).not.toThrow()
|
|
108
|
+
|
|
109
|
+
authStore.setToken('memory-token', 3600)
|
|
110
|
+
expect(authStore.token).toBe('memory-token')
|
|
111
|
+
|
|
112
|
+
authStore.clearToken()
|
|
113
|
+
expect(authStore.token).toBeNull()
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
97
117
|
describe('refreshToken - race condition', () => {
|
|
98
118
|
it('should deduplicate concurrent refresh calls', async () => {
|
|
99
119
|
const authStore = useAuthStore()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { useProtocolTemplates } from '../../composables/useProtocolTemplates'
|
|
3
|
+
|
|
4
|
+
const STORAGE_KEY = 'mint-custom-protocol-templates'
|
|
5
|
+
|
|
6
|
+
function installLocalStorage() {
|
|
7
|
+
const storage: Record<string, string> = {}
|
|
8
|
+
vi.stubGlobal('localStorage', {
|
|
9
|
+
get length() {
|
|
10
|
+
return Object.keys(storage).length
|
|
11
|
+
},
|
|
12
|
+
key: vi.fn((index: number) => Object.keys(storage)[index] ?? null),
|
|
13
|
+
getItem: vi.fn((key: string) => storage[key] ?? null),
|
|
14
|
+
setItem: vi.fn((key: string, value: string) => {
|
|
15
|
+
storage[key] = value
|
|
16
|
+
}),
|
|
17
|
+
removeItem: vi.fn((key: string) => {
|
|
18
|
+
delete storage[key]
|
|
19
|
+
}),
|
|
20
|
+
clear: vi.fn(() => {
|
|
21
|
+
for (const key of Object.keys(storage)) delete storage[key]
|
|
22
|
+
}),
|
|
23
|
+
} as unknown as Storage)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('useProtocolTemplates', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.restoreAllMocks()
|
|
29
|
+
vi.unstubAllGlobals()
|
|
30
|
+
installLocalStorage()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('ignores corrupted custom template storage', () => {
|
|
34
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify({ invalid: true }))
|
|
35
|
+
|
|
36
|
+
const templates = useProtocolTemplates()
|
|
37
|
+
|
|
38
|
+
expect(templates.customTemplates.value).toEqual([])
|
|
39
|
+
expect(templates.allTemplates.value.length).toBeGreaterThan(0)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('filters invalid custom template entries', () => {
|
|
43
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify([
|
|
44
|
+
{ id: 'bad', name: 'Missing parameters', type: 'custom' },
|
|
45
|
+
{
|
|
46
|
+
id: 'custom-valid',
|
|
47
|
+
name: 'Valid custom step',
|
|
48
|
+
type: 'custom',
|
|
49
|
+
parameters: [],
|
|
50
|
+
},
|
|
51
|
+
]))
|
|
52
|
+
|
|
53
|
+
const templates = useProtocolTemplates()
|
|
54
|
+
|
|
55
|
+
expect(templates.customTemplates.value).toEqual([
|
|
56
|
+
{
|
|
57
|
+
id: 'custom-valid',
|
|
58
|
+
name: 'Valid custom step',
|
|
59
|
+
type: 'custom',
|
|
60
|
+
parameters: [],
|
|
61
|
+
},
|
|
62
|
+
])
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { createPinia, setActivePinia } from 'pinia'
|
|
3
|
+
import { useSettingsStore } from '../../stores/settings'
|
|
4
|
+
|
|
5
|
+
function mockMatchMedia(matches = false) {
|
|
6
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
7
|
+
configurable: true,
|
|
8
|
+
writable: true,
|
|
9
|
+
value: vi.fn(() => ({
|
|
10
|
+
matches,
|
|
11
|
+
media: '(prefers-color-scheme: dark)',
|
|
12
|
+
onchange: null,
|
|
13
|
+
addEventListener: vi.fn(),
|
|
14
|
+
removeEventListener: vi.fn(),
|
|
15
|
+
addListener: vi.fn(),
|
|
16
|
+
removeListener: vi.fn(),
|
|
17
|
+
dispatchEvent: vi.fn(),
|
|
18
|
+
})),
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function installLocalStorage() {
|
|
23
|
+
const storage: Record<string, string> = {}
|
|
24
|
+
vi.stubGlobal('localStorage', {
|
|
25
|
+
get length() {
|
|
26
|
+
return Object.keys(storage).length
|
|
27
|
+
},
|
|
28
|
+
key: vi.fn((index: number) => Object.keys(storage)[index] ?? null),
|
|
29
|
+
getItem: vi.fn((key: string) => storage[key] ?? null),
|
|
30
|
+
setItem: vi.fn((key: string, value: string) => {
|
|
31
|
+
storage[key] = value
|
|
32
|
+
}),
|
|
33
|
+
removeItem: vi.fn((key: string) => {
|
|
34
|
+
delete storage[key]
|
|
35
|
+
}),
|
|
36
|
+
clear: vi.fn(() => {
|
|
37
|
+
for (const key of Object.keys(storage)) delete storage[key]
|
|
38
|
+
}),
|
|
39
|
+
} as unknown as Storage)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('useSettingsStore', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.restoreAllMocks()
|
|
45
|
+
vi.unstubAllGlobals()
|
|
46
|
+
setActivePinia(createPinia())
|
|
47
|
+
installLocalStorage()
|
|
48
|
+
document.documentElement.classList.remove('dark')
|
|
49
|
+
mockMatchMedia(false)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('migrates legacy MLD settings into the MINT settings key', () => {
|
|
53
|
+
localStorage.setItem('mld-settings', JSON.stringify({
|
|
54
|
+
serverHost: '127.0.0.1',
|
|
55
|
+
serverPort: 8001,
|
|
56
|
+
theme: 'dark',
|
|
57
|
+
colorPalette: 'viridis',
|
|
58
|
+
tableDensity: 'compact',
|
|
59
|
+
}))
|
|
60
|
+
|
|
61
|
+
const settings = useSettingsStore()
|
|
62
|
+
settings.initialize()
|
|
63
|
+
|
|
64
|
+
expect(settings.serverHost).toBe('127.0.0.1')
|
|
65
|
+
expect(settings.serverPort).toBe(8001)
|
|
66
|
+
expect(settings.theme).toBe('dark')
|
|
67
|
+
expect(settings.colorPalette).toBe('viridis')
|
|
68
|
+
expect(settings.tableDensity).toBe('compact')
|
|
69
|
+
expect(localStorage.getItem('mld-settings')).toBeNull()
|
|
70
|
+
expect(JSON.parse(localStorage.getItem('mint-settings') || '{}')).toEqual(expect.objectContaining({
|
|
71
|
+
serverHost: '127.0.0.1',
|
|
72
|
+
serverPort: 8001,
|
|
73
|
+
theme: 'dark',
|
|
74
|
+
colorPalette: 'viridis',
|
|
75
|
+
tableDensity: 'compact',
|
|
76
|
+
}))
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -35,9 +35,12 @@ const { isOpen, rootRef, close, toggle } = useDropdownState({
|
|
|
35
35
|
const initials = computed(() => {
|
|
36
36
|
if (props.userInitial) return props.userInitial.slice(0, 2).toUpperCase()
|
|
37
37
|
if (props.userName) {
|
|
38
|
-
const parts = props.userName.trim().split(/\s+/)
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const parts = props.userName.trim().split(/\s+/).filter(Boolean)
|
|
39
|
+
const first = parts[0]
|
|
40
|
+
if (!first) return 'U'
|
|
41
|
+
const second = parts[1]
|
|
42
|
+
if (second) return (first.charAt(0) + second.charAt(0)).toUpperCase()
|
|
43
|
+
return first.slice(0, 2).toUpperCase()
|
|
41
44
|
}
|
|
42
45
|
return 'U'
|
|
43
46
|
})
|
|
@@ -117,7 +117,12 @@ const { isIntegrated, plugin } = usePlatformContext()
|
|
|
117
117
|
const isStandalone = computed(() => !isIntegrated.value)
|
|
118
118
|
const appExperiment = inject(APP_EXPERIMENT_KEY, null)
|
|
119
119
|
|
|
120
|
-
const
|
|
120
|
+
const pluginIcon = computed(() => {
|
|
121
|
+
const currentPlugin = plugin.value
|
|
122
|
+
return currentPlugin?.icon
|
|
123
|
+
? { icon: currentPlugin.icon, color: currentPlugin.color }
|
|
124
|
+
: null
|
|
125
|
+
})
|
|
121
126
|
|
|
122
127
|
const profileInitial = computed(() => {
|
|
123
128
|
if (props.userInitial) return props.userInitial
|
|
@@ -225,10 +230,10 @@ function currentItemIdFromLocation(pages: Array<Pick<PageSelectorItem, 'id' | 't
|
|
|
225
230
|
>
|
|
226
231
|
<slot name="icon">
|
|
227
232
|
<PluginIcon
|
|
228
|
-
v-if="
|
|
233
|
+
v-if="pluginIcon"
|
|
229
234
|
class="mint-topbar__plugin-icon"
|
|
230
|
-
:icon="
|
|
231
|
-
:tone="
|
|
235
|
+
:icon="pluginIcon.icon"
|
|
236
|
+
:tone="pluginIcon.color"
|
|
232
237
|
size="md"
|
|
233
238
|
variant="solid"
|
|
234
239
|
/>
|
|
@@ -248,10 +253,10 @@ function currentItemIdFromLocation(pages: Array<Pick<PageSelectorItem, 'id' | 't
|
|
|
248
253
|
>
|
|
249
254
|
<slot name="icon">
|
|
250
255
|
<PluginIcon
|
|
251
|
-
v-if="
|
|
256
|
+
v-if="pluginIcon"
|
|
252
257
|
class="mint-topbar__plugin-icon"
|
|
253
|
-
:icon="
|
|
254
|
-
:tone="
|
|
258
|
+
:icon="pluginIcon.icon"
|
|
259
|
+
:tone="pluginIcon.color"
|
|
255
260
|
size="md"
|
|
256
261
|
variant="solid"
|
|
257
262
|
/>
|
|
@@ -267,10 +272,10 @@ function currentItemIdFromLocation(pages: Array<Pick<PageSelectorItem, 'id' | 't
|
|
|
267
272
|
<template v-else>
|
|
268
273
|
<slot name="icon">
|
|
269
274
|
<PluginIcon
|
|
270
|
-
v-if="
|
|
275
|
+
v-if="pluginIcon"
|
|
271
276
|
class="mint-topbar__plugin-icon"
|
|
272
|
-
:icon="
|
|
273
|
-
:tone="
|
|
277
|
+
:icon="pluginIcon.icon"
|
|
278
|
+
:tone="pluginIcon.color"
|
|
274
279
|
size="md"
|
|
275
280
|
variant="solid"
|
|
276
281
|
/>
|
|
@@ -32,7 +32,7 @@ const uniqueTypes = computed(() => {
|
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
const uniqueUsers = computed(() => {
|
|
35
|
-
const users = new Set(props.entries.
|
|
35
|
+
const users = new Set(props.entries.flatMap(e => e.user ? [e.user] : []))
|
|
36
36
|
return Array.from(users)
|
|
37
37
|
})
|
|
38
38
|
|
|
@@ -134,8 +134,12 @@ const markerMap = computed(() => {
|
|
|
134
134
|
for (const marker of props.markers) {
|
|
135
135
|
const date = parseDate(marker.date)
|
|
136
136
|
const key = toDateKey(date)
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
const markers = map.get(key)
|
|
138
|
+
if (markers) {
|
|
139
|
+
markers.push(marker)
|
|
140
|
+
} else {
|
|
141
|
+
map.set(key, [marker])
|
|
142
|
+
}
|
|
139
143
|
}
|
|
140
144
|
return map
|
|
141
145
|
})
|
|
@@ -37,13 +37,14 @@ const { unitCategories, getConversionHint } = useConcentrationUnits()
|
|
|
37
37
|
|
|
38
38
|
// Filter categories based on allowedUnits
|
|
39
39
|
const filteredCategories = computed(() => {
|
|
40
|
-
|
|
40
|
+
const allowedUnits = props.allowedUnits
|
|
41
|
+
if (!allowedUnits || allowedUnits.length === 0) {
|
|
41
42
|
return unitCategories.value
|
|
42
43
|
}
|
|
43
44
|
return unitCategories.value
|
|
44
45
|
.map(cat => ({
|
|
45
46
|
label: cat.label,
|
|
46
|
-
units: cat.units.filter(u =>
|
|
47
|
+
units: cat.units.filter(u => allowedUnits.includes(u)),
|
|
47
48
|
}))
|
|
48
49
|
.filter(cat => cat.units.length > 0)
|
|
49
50
|
})
|
|
@@ -42,9 +42,12 @@ const assignment = useGroupAssignment({
|
|
|
42
42
|
const { unassignedGroups, zone1Groups, zone2Groups, zone1Count, zone2Count, validationMessage } = assignment
|
|
43
43
|
|
|
44
44
|
function handleDragStart(event: DragEvent, groupName: string) {
|
|
45
|
+
const transfer = event.dataTransfer
|
|
46
|
+
if (!transfer) return
|
|
47
|
+
|
|
45
48
|
draggingGroup.value = groupName
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
transfer.setData('groupName', groupName)
|
|
50
|
+
transfer.effectAllowed = 'move'
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
function handleDragEnd() {
|
|
@@ -54,7 +57,9 @@ function handleDragEnd() {
|
|
|
54
57
|
|
|
55
58
|
function handleDragOver(event: DragEvent, zone: GroupAssignmentZone) {
|
|
56
59
|
event.preventDefault()
|
|
57
|
-
event.dataTransfer
|
|
60
|
+
if (event.dataTransfer) {
|
|
61
|
+
event.dataTransfer.dropEffect = 'move'
|
|
62
|
+
}
|
|
58
63
|
dragOverZone.value = zone
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -28,10 +28,12 @@ const emit = defineEmits<{
|
|
|
28
28
|
const isSliderMode = computed(() => props.min !== undefined && props.max !== undefined)
|
|
29
29
|
|
|
30
30
|
const sliderPercent = computed(() => {
|
|
31
|
-
|
|
32
|
-
const
|
|
31
|
+
const min = props.min
|
|
32
|
+
const max = props.max
|
|
33
|
+
if (min === undefined || max === undefined || props.modelValue === undefined) return 0
|
|
34
|
+
const range = max - min
|
|
33
35
|
if (range === 0) return 0
|
|
34
|
-
return ((props.modelValue -
|
|
36
|
+
return ((props.modelValue - min) / range) * 100
|
|
35
37
|
})
|
|
36
38
|
|
|
37
39
|
const canDecrement = computed(() => {
|
|
@@ -149,6 +149,7 @@ function canShowChildren(node: TreeNode, depth: number): boolean {
|
|
|
149
149
|
|
|
150
150
|
// Render tree node recursively
|
|
151
151
|
function renderNode(node: TreeNode, depth: number): VNode {
|
|
152
|
+
const children = node.children
|
|
152
153
|
const expanded = isExpanded(node.id)
|
|
153
154
|
const canExpand = hasChildren(node)
|
|
154
155
|
const showChildNodes = canShowChildren(node, depth)
|
|
@@ -197,11 +198,11 @@ function renderNode(node: TreeNode, depth: number): VNode {
|
|
|
197
198
|
)
|
|
198
199
|
|
|
199
200
|
const childNodes =
|
|
200
|
-
showChildNodes &&
|
|
201
|
+
showChildNodes && children
|
|
201
202
|
? h(
|
|
202
203
|
Transition,
|
|
203
204
|
{ enterActiveClass: 'mint-sample-tree__children--entering', leaveActiveClass: 'mint-sample-tree__children--leaving' },
|
|
204
|
-
() => h('div', { class: 'mint-sample-tree__children' },
|
|
205
|
+
() => h('div', { class: 'mint-sample-tree__children' }, children.map((child) => renderNode(child, depth + 1)))
|
|
205
206
|
)
|
|
206
207
|
: null
|
|
207
208
|
|
|
@@ -416,6 +416,13 @@ function isControlModelBinding(model: ControlModel | ControlModelBinding): model
|
|
|
416
416
|
</div>
|
|
417
417
|
</component>
|
|
418
418
|
</div>
|
|
419
|
+
<div v-if="$slots.footer" class="mint-settings-modal__footer">
|
|
420
|
+
<slot
|
|
421
|
+
name="footer"
|
|
422
|
+
:values="builder.form.data"
|
|
423
|
+
:close="handleClose"
|
|
424
|
+
/>
|
|
425
|
+
</div>
|
|
419
426
|
</BaseModal>
|
|
420
427
|
</template>
|
|
421
428
|
|
|
@@ -47,8 +47,12 @@ const groupedUnits = computed(() => {
|
|
|
47
47
|
const ungrouped: UnitOption[] = []
|
|
48
48
|
for (const u of props.units) {
|
|
49
49
|
if (u.group) {
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const group = groups.get(u.group)
|
|
51
|
+
if (group) {
|
|
52
|
+
group.push(u)
|
|
53
|
+
} else {
|
|
54
|
+
groups.set(u.group, [u])
|
|
55
|
+
}
|
|
52
56
|
} else {
|
|
53
57
|
ungrouped.push(u)
|
|
54
58
|
}
|
|
@@ -759,10 +759,10 @@ const tableStyle = computed(() => ({
|
|
|
759
759
|
<span
|
|
760
760
|
v-if="getWellBadge(well)"
|
|
761
761
|
class="mint-well-plate__badge"
|
|
762
|
-
:style="{ backgroundColor: getWellBadge(well)
|
|
763
|
-
:title="getWellBadge(well)
|
|
762
|
+
:style="{ backgroundColor: getWellBadge(well)?.color }"
|
|
763
|
+
:title="getWellBadge(well)?.text === '+' ? 'Custom method' : `${getWellBadge(well)?.text ?? ''}x injections`"
|
|
764
764
|
>
|
|
765
|
-
{{ getWellBadge(well)
|
|
765
|
+
{{ getWellBadge(well)?.text }}
|
|
766
766
|
</span>
|
|
767
767
|
</div>
|
|
768
768
|
</td>
|