@morscherlab/mint-sdk 1.0.0-beta.7 → 1.0.0-rc.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/README.md +9 -1
- package/dist/__tests__/components/LcmsSequenceTable.test.d.ts +1 -0
- package/dist/__tests__/components/ProgressBar.test.d.ts +1 -0
- package/dist/__tests__/components/RackEditor.test.d.ts +1 -0
- package/dist/__tests__/components/SequenceProgressBar.test.d.ts +1 -0
- package/dist/__tests__/composables/useExperimentSamples.test.d.ts +1 -0
- package/dist/__tests__/composables/useProtocolTemplates.test.d.ts +1 -0
- package/dist/__tests__/stores/settings.test.d.ts +1 -0
- package/dist/__tests__/utils/instrument.test.d.ts +1 -0
- package/dist/__tests__/utils/lcms.test.d.ts +1 -0
- package/dist/__tests__/utils/permissions.test.d.ts +1 -0
- package/dist/__tests__/utils/rack.test.d.ts +1 -0
- package/dist/{auth-QQj2kkze.js → auth-B7g4J4ZF.js} +148 -24
- package/dist/auth-B7g4J4ZF.js.map +1 -0
- package/dist/components/AutoGroupModal.vue.d.ts +1 -1
- package/dist/components/BaseCheckbox.vue.d.ts +1 -1
- package/dist/components/BaseToggle.vue.d.ts +2 -2
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
- package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -1
- package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +1 -1
- package/dist/components/DoseDesignWorkspaceView.vue.d.ts +1 -1
- package/dist/components/FormulaInput.vue.d.ts +1 -1
- package/dist/components/InstrumentAlertLog.vue.d.ts +22 -0
- package/dist/components/InstrumentStateBadge.vue.d.ts +11 -0
- package/dist/components/InstrumentStatusCard.vue.d.ts +13 -0
- package/dist/components/LcmsSequenceTable.vue.d.ts +26 -0
- package/dist/components/ProgressBar.vue.d.ts +1 -0
- package/dist/components/RackEditor.vue.d.ts +41 -3
- package/dist/components/ReagentList.vue.d.ts +1 -1
- package/dist/components/SampleSelector.vue.d.ts +5 -2
- package/dist/components/SegmentedControl.vue.d.ts +2 -0
- package/dist/components/SequenceInput.vue.d.ts +1 -1
- package/dist/components/SequenceProgressBar.vue.d.ts +15 -0
- package/dist/components/SettingsModal.vue.d.ts +8 -1
- package/dist/components/TagsInput.vue.d.ts +1 -1
- package/dist/components/WellPlate.vue.d.ts +42 -3
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.js +3 -3
- package/dist/{components-DihbSJjU.js → components-BhK-dW99.js} +2135 -1075
- package/dist/components-BhK-dW99.js.map +1 -0
- package/dist/composables/experimentDesignData.d.ts +17 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/index.js +4 -4
- package/dist/composables/useControlSchema.d.ts +11 -0
- package/dist/composables/useExperimentData.d.ts +11 -3
- package/dist/composables/useExperimentSamples.d.ts +42 -0
- package/dist/composables/usePlatformContext.d.ts +54 -0
- package/dist/{composables-BcgZ6diz.js → composables-Bg7CFuNz.js} +5 -3
- package/dist/composables-Bg7CFuNz.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +168 -6
- package/dist/index.js.map +1 -0
- package/dist/install.js +2 -2
- package/dist/instrument.d.ts +7 -0
- package/dist/lcms.d.ts +27 -0
- package/dist/permissions.d.ts +46 -0
- package/dist/stores/auth.d.ts +74 -2
- package/dist/stores/index.js +1 -1
- package/dist/styles.css +3186 -1070
- package/dist/templates/builders.d.ts +7 -3
- package/dist/templates/index.d.ts +2 -2
- package/dist/templates/index.js +2 -2
- package/dist/templates/presets.d.ts +12 -0
- package/dist/templates/types.d.ts +16 -1
- package/dist/{templates-Cyt0Suwf.js → templates-BorLR_7p.js} +324 -10
- package/dist/templates-BorLR_7p.js.map +1 -0
- package/dist/types/auth.d.ts +2 -0
- package/dist/types/components.d.ts +32 -3
- package/dist/types/form-builder.d.ts +2 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/instrument.d.ts +56 -0
- package/dist/types/platform.d.ts +3 -0
- package/dist/{useExperimentData-CM6Y0u5L.js → useProtocolTemplates-n6AJqSqv.js} +627 -380
- package/dist/useProtocolTemplates-n6AJqSqv.js.map +1 -0
- package/dist/utils/rack.d.ts +47 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppTopBar.test.ts +15 -0
- package/src/__tests__/components/BaseTabs.test.ts +15 -0
- package/src/__tests__/components/GroupAssigner.test.ts +18 -0
- package/src/__tests__/components/LcmsSequenceTable.test.ts +57 -0
- package/src/__tests__/components/ProgressBar.test.ts +18 -0
- package/src/__tests__/components/RackEditor.test.ts +125 -0
- package/src/__tests__/components/SampleSelector.test.ts +25 -0
- package/src/__tests__/components/SegmentedControl.test.ts +45 -0
- package/src/__tests__/components/SequenceProgressBar.test.ts +39 -0
- package/src/__tests__/components/SettingsModal.test.ts +83 -2
- package/src/__tests__/composables/useApi.test.ts +45 -0
- package/src/__tests__/composables/useAuth.test.ts +20 -0
- package/src/__tests__/composables/useControlSchema.test.ts +4 -0
- package/src/__tests__/composables/useExperimentData.test.ts +23 -0
- package/src/__tests__/composables/useExperimentSamples.test.ts +91 -0
- package/src/__tests__/composables/useProtocolTemplates.test.ts +64 -0
- package/src/__tests__/stores/settings.test.ts +78 -0
- package/src/__tests__/templates/templates.test.ts +86 -0
- package/src/__tests__/utils/instrument.test.ts +47 -0
- package/src/__tests__/utils/lcms.test.ts +73 -0
- package/src/__tests__/utils/permissions.test.ts +50 -0
- package/src/__tests__/utils/rack.test.ts +120 -0
- package/src/components/AppAvatarMenu.vue +6 -3
- package/src/components/AppTopBar.vue +16 -10
- package/src/components/AuditTrail.vue +1 -1
- package/src/components/BaseTabs.vue +22 -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/InstrumentAlertLog.vue +191 -0
- package/src/components/InstrumentStateBadge.vue +50 -0
- package/src/components/InstrumentStatusCard.vue +188 -0
- package/src/components/LcmsSequenceTable.vue +191 -0
- package/src/components/NumberInput.vue +5 -3
- package/src/components/ProgressBar.vue +3 -0
- package/src/components/RackEditor.vue +73 -2
- package/src/components/SampleHierarchyTree.vue +3 -2
- package/src/components/SampleSelector.vue +28 -9
- package/src/components/SegmentedControl.story.vue +17 -0
- package/src/components/SegmentedControl.vue +14 -3
- package/src/components/SequenceProgressBar.vue +71 -0
- package/src/components/SettingsModal.vue +49 -2
- package/src/components/UnitInput.vue +6 -2
- package/src/components/WellPlate.vue +145 -24
- package/src/components/index.ts +5 -0
- package/src/components/internal/WellEditPopupInternal.vue +1 -0
- package/src/composables/experimentDesignData.ts +182 -0
- package/src/composables/index.ts +14 -0
- package/src/composables/useApi.ts +113 -16
- package/src/composables/useAuth.ts +4 -0
- package/src/composables/useAutoGroup.ts +18 -9
- package/src/composables/useControlSchema.ts +21 -0
- package/src/composables/useExperimentData.ts +57 -16
- package/src/composables/useExperimentSamples.ts +142 -0
- package/src/composables/useProtocolTemplates.ts +13 -1
- package/src/composables/useRackEditor.ts +3 -2
- package/src/index.ts +27 -0
- package/src/instrument.ts +90 -0
- package/src/lcms.ts +108 -0
- package/src/permissions.ts +143 -0
- package/src/stores/auth.ts +79 -26
- package/src/stores/settings.ts +10 -0
- package/src/styles/components/instrument-monitor.css +478 -0
- package/src/styles/components/lcms-sequence-table.css +189 -0
- package/src/styles/components/sequence-progress-bar.css +63 -0
- package/src/styles/components/settings-modal.css +9 -0
- package/src/styles/components/tabs.css +9 -0
- package/src/styles/components/well-edit-popup.css +7 -1
- package/src/styles/components/well-plate.css +5 -0
- package/src/styles/index.css +3 -0
- package/src/templates/builders.ts +201 -0
- package/src/templates/controlSchemas.ts +68 -0
- package/src/templates/index.ts +2 -0
- package/src/templates/presets.ts +23 -0
- package/src/templates/types.ts +17 -0
- package/src/types/auth.ts +3 -0
- package/src/types/components.ts +45 -3
- package/src/types/form-builder.ts +2 -1
- package/src/types/index.ts +35 -0
- package/src/types/instrument.ts +61 -0
- package/src/types/platform.ts +4 -0
- package/src/utils/rack.ts +209 -0
- package/dist/auth-QQj2kkze.js.map +0 -1
- package/dist/components-DihbSJjU.js.map +0 -1
- package/dist/composables-BcgZ6diz.js.map +0 -1
- package/dist/templates-Cyt0Suwf.js.map +0 -1
- package/dist/useExperimentData-CM6Y0u5L.js.map +0 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { SelectOption } from '../types'
|
|
2
|
+
|
|
3
|
+
export interface ExperimentDesignDataResponse {
|
|
4
|
+
experiment_id?: number
|
|
5
|
+
plugin_id?: string
|
|
6
|
+
data?: Record<string, unknown>
|
|
7
|
+
schema_version?: string
|
|
8
|
+
updated_at?: string | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ExtractExperimentSamplesOptions {
|
|
12
|
+
includeControls?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
16
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isPlatformDesignDataResponse(value: Record<string, unknown>): boolean {
|
|
20
|
+
return (
|
|
21
|
+
'data' in value
|
|
22
|
+
&& (
|
|
23
|
+
'experiment_id' in value
|
|
24
|
+
|| 'plugin_id' in value
|
|
25
|
+
|| 'schema_version' in value
|
|
26
|
+
|| 'updated_at' in value
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Return the plugin-defined design_data payload from common platform and plugin response shapes. */
|
|
32
|
+
export function unwrapExperimentDesignData(
|
|
33
|
+
rawData: Record<string, unknown> | null | undefined,
|
|
34
|
+
): Record<string, unknown> | null {
|
|
35
|
+
if (!rawData) return null
|
|
36
|
+
|
|
37
|
+
if (isRecord(rawData.data) && isPlatformDesignDataResponse(rawData)) {
|
|
38
|
+
return rawData.data
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isRecord(rawData.design_data)) {
|
|
42
|
+
return rawData.design_data
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (isRecord(rawData.designData)) {
|
|
46
|
+
return rawData.designData
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return rawData
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function shouldIncludeSample(record: Record<string, unknown>, options: ExtractExperimentSamplesOptions): boolean {
|
|
53
|
+
if (options.includeControls) return true
|
|
54
|
+
const rawType = record.sample_type ?? record.sampleType ?? record.type ?? record.well_type ?? record.wellType
|
|
55
|
+
const type = typeof rawType === 'string' ? rawType.toLowerCase() : ''
|
|
56
|
+
return !['blank', 'qc'].includes(type)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readSampleValue(record: Record<string, unknown>): string | null {
|
|
60
|
+
const candidates = [
|
|
61
|
+
record.sample_name,
|
|
62
|
+
record.sampleName,
|
|
63
|
+
record.sample_id,
|
|
64
|
+
record.sampleId,
|
|
65
|
+
record.name,
|
|
66
|
+
record.id,
|
|
67
|
+
record.label,
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
for (const candidate of candidates) {
|
|
71
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
72
|
+
return candidate
|
|
73
|
+
}
|
|
74
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate)) {
|
|
75
|
+
return String(candidate)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readSampleLabel(record: Record<string, unknown>, fallback: string): string {
|
|
83
|
+
const label = record.name ?? record.label ?? record.sample_name ?? record.sampleName
|
|
84
|
+
return typeof label === 'string' && label.trim() ? label : fallback
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readSampleDescription(record: Record<string, unknown>): string | undefined {
|
|
88
|
+
const parts = [
|
|
89
|
+
record.group,
|
|
90
|
+
record.batch,
|
|
91
|
+
record.batch_id,
|
|
92
|
+
record.batchId,
|
|
93
|
+
record.plate_id,
|
|
94
|
+
record.plateId,
|
|
95
|
+
record.well_id,
|
|
96
|
+
record.wellId,
|
|
97
|
+
].filter((value): value is string | number =>
|
|
98
|
+
(typeof value === 'string' && value.trim().length > 0)
|
|
99
|
+
|| typeof value === 'number',
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return parts.length > 0 ? parts.map(String).join(' / ') : undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function addSampleRecord(
|
|
106
|
+
options: SelectOption<string>[],
|
|
107
|
+
seen: Set<string>,
|
|
108
|
+
value: unknown,
|
|
109
|
+
extractOptions: ExtractExperimentSamplesOptions,
|
|
110
|
+
): void {
|
|
111
|
+
if (typeof value === 'string') {
|
|
112
|
+
const trimmed = value.trim()
|
|
113
|
+
if (trimmed && !seen.has(trimmed)) {
|
|
114
|
+
seen.add(trimmed)
|
|
115
|
+
options.push({ value: trimmed, label: trimmed })
|
|
116
|
+
}
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!isRecord(value) || !shouldIncludeSample(value, extractOptions)) return
|
|
121
|
+
|
|
122
|
+
const sampleValue = readSampleValue(value)
|
|
123
|
+
if (!sampleValue || seen.has(sampleValue)) return
|
|
124
|
+
|
|
125
|
+
seen.add(sampleValue)
|
|
126
|
+
options.push({
|
|
127
|
+
value: sampleValue,
|
|
128
|
+
label: readSampleLabel(value, sampleValue),
|
|
129
|
+
description: readSampleDescription(value),
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function addSampleArray(
|
|
134
|
+
options: SelectOption<string>[],
|
|
135
|
+
seen: Set<string>,
|
|
136
|
+
value: unknown,
|
|
137
|
+
extractOptions: ExtractExperimentSamplesOptions,
|
|
138
|
+
): void {
|
|
139
|
+
if (!Array.isArray(value)) return
|
|
140
|
+
for (const sample of value) {
|
|
141
|
+
addSampleRecord(options, seen, sample, extractOptions)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Extract selectable sample options from raw or wrapped experiment design_data. */
|
|
146
|
+
export function extractSampleOptionsFromDesignData(
|
|
147
|
+
rawData: Record<string, unknown> | null | undefined,
|
|
148
|
+
options: ExtractExperimentSamplesOptions = {},
|
|
149
|
+
): SelectOption<string>[] {
|
|
150
|
+
const designData = unwrapExperimentDesignData(rawData)
|
|
151
|
+
if (!designData) return []
|
|
152
|
+
|
|
153
|
+
const sampleOptions: SelectOption<string>[] = []
|
|
154
|
+
const seen = new Set<string>()
|
|
155
|
+
|
|
156
|
+
addSampleArray(sampleOptions, seen, designData.samples, options)
|
|
157
|
+
|
|
158
|
+
if (isRecord(designData.templates)) {
|
|
159
|
+
for (const template of Object.values(designData.templates)) {
|
|
160
|
+
if (!isRecord(template)) continue
|
|
161
|
+
const templateData = isRecord(template.data) ? template.data : template
|
|
162
|
+
addSampleArray(sampleOptions, seen, templateData.samples, options)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (Array.isArray(designData.plates)) {
|
|
167
|
+
for (const plate of designData.plates) {
|
|
168
|
+
if (!isRecord(plate)) continue
|
|
169
|
+
addSampleArray(sampleOptions, seen, plate.samples, options)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return sampleOptions
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Extract SampleSelector-compatible string values from raw or wrapped experiment design_data. */
|
|
177
|
+
export function extractSampleNamesFromDesignData(
|
|
178
|
+
rawData: Record<string, unknown> | null | undefined,
|
|
179
|
+
options: ExtractExperimentSamplesOptions = {},
|
|
180
|
+
): string[] {
|
|
181
|
+
return extractSampleOptionsFromDesignData(rawData, options).map(sample => sample.value)
|
|
182
|
+
}
|
package/src/composables/index.ts
CHANGED
|
@@ -144,6 +144,20 @@ export {
|
|
|
144
144
|
type UseExperimentDataOptions,
|
|
145
145
|
type UseExperimentDataReturn,
|
|
146
146
|
} from './useExperimentData'
|
|
147
|
+
export {
|
|
148
|
+
extractSampleNamesFromDesignData,
|
|
149
|
+
extractSampleOptionsFromDesignData,
|
|
150
|
+
unwrapExperimentDesignData,
|
|
151
|
+
type ExperimentDesignDataResponse,
|
|
152
|
+
type ExtractExperimentSamplesOptions,
|
|
153
|
+
} from './experimentDesignData'
|
|
154
|
+
export {
|
|
155
|
+
useExperimentSamples,
|
|
156
|
+
type ExperimentDesignDataSource,
|
|
157
|
+
type ExperimentIdSource,
|
|
158
|
+
type UseExperimentSamplesOptions,
|
|
159
|
+
type UseExperimentSamplesReturn,
|
|
160
|
+
} from './useExperimentSamples'
|
|
147
161
|
export {
|
|
148
162
|
getFieldRegistryEntry,
|
|
149
163
|
getTypeDefault,
|
|
@@ -1,17 +1,97 @@
|
|
|
1
|
-
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'
|
|
1
|
+
import axios, { type AxiosInstance, type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios'
|
|
2
2
|
import { useSettingsStore } from '../stores/settings'
|
|
3
3
|
import { useAuthStore } from '../stores/auth'
|
|
4
4
|
|
|
5
5
|
let apiClientInstance: AxiosInstance | null = null
|
|
6
6
|
let interceptorAttached = false
|
|
7
7
|
|
|
8
|
+
interface MintAxiosRequestConfig extends AxiosRequestConfig {
|
|
9
|
+
_mintSkipAuth?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface MintInternalAxiosRequestConfig extends InternalAxiosRequestConfig {
|
|
13
|
+
_mintSkipAuth?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type MutableHeaders = Record<string, unknown> & {
|
|
17
|
+
has?: (header: string) => boolean
|
|
18
|
+
set?: (header: string, value: string) => void
|
|
19
|
+
delete?: (header: string) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
function joinUrlPath(baseUrl: string, path: string): string {
|
|
9
23
|
if (!path) return baseUrl
|
|
24
|
+
if (path.startsWith('?') || path.startsWith('#')) return `${baseUrl.replace(/\/+$/, '')}${path}`
|
|
10
25
|
const normalizedBase = baseUrl.replace(/\/+$/, '')
|
|
11
26
|
const normalizedPath = path.replace(/^\/+/, '/')
|
|
12
27
|
return `${normalizedBase}${normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`}`
|
|
13
28
|
}
|
|
14
29
|
|
|
30
|
+
function getBasePath(baseUrl: string): string {
|
|
31
|
+
if (!baseUrl) return '/'
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
|
|
35
|
+
return new URL(baseUrl, origin).pathname.replace(/\/+$/, '') || '/'
|
|
36
|
+
} catch {
|
|
37
|
+
const path = baseUrl.replace(/^https?:\/\/[^/]+/i, '')
|
|
38
|
+
return path.replace(/\/+$/, '') || '/'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeRequestUrl(baseUrl: string, url: string): string {
|
|
43
|
+
if (!url || /^https?:\/\//.test(url)) return url
|
|
44
|
+
|
|
45
|
+
const basePath = getBasePath(baseUrl)
|
|
46
|
+
if (basePath === '/') return url
|
|
47
|
+
|
|
48
|
+
const normalizedUrl = url.startsWith('/') ? url : `/${url}`
|
|
49
|
+
if (
|
|
50
|
+
normalizedUrl === basePath
|
|
51
|
+
|| normalizedUrl.startsWith(`${basePath}?`)
|
|
52
|
+
|| normalizedUrl.startsWith(`${basePath}#`)
|
|
53
|
+
) {
|
|
54
|
+
return normalizedUrl.slice(basePath.length)
|
|
55
|
+
}
|
|
56
|
+
if (normalizedUrl.startsWith(`${basePath}/`)) {
|
|
57
|
+
return normalizedUrl.slice(basePath.length) || ''
|
|
58
|
+
}
|
|
59
|
+
return url
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function asMutableHeaders(headers: AxiosRequestConfig['headers']): MutableHeaders | null {
|
|
63
|
+
return headers ? (headers as MutableHeaders) : null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function hasAuthorizationHeader(headers: AxiosRequestConfig['headers']): boolean {
|
|
67
|
+
const bag = asMutableHeaders(headers)
|
|
68
|
+
if (!bag) return false
|
|
69
|
+
if (typeof bag.has === 'function') return bag.has('Authorization')
|
|
70
|
+
return Object.keys(bag).some((key) => key.toLowerCase() === 'authorization')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function setAuthorizationHeader(headers: AxiosRequestConfig['headers'], value: string): void {
|
|
74
|
+
const bag = asMutableHeaders(headers)
|
|
75
|
+
if (!bag) return
|
|
76
|
+
if (typeof bag.set === 'function') {
|
|
77
|
+
bag.set('Authorization', value)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
bag.Authorization = value
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function deleteAuthorizationHeader(headers: AxiosRequestConfig['headers']): void {
|
|
84
|
+
const bag = asMutableHeaders(headers)
|
|
85
|
+
if (!bag) return
|
|
86
|
+
if (typeof bag.delete === 'function') {
|
|
87
|
+
bag.delete('Authorization')
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
for (const key of Object.keys(bag)) {
|
|
91
|
+
if (key.toLowerCase() === 'authorization') delete bag[key]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
15
95
|
function getApiClient(): AxiosInstance {
|
|
16
96
|
if (!apiClientInstance) {
|
|
17
97
|
apiClientInstance = axios.create({
|
|
@@ -56,51 +136,68 @@ export function useApi(options: ApiClientOptions = {}): UseApiReturn {
|
|
|
56
136
|
// Attach auth interceptor only once (reads token dynamically, not from closure)
|
|
57
137
|
if (!interceptorAttached) {
|
|
58
138
|
apiClient.interceptors.request.use((config) => {
|
|
59
|
-
|
|
60
|
-
|
|
139
|
+
const request = config as MintInternalAxiosRequestConfig
|
|
140
|
+
if (request._mintSkipAuth) {
|
|
141
|
+
delete request._mintSkipAuth
|
|
142
|
+
deleteAuthorizationHeader(request.headers)
|
|
143
|
+
return request
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const currentAuthStore = useAuthStore()
|
|
147
|
+
if (currentAuthStore.token && config.headers && !hasAuthorizationHeader(config.headers)) {
|
|
148
|
+
setAuthorizationHeader(config.headers, `Bearer ${currentAuthStore.token}`)
|
|
61
149
|
}
|
|
62
150
|
return config
|
|
63
151
|
})
|
|
64
152
|
interceptorAttached = true
|
|
65
153
|
}
|
|
66
154
|
|
|
155
|
+
function getBaseUrl(): string {
|
|
156
|
+
return options.baseUrl ?? settingsStore.getApiBaseUrl()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeUrl(url: string): string {
|
|
160
|
+
return normalizeRequestUrl(getBaseUrl(), url)
|
|
161
|
+
}
|
|
162
|
+
|
|
67
163
|
// Build per-request config that applies this caller's options
|
|
68
|
-
function requestConfig(config?: AxiosRequestConfig):
|
|
69
|
-
const base:
|
|
70
|
-
baseURL:
|
|
164
|
+
function requestConfig(config?: AxiosRequestConfig): MintAxiosRequestConfig {
|
|
165
|
+
const base: MintAxiosRequestConfig = {
|
|
166
|
+
baseURL: getBaseUrl(),
|
|
71
167
|
timeout: options.timeout ?? settingsStore.requestTimeout,
|
|
72
168
|
...config,
|
|
73
169
|
}
|
|
74
170
|
// Strip auth header if explicitly disabled
|
|
75
171
|
if (options.withAuth === false) {
|
|
76
|
-
base.
|
|
172
|
+
base._mintSkipAuth = true
|
|
173
|
+
deleteAuthorizationHeader(base.headers)
|
|
77
174
|
}
|
|
78
175
|
return base
|
|
79
176
|
}
|
|
80
177
|
|
|
81
178
|
// Generic request methods
|
|
82
179
|
async function get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
83
|
-
const response = await apiClient.get<T>(url, requestConfig(config))
|
|
180
|
+
const response = await apiClient.get<T>(normalizeUrl(url), requestConfig(config))
|
|
84
181
|
return response.data
|
|
85
182
|
}
|
|
86
183
|
|
|
87
184
|
async function post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
88
|
-
const response = await apiClient.post<T>(url, data, requestConfig(config))
|
|
185
|
+
const response = await apiClient.post<T>(normalizeUrl(url), data, requestConfig(config))
|
|
89
186
|
return response.data
|
|
90
187
|
}
|
|
91
188
|
|
|
92
189
|
async function put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
93
|
-
const response = await apiClient.put<T>(url, data, requestConfig(config))
|
|
190
|
+
const response = await apiClient.put<T>(normalizeUrl(url), data, requestConfig(config))
|
|
94
191
|
return response.data
|
|
95
192
|
}
|
|
96
193
|
|
|
97
194
|
async function patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
98
|
-
const response = await apiClient.patch<T>(url, data, requestConfig(config))
|
|
195
|
+
const response = await apiClient.patch<T>(normalizeUrl(url), data, requestConfig(config))
|
|
99
196
|
return response.data
|
|
100
197
|
}
|
|
101
198
|
|
|
102
199
|
async function del<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
103
|
-
const response = await apiClient.delete<T>(url, requestConfig(config))
|
|
200
|
+
const response = await apiClient.delete<T>(normalizeUrl(url), requestConfig(config))
|
|
104
201
|
return response.data
|
|
105
202
|
}
|
|
106
203
|
|
|
@@ -117,7 +214,7 @@ export function useApi(options: ApiClientOptions = {}): UseApiReturn {
|
|
|
117
214
|
})
|
|
118
215
|
}
|
|
119
216
|
|
|
120
|
-
const response = await apiClient.post<T>(url, formData, requestConfig({
|
|
217
|
+
const response = await apiClient.post<T>(normalizeUrl(url), formData, requestConfig({
|
|
121
218
|
// Let Axios set Content-Type with the correct multipart boundary
|
|
122
219
|
headers: { 'Content-Type': undefined },
|
|
123
220
|
}))
|
|
@@ -126,7 +223,7 @@ export function useApi(options: ApiClientOptions = {}): UseApiReturn {
|
|
|
126
223
|
|
|
127
224
|
// Download helper - returns blob URL
|
|
128
225
|
async function download(url: string, filename?: string): Promise<string> {
|
|
129
|
-
const response = await apiClient.get(url, requestConfig({ responseType: 'blob' }))
|
|
226
|
+
const response = await apiClient.get(normalizeUrl(url), requestConfig({ responseType: 'blob' }))
|
|
130
227
|
const blob = new Blob([response.data])
|
|
131
228
|
const blobUrl = URL.createObjectURL(blob)
|
|
132
229
|
|
|
@@ -150,8 +247,8 @@ export function useApi(options: ApiClientOptions = {}): UseApiReturn {
|
|
|
150
247
|
|
|
151
248
|
// Build full URL for external use (e.g., <a href="...">)
|
|
152
249
|
function buildUrl(path: string): string {
|
|
153
|
-
const baseUrl =
|
|
154
|
-
return joinUrlPath(baseUrl, path)
|
|
250
|
+
const baseUrl = getBaseUrl()
|
|
251
|
+
return joinUrlPath(baseUrl, normalizeRequestUrl(baseUrl, path))
|
|
155
252
|
}
|
|
156
253
|
|
|
157
254
|
// WebSocket URL builder
|
|
@@ -3,6 +3,7 @@ import { ref, onMounted, onUnmounted, watch, getCurrentInstance, type Ref } from
|
|
|
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'
|
|
6
|
+
import type { RoleInfo } from '../permissions'
|
|
6
7
|
|
|
7
8
|
interface UserResponse {
|
|
8
9
|
id: string
|
|
@@ -10,6 +11,7 @@ interface UserResponse {
|
|
|
10
11
|
shortname: string | null
|
|
11
12
|
email: string | null
|
|
12
13
|
role: string
|
|
14
|
+
role_obj?: RoleInfo | null
|
|
13
15
|
is_active: boolean
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -187,6 +189,7 @@ export function useAuth(): UseAuthReturn {
|
|
|
187
189
|
shortname: response.data.shortname,
|
|
188
190
|
email: response.data.email,
|
|
189
191
|
role: response.data.role,
|
|
192
|
+
roleObj: response.data.role_obj ?? null,
|
|
190
193
|
isActive: response.data.is_active,
|
|
191
194
|
}
|
|
192
195
|
|
|
@@ -379,6 +382,7 @@ export function useAuth(): UseAuthReturn {
|
|
|
379
382
|
shortname: response.data.shortname,
|
|
380
383
|
email: response.data.email,
|
|
381
384
|
role: response.data.role,
|
|
385
|
+
roleObj: response.data.role_obj ?? null,
|
|
382
386
|
isActive: response.data.is_active,
|
|
383
387
|
}
|
|
384
388
|
authStore.setUserInfo(userInfo)
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
ParsedCsvData,
|
|
10
10
|
} from '../types/auto-group'
|
|
11
11
|
import type { SampleGroup } from '../types/components'
|
|
12
|
+
import { unwrapExperimentDesignData } from './experimentDesignData'
|
|
12
13
|
|
|
13
14
|
export const DEFAULT_COLORS = [
|
|
14
15
|
'#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6',
|
|
@@ -254,10 +255,12 @@ export function computeGroups(
|
|
|
254
255
|
}
|
|
255
256
|
const groupKey = keyParts.join(' / ')
|
|
256
257
|
|
|
257
|
-
|
|
258
|
-
|
|
258
|
+
const group = groupMap.get(groupKey)
|
|
259
|
+
if (group) {
|
|
260
|
+
group.push(sample)
|
|
261
|
+
} else {
|
|
262
|
+
groupMap.set(groupKey, [sample])
|
|
259
263
|
}
|
|
260
|
-
groupMap.get(groupKey)!.push(sample)
|
|
261
264
|
|
|
262
265
|
// Build metadata row with ALL columns
|
|
263
266
|
const fields: Record<string, string> = {}
|
|
@@ -309,7 +312,10 @@ export function computeGroups(
|
|
|
309
312
|
export function extractSamplesFromDesignData(
|
|
310
313
|
rawData: Record<string, unknown>,
|
|
311
314
|
): ParsedCsvData | null {
|
|
312
|
-
const
|
|
315
|
+
const designData = unwrapExperimentDesignData(rawData)
|
|
316
|
+
if (!designData) return null
|
|
317
|
+
|
|
318
|
+
const samples = designData.samples
|
|
313
319
|
if (!Array.isArray(samples) || samples.length === 0) return null
|
|
314
320
|
|
|
315
321
|
// Single pass: filter QC/blank and collect all condition keys
|
|
@@ -369,10 +375,12 @@ export function computeGroupsFromCsv(
|
|
|
369
375
|
const keyParts = enabledCols.map(col => row[col.originalName ?? col.name])
|
|
370
376
|
const groupKey = keyParts.join(' / ')
|
|
371
377
|
|
|
372
|
-
|
|
373
|
-
|
|
378
|
+
const group = groupMap.get(groupKey)
|
|
379
|
+
if (group) {
|
|
380
|
+
group.push(sampleName)
|
|
381
|
+
} else {
|
|
382
|
+
groupMap.set(groupKey, [sampleName])
|
|
374
383
|
}
|
|
375
|
-
groupMap.get(groupKey)!.push(sampleName)
|
|
376
384
|
|
|
377
385
|
// Build metadata row with ALL columns — use display name as key, original for lookup
|
|
378
386
|
const fields: Record<string, string> = {}
|
|
@@ -417,8 +425,9 @@ export function useAutoGroup() {
|
|
|
417
425
|
)
|
|
418
426
|
|
|
419
427
|
const samples = computed(() => {
|
|
420
|
-
|
|
421
|
-
|
|
428
|
+
const data = csvData.value
|
|
429
|
+
if (isTabularMode.value && data) {
|
|
430
|
+
return data.rows.map(r => r[data.sampleColumn])
|
|
422
431
|
}
|
|
423
432
|
return rawText.value
|
|
424
433
|
.split('\n')
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
SettingsModalSchema,
|
|
7
7
|
TopBarSettingsConfig,
|
|
8
8
|
} from '../types'
|
|
9
|
+
import type { AccessPolicy, AccessAudienceInput } from '../permissions'
|
|
9
10
|
import type {
|
|
10
11
|
FieldCondition,
|
|
11
12
|
FieldValidation,
|
|
@@ -34,6 +35,11 @@ export interface ControlSectionConfig {
|
|
|
34
35
|
iconBg?: string
|
|
35
36
|
columns?: 1 | 2 | 3
|
|
36
37
|
condition?: FieldCondition
|
|
38
|
+
access?: AccessPolicy
|
|
39
|
+
visibleFor?: AccessAudienceInput
|
|
40
|
+
requiresAdmin?: boolean
|
|
41
|
+
permissions?: readonly string[]
|
|
42
|
+
anyPermissions?: readonly string[]
|
|
37
43
|
defaultOpen?: boolean
|
|
38
44
|
showToggle?: boolean
|
|
39
45
|
}
|
|
@@ -70,6 +76,11 @@ export interface ControlDefinition {
|
|
|
70
76
|
pattern?: string | { value: string; message: string }
|
|
71
77
|
validation?: FieldValidation
|
|
72
78
|
condition?: FieldCondition
|
|
79
|
+
access?: AccessPolicy
|
|
80
|
+
visibleFor?: AccessAudienceInput
|
|
81
|
+
requiresAdmin?: boolean
|
|
82
|
+
permissions?: readonly string[]
|
|
83
|
+
anyPermissions?: readonly string[]
|
|
73
84
|
options?: readonly ControlOption[]
|
|
74
85
|
section?: string
|
|
75
86
|
sectionLabel?: string
|
|
@@ -760,6 +771,11 @@ export function controlsToSettingsSchema(
|
|
|
760
771
|
fields: sectionControls.map(controlToFormField),
|
|
761
772
|
columns: config.columns ?? options.columns ?? 1,
|
|
762
773
|
condition: config.condition,
|
|
774
|
+
access: config.access,
|
|
775
|
+
visibleFor: config.visibleFor,
|
|
776
|
+
requiresAdmin: config.requiresAdmin,
|
|
777
|
+
permissions: config.permissions,
|
|
778
|
+
anyPermissions: config.anyPermissions,
|
|
763
779
|
}
|
|
764
780
|
}),
|
|
765
781
|
}
|
|
@@ -1150,6 +1166,11 @@ function controlToFormField(control: NormalizedControl): FormFieldSchema {
|
|
|
1150
1166
|
readonly: definition.readonly,
|
|
1151
1167
|
validation: validationForControl(definition),
|
|
1152
1168
|
condition: definition.condition,
|
|
1169
|
+
access: definition.access,
|
|
1170
|
+
visibleFor: definition.visibleFor,
|
|
1171
|
+
requiresAdmin: definition.requiresAdmin,
|
|
1172
|
+
permissions: definition.permissions,
|
|
1173
|
+
anyPermissions: definition.anyPermissions,
|
|
1153
1174
|
colSpan: definition.colSpan,
|
|
1154
1175
|
props,
|
|
1155
1176
|
}
|