@morscherlab/mint-sdk 1.3.0-beta.3 → 1.3.0-beta.5

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 (113) hide show
  1. package/README.md +23 -140
  2. package/dist/{ResourcePickerListInternal-c3IT8PIu.js → ResourcePickerListInternal-BEXnyBNz.js} +7 -3
  3. package/dist/ResourcePickerListInternal-BEXnyBNz.js.map +1 -0
  4. package/dist/components/FilePicker.vue.d.ts +4 -1
  5. package/dist/components/WellPlate.colors.d.ts +1 -5
  6. package/dist/components/WellPlate.rendering.d.ts +5 -2
  7. package/dist/components/index.d.ts +32 -17
  8. package/dist/components/index.js +3 -3
  9. package/dist/components/internal/FilePickerListInternal.vue.d.ts +2 -0
  10. package/dist/{components-CvY105kJ.js → components-BTzcqrJp.js} +400 -358
  11. package/dist/components-BTzcqrJp.js.map +1 -0
  12. package/dist/composables/createFilePickerAdapter.d.ts +5 -1
  13. package/dist/composables/filePicker/usePickerNavigation.d.ts +3 -2
  14. package/dist/composables/index.d.ts +0 -3
  15. package/dist/composables/index.js +4 -4
  16. package/dist/composables/useAutoGroup.d.ts +1 -1
  17. package/dist/composables/useBioTemplateComponents.d.ts +15 -3
  18. package/dist/composables/useBioTemplateControls.d.ts +5 -1
  19. package/dist/composables/useBioTemplatePackWorkspace.d.ts +2 -0
  20. package/dist/composables/useBioTemplatePresetWorkspace.d.ts +5 -1
  21. package/dist/composables/useBioTemplateWorkspace.d.ts +5 -1
  22. package/dist/composables/useExperimentSave.d.ts +5 -1
  23. package/dist/composables/useFilePicker.d.ts +4 -1
  24. package/dist/composables/useGroupAssignment.d.ts +5 -1
  25. package/dist/composables/useTemplateCollection.d.ts +5 -1
  26. package/dist/composables-Doqb7g1z.js +591 -0
  27. package/dist/composables-Doqb7g1z.js.map +1 -0
  28. package/dist/index.js +24 -24
  29. package/dist/index.js.map +1 -1
  30. package/dist/install.js +1 -1
  31. package/dist/lcms.d.ts +11 -11
  32. package/dist/styles.css +60 -89
  33. package/dist/templates/index.js +1 -1
  34. package/dist/{templates-Bj9hZ7iV.js → templates-DgAG5kMq.js} +2 -2
  35. package/dist/{templates-Bj9hZ7iV.js.map → templates-DgAG5kMq.js.map} +1 -1
  36. package/dist/types/auth.d.ts +0 -27
  37. package/dist/types/components.d.ts +4 -0
  38. package/dist/types/fileBrowserTypes.d.ts +13 -0
  39. package/dist/types/filePicker.d.ts +8 -1
  40. package/dist/types/index.d.ts +2 -2
  41. package/dist/utils/rack.d.ts +16 -16
  42. package/dist/{validation-B4Y8_eEe.js → validation-DILksa0c.js} +50 -12
  43. package/dist/validation-DILksa0c.js.map +1 -0
  44. package/package.json +1 -6
  45. package/src/__tests__/components/ActionMenu.test.ts +10 -0
  46. package/src/__tests__/components/AppTopBar.test.ts +16 -8
  47. package/src/__tests__/components/EmptyState.test.ts +15 -0
  48. package/src/__tests__/components/FilePicker.test.ts +65 -1
  49. package/src/__tests__/components/WellPlate.decoration.test.ts +4 -28
  50. package/src/__tests__/components/WellPlate.rendering.test.ts +20 -33
  51. package/src/__tests__/components/WellPlate.sampleTypeIndicator.test.ts +7 -7
  52. package/src/__tests__/composables/createFilePickerAdapter.test.ts +92 -1
  53. package/src/__tests__/composables/usePickerNavigation.test.ts +61 -1
  54. package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +18 -6
  55. package/src/__tests__/docs/frontendDocsCatalog.test.ts +2 -2
  56. package/src/__tests__/styles/controlRefreshRules.test.ts +0 -6
  57. package/src/__tests__/styles/menusShellRefresh.test.ts +8 -2
  58. package/src/__tests__/templates/templatesDeprecation.test.ts +51 -11
  59. package/src/components/ActionMenu.vue +10 -2
  60. package/src/components/AppTopBar.vue +2 -4
  61. package/src/components/EmptyState.vue +3 -1
  62. package/src/components/FilePicker.vue +42 -0
  63. package/src/components/WellPlate.colors.ts +6 -10
  64. package/src/components/WellPlate.legend.ts +4 -4
  65. package/src/components/WellPlate.rendering.ts +9 -8
  66. package/src/components/WellPlate.vue +5 -22
  67. package/src/components/index.ts +32 -17
  68. package/src/components/internal/FilePickerListInternal.vue +11 -2
  69. package/src/composables/autoGroup/index.ts +1 -1
  70. package/src/composables/createFilePickerAdapter.ts +82 -22
  71. package/src/composables/filePicker/usePickerNavigation.ts +29 -2
  72. package/src/composables/index.ts +0 -7
  73. package/src/composables/useAutoGroup.ts +1 -1
  74. package/src/composables/useBioTemplateComponents.ts +15 -3
  75. package/src/composables/useBioTemplateControls.ts +5 -1
  76. package/src/composables/useBioTemplatePackWorkspace.ts +2 -0
  77. package/src/composables/useBioTemplatePresetWorkspace.ts +5 -1
  78. package/src/composables/useBioTemplateWorkspace.ts +5 -1
  79. package/src/composables/useExperimentSave.ts +5 -1
  80. package/src/composables/useFilePicker.ts +8 -1
  81. package/src/composables/useGroupAssignment.ts +5 -1
  82. package/src/composables/usePlatformFilePickerAdapter.ts +26 -4
  83. package/src/composables/useTemplateCollection.ts +5 -1
  84. package/src/index.ts +1 -1
  85. package/src/lcms.ts +11 -11
  86. package/src/styles/components/action-menu.css +10 -0
  87. package/src/styles/components/app-top-bar.css +7 -31
  88. package/src/styles/components/empty-state.css +3 -2
  89. package/src/styles/components/file-picker.css +23 -1
  90. package/src/styles/components/well-plate.css +18 -55
  91. package/src/types/auth.ts +0 -37
  92. package/src/types/components.ts +4 -0
  93. package/src/types/fileBrowserTypes.ts +15 -0
  94. package/src/types/filePicker.ts +8 -1
  95. package/src/types/index.ts +2 -5
  96. package/src/utils/rack.ts +16 -16
  97. package/dist/ResourcePickerListInternal-c3IT8PIu.js.map +0 -1
  98. package/dist/__tests__/composables/useAuth.test.d.ts +0 -1
  99. package/dist/__tests__/composables/usePasskey.test.d.ts +0 -1
  100. package/dist/components-CvY105kJ.js.map +0 -1
  101. package/dist/composables/useAuth.d.ts +0 -47
  102. package/dist/composables/usePasskey.d.ts +0 -10
  103. package/dist/composables/useWellPlateEditor.d.ts +0 -33
  104. package/dist/composables-DL4Vca7L.js +0 -1193
  105. package/dist/composables-DL4Vca7L.js.map +0 -1
  106. package/dist/plotly.min-D6Ge6qgX.js +0 -168582
  107. package/dist/plotly.min-D6Ge6qgX.js.map +0 -1
  108. package/dist/validation-B4Y8_eEe.js.map +0 -1
  109. package/src/__tests__/composables/useAuth.test.ts +0 -290
  110. package/src/__tests__/composables/usePasskey.test.ts +0 -38
  111. package/src/composables/useAuth.ts +0 -470
  112. package/src/composables/usePasskey.ts +0 -216
  113. package/src/composables/useWellPlateEditor.ts +0 -415
@@ -1,290 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { createPinia, setActivePinia } from 'pinia'
3
- import axios from 'axios'
4
-
5
- vi.mock('axios')
6
- const mockedAxios = vi.mocked(axios, true)
7
-
8
- const unmountedCallbacks: Array<() => void> = []
9
-
10
- // Mock vue lifecycle hooks since we're not in a component
11
- vi.mock('vue', async () => {
12
- const actual = await vi.importActual<typeof import('vue')>('vue')
13
- return {
14
- ...actual,
15
- getCurrentInstance: vi.fn(() => ({})),
16
- onMounted: vi.fn((cb: () => void) => cb()),
17
- onUnmounted: vi.fn((cb: () => void) => {
18
- unmountedCallbacks.push(cb)
19
- }),
20
- }
21
- })
22
-
23
- import { useAuth, _resetAuthStateForTesting } from '../../composables/useAuth'
24
- import { useAuthStore } from '../../stores/auth'
25
- import { useSettingsStore } from '../../stores/settings'
26
-
27
- describe('useAuth', () => {
28
- beforeEach(() => {
29
- setActivePinia(createPinia())
30
- vi.clearAllMocks()
31
- vi.useFakeTimers()
32
- unmountedCallbacks.length = 0
33
- _resetAuthStateForTesting()
34
-
35
- // Mock settings store
36
- const settingsStore = useSettingsStore()
37
- settingsStore.getApiBaseUrl = vi.fn(() => 'http://localhost:8000')
38
-
39
- // Mock localStorage
40
- const storage: Record<string, string> = {}
41
- vi.stubGlobal('localStorage', {
42
- getItem: vi.fn((key: string) => storage[key] ?? null),
43
- setItem: vi.fn((key: string, val: string) => { storage[key] = val }),
44
- removeItem: vi.fn((key: string) => { delete storage[key] }),
45
- })
46
- })
47
-
48
- afterEach(() => {
49
- vi.useRealTimers()
50
- vi.restoreAllMocks()
51
- })
52
-
53
- it('loads the PostgreSQL-only public auth contract', async () => {
54
- mockedAxios.get.mockResolvedValueOnce({
55
- data: {
56
- auth_required: true,
57
- passkey_enabled: true,
58
- passkey_registered: false,
59
- registration_enabled: true,
60
- database_mode: 'postgresql',
61
- access: { experiment_visibility_mode: 'restricted' },
62
- },
63
- })
64
-
65
- const config = await useAuth().fetchAuthConfig()
66
- const expected = {
67
- authRequired: true,
68
- passkeyEnabled: true,
69
- passkeyRegistered: false,
70
- registrationEnabled: true,
71
- databaseMode: 'postgresql',
72
- experimentVisibilityMode: 'restricted',
73
- }
74
-
75
- expect([config, useAuthStore().authConfig]).toEqual([expected, expected])
76
- })
77
-
78
- describe('login', () => {
79
- it('should set token on successful login', async () => {
80
- mockedAxios.post.mockResolvedValueOnce({
81
- data: { access_token: 'test-token', expires_in: 3600, token_type: 'bearer' },
82
- })
83
- mockedAxios.get.mockResolvedValueOnce({
84
- data: { id: 1, username: 'test', shortname: null, email: null, role: 'user', is_active: true },
85
- })
86
-
87
- const { login } = useAuth()
88
- const result = await login('test', 'password')
89
-
90
- expect(result).toBe(true)
91
- const authStore = useAuthStore()
92
- expect(authStore.token).toBe('test-token')
93
- })
94
-
95
- it('should return false on failed login', async () => {
96
- mockedAxios.post.mockRejectedValueOnce({
97
- isAxiosError: true,
98
- response: { data: { detail: 'Invalid credentials' }, status: 401 },
99
- })
100
- // @ts-expect-error -- vitest mock assignment
101
- mockedAxios.isAxiosError = vi.fn(() => true)
102
-
103
- const { login } = useAuth()
104
- const result = await login('test', 'wrong')
105
-
106
- expect(result).toBe(false)
107
- })
108
- })
109
-
110
- describe('logout', () => {
111
- it('should clear token on logout', async () => {
112
- const authStore = useAuthStore()
113
- authStore.setToken('test-token', 3600)
114
-
115
- const { logout } = useAuth()
116
- logout()
117
-
118
- expect(authStore.token).toBeNull()
119
- })
120
- })
121
-
122
- describe('storage fallback', () => {
123
- it('keeps auth usable when localStorage operations throw', () => {
124
- vi.stubGlobal('localStorage', {
125
- getItem: vi.fn(() => { throw new Error('blocked') }),
126
- setItem: vi.fn(() => { throw new Error('blocked') }),
127
- removeItem: vi.fn(() => { throw new Error('blocked') }),
128
- })
129
-
130
- const authStore = useAuthStore()
131
-
132
- expect(() => authStore.initialize()).not.toThrow()
133
-
134
- authStore.setToken('memory-token', 3600)
135
- expect(authStore.token).toBe('memory-token')
136
-
137
- authStore.clearToken()
138
- expect(authStore.token).toBeNull()
139
- })
140
- })
141
-
142
- describe('refreshToken - race condition', () => {
143
- it('should deduplicate concurrent refresh calls', async () => {
144
- const authStore = useAuthStore()
145
- authStore.setToken('old-token', 3600)
146
-
147
- let resolveRefresh: (value: unknown) => void
148
- const refreshPromise = new Promise((resolve) => {
149
- resolveRefresh = resolve
150
- })
151
-
152
- mockedAxios.post.mockImplementation(() => refreshPromise as Promise<unknown>)
153
-
154
- const { refreshToken } = useAuth()
155
-
156
- // Fire two concurrent refreshes
157
- const p1 = refreshToken()
158
- const p2 = refreshToken()
159
-
160
- // Both should return the same promise
161
- resolveRefresh!({
162
- data: { access_token: 'new-token', expires_in: 3600, token_type: 'bearer' },
163
- })
164
-
165
- const [r1, r2] = await Promise.all([p1, p2])
166
- expect(r1).toBe(true)
167
- expect(r2).toBe(true)
168
-
169
- // axios.post should have been called only once
170
- expect(mockedAxios.post).toHaveBeenCalledTimes(1)
171
- })
172
- })
173
-
174
- describe('refreshToken - lifecycle behavior', () => {
175
- it('keeps scheduled refresh when one of multiple consumers unmounts', async () => {
176
- mockedAxios.post.mockImplementation((url: string) => {
177
- if (url.endsWith('/auth/login')) {
178
- return Promise.resolve({
179
- data: { access_token: 'test-token', expires_in: 301, token_type: 'bearer' },
180
- })
181
- }
182
- if (url.endsWith('/auth/refresh')) {
183
- return Promise.resolve({
184
- data: { access_token: 'refreshed-token', expires_in: 3600, token_type: 'bearer' },
185
- })
186
- }
187
- throw new Error(`Unexpected POST ${url}`)
188
- })
189
- mockedAxios.get.mockResolvedValue({
190
- data: { id: 1, username: 'test', shortname: null, email: null, role: 'user', is_active: true },
191
- })
192
-
193
- const authA = useAuth()
194
- useAuth()
195
-
196
- await authA.login('test', 'password')
197
-
198
- expect(unmountedCallbacks).toHaveLength(2)
199
- unmountedCallbacks[0]!()
200
-
201
- vi.advanceTimersByTime(2_000)
202
- await Promise.resolve()
203
-
204
- const refreshCalls = mockedAxios.post.mock.calls.filter(
205
- ([url]) => typeof url === 'string' && url.endsWith('/auth/refresh'),
206
- )
207
- expect(refreshCalls).toHaveLength(1)
208
-
209
- unmountedCallbacks[1]!()
210
- })
211
- })
212
-
213
- describe('verifyToken', () => {
214
- it('should return false when no token', async () => {
215
- const { verifyToken } = useAuth()
216
- const result = await verifyToken()
217
- expect(result).toBe(false)
218
- })
219
-
220
- it('should return true on valid token', async () => {
221
- const authStore = useAuthStore()
222
- authStore.setToken('valid-token', 3600)
223
-
224
- mockedAxios.get.mockResolvedValueOnce({
225
- data: { valid: true, username: 'testuser' },
226
- })
227
-
228
- const { verifyToken } = useAuth()
229
- const result = await verifyToken()
230
- expect(result).toBe(true)
231
- })
232
- })
233
-
234
- describe('updateProfile', () => {
235
- it('should swap in the replacement token when a password change returns one', async () => {
236
- const authStore = useAuthStore()
237
- authStore.setToken('old-token', 3600)
238
- mockedAxios.put.mockResolvedValueOnce({
239
- data: {
240
- id: 1, username: 'test', shortname: null, email: null, role: 'user', is_active: true,
241
- access_token: 'fresh-token', expires_in: 7200,
242
- },
243
- })
244
-
245
- await useAuth().updateProfile({ currentPassword: 'old', newPassword: 'new-password' })
246
-
247
- expect(authStore.token).toBe('fresh-token')
248
- })
249
-
250
- async function changePasswordDuringRefresh(settle: (reject: (e: unknown) => void, resolve: (v: unknown) => void) => void) {
251
- const authStore = useAuthStore()
252
- authStore.setToken('old-token', 3600)
253
- let resolveRefresh!: (v: unknown) => void
254
- let rejectRefresh!: (e: unknown) => void
255
- mockedAxios.post.mockReturnValueOnce(new Promise((resolve, reject) => {
256
- resolveRefresh = resolve
257
- rejectRefresh = reject
258
- }))
259
- mockedAxios.put.mockResolvedValueOnce({
260
- data: {
261
- id: 1, username: 'test', shortname: null, email: null, role: 'user', is_active: true,
262
- access_token: 'fresh-token', expires_in: 7200,
263
- },
264
- })
265
- const auth = useAuth()
266
- const refreshing = auth.refreshToken()
267
- await auth.updateProfile({ currentPassword: 'old', newPassword: 'new-password' })
268
- settle(rejectRefresh, resolveRefresh)
269
- await refreshing
270
- return authStore.token
271
- }
272
-
273
- it('should keep the fresh token when an old-token refresh fails with 401 afterwards', async () => {
274
- mockedAxios.isAxiosError.mockReturnValue(true)
275
-
276
- const token = await changePasswordDuringRefresh((reject) => reject({ response: { status: 401 } }))
277
-
278
- expect(token).toBe('fresh-token')
279
- })
280
-
281
- it('should keep the fresh token when an old-token refresh succeeds afterwards', async () => {
282
- const token = await changePasswordDuringRefresh((_reject, resolve) =>
283
- resolve({ data: { access_token: 'revoked-token', expires_in: 3600, token_type: 'bearer' } }),
284
- )
285
-
286
- expect(token).toBe('fresh-token')
287
- })
288
- })
289
- })
290
-
@@ -1,38 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
- import { createPinia, setActivePinia } from 'pinia'
3
- import axios from 'axios'
4
- import { startAuthentication, startRegistration } from '@simplewebauthn/browser'
5
- import { usePasskey } from '../../composables/usePasskey'
6
-
7
- vi.mock('axios')
8
- vi.mock('@simplewebauthn/browser', () => ({
9
- browserSupportsWebAuthn: vi.fn(() => true),
10
- startAuthentication: vi.fn(),
11
- startRegistration: vi.fn(),
12
- }))
13
-
14
- const options = { challenge: 'challenge' }
15
-
16
- beforeEach(() => {
17
- setActivePinia(createPinia())
18
- vi.mocked(axios.get).mockResolvedValueOnce({ data: { options: JSON.stringify(options) } })
19
- vi.mocked(axios.post).mockResolvedValueOnce({ data: { access_token: 'token', expires_in: 3600 } })
20
- })
21
-
22
- afterEach(() => {
23
- vi.resetAllMocks()
24
- })
25
-
26
- describe('usePasskey', () => {
27
- it('should pass server options as optionsJSON when logging in', async () => {
28
- await usePasskey().loginWithPasskey()
29
-
30
- expect(startAuthentication).toHaveBeenCalledWith({ optionsJSON: options })
31
- })
32
-
33
- it('should pass server options as optionsJSON when registering', async () => {
34
- await usePasskey().registerPasskey('Laptop')
35
-
36
- expect(startRegistration).toHaveBeenCalledWith({ optionsJSON: options })
37
- })
38
- })