@morscherlab/mint-sdk 1.3.0-beta.1 → 1.3.0-beta.11
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 +23 -140
- package/dist/{ResourcePickerListInternal-c3IT8PIu.js → ResourcePickerListInternal-CFAw4vTZ.js} +26 -19
- package/dist/ResourcePickerListInternal-CFAw4vTZ.js.map +1 -0
- package/dist/components/FilePicker.vue.d.ts +4 -1
- package/dist/components/SettingsModal.schema.d.ts +1 -1
- package/dist/components/SettingsModal.vue.d.ts +2 -0
- package/dist/components/WellPlate.colors.d.ts +1 -5
- package/dist/components/WellPlate.rendering.d.ts +5 -2
- package/dist/components/index.d.ts +32 -17
- package/dist/components/index.js +3 -3
- package/dist/components/internal/FilePickerListInternal.vue.d.ts +2 -0
- package/dist/components/internal/ResourcePickerListInternal.vue.d.ts +9 -3
- package/dist/{components-CvY105kJ.js → components-vHfxV2PB.js} +425 -392
- package/dist/components-vHfxV2PB.js.map +1 -0
- package/dist/composables/createFilePickerAdapter.d.ts +5 -1
- package/dist/composables/filePicker/usePickerNavigation.d.ts +3 -2
- package/dist/composables/index.d.ts +0 -3
- package/dist/composables/index.js +4 -4
- package/dist/composables/useAutoGroup.d.ts +1 -1
- package/dist/composables/useBioTemplateComponents.d.ts +15 -3
- package/dist/composables/useBioTemplateControls.d.ts +5 -1
- package/dist/composables/useBioTemplatePackWorkspace.d.ts +2 -0
- package/dist/composables/useBioTemplatePresetWorkspace.d.ts +5 -1
- package/dist/composables/useBioTemplateWorkspace.d.ts +5 -1
- package/dist/composables/useExperimentSave.d.ts +5 -1
- package/dist/composables/useFilePicker.d.ts +4 -1
- package/dist/composables/useGroupAssignment.d.ts +5 -1
- package/dist/composables/usePlatformContext.d.ts +3 -0
- package/dist/composables/useTemplateCollection.d.ts +5 -1
- package/dist/composables-D6uqp0UF.js +591 -0
- package/dist/composables-D6uqp0UF.js.map +1 -0
- package/dist/index.js +24 -24
- package/dist/index.js.map +1 -1
- package/dist/install.js +1 -1
- package/dist/lcms.d.ts +11 -11
- package/dist/styles.css +228 -214
- package/dist/templates/index.js +1 -1
- package/dist/{templates-Bj9hZ7iV.js → templates-BU1pet28.js} +31 -44
- package/dist/templates-BU1pet28.js.map +1 -0
- package/dist/types/auth.d.ts +0 -27
- package/dist/types/components.d.ts +8 -0
- package/dist/types/fileBrowserTypes.d.ts +13 -0
- package/dist/types/filePicker.d.ts +8 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/platform.d.ts +2 -0
- package/dist/utils/rack.d.ts +16 -16
- package/dist/{validation-B4Y8_eEe.js → validation-Dmp9Q5W0.js} +50 -12
- package/dist/validation-Dmp9Q5W0.js.map +1 -0
- package/package.json +1 -6
- package/src/__tests__/components/ActionMenu.test.ts +10 -0
- package/src/__tests__/components/AppTopBar.test.ts +24 -8
- package/src/__tests__/components/AppTopBarPageSelector.test.ts +4 -4
- package/src/__tests__/components/EmptyState.test.ts +15 -0
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +12 -5
- package/src/__tests__/components/FilePicker.test.ts +65 -1
- package/src/__tests__/components/FileUploader.test.ts +6 -9
- package/src/__tests__/components/ResourcePickerListInternal.test.ts +23 -4
- package/src/__tests__/components/SettingsModal.test.ts +22 -0
- package/src/__tests__/components/WellPlate.decoration.test.ts +4 -28
- package/src/__tests__/components/WellPlate.rendering.test.ts +20 -33
- package/src/__tests__/components/WellPlate.sampleTypeIndicator.test.ts +7 -7
- package/src/__tests__/composables/createFilePickerAdapter.test.ts +92 -1
- package/src/__tests__/composables/usePickerNavigation.test.ts +61 -1
- package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +18 -6
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +2 -2
- package/src/__tests__/styles/controlRefreshRules.test.ts +21 -8
- package/src/__tests__/styles/menusShellRefresh.test.ts +20 -5
- package/src/__tests__/templates/templatesDeprecation.test.ts +58 -11
- package/src/components/ActionMenu.vue +10 -2
- package/src/components/AppTopBar.vue +6 -4
- package/src/components/EmptyState.vue +3 -1
- package/src/components/ExperimentSelectorModal.vue +4 -1
- package/src/components/FilePicker.vue +42 -0
- package/src/components/FileUploader.vue +30 -46
- package/src/components/SettingsModal.schema.ts +4 -2
- package/src/components/SettingsModal.vue +4 -2
- package/src/components/WellPlate.colors.ts +6 -10
- package/src/components/WellPlate.legend.ts +4 -4
- package/src/components/WellPlate.rendering.ts +9 -8
- package/src/components/WellPlate.vue +5 -22
- package/src/components/index.ts +32 -17
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +3 -6
- package/src/components/internal/FilePickerListInternal.vue +11 -2
- package/src/components/internal/ResourcePickerListInternal.vue +21 -27
- package/src/composables/autoGroup/index.ts +1 -1
- package/src/composables/createFilePickerAdapter.ts +82 -22
- package/src/composables/filePicker/usePickerNavigation.ts +29 -2
- package/src/composables/index.ts +0 -7
- package/src/composables/useAutoGroup.ts +1 -1
- package/src/composables/useBioTemplateComponents.ts +15 -3
- package/src/composables/useBioTemplateControls.ts +5 -1
- package/src/composables/useBioTemplatePackWorkspace.ts +2 -0
- package/src/composables/useBioTemplatePresetWorkspace.ts +5 -1
- package/src/composables/useBioTemplateWorkspace.ts +5 -1
- package/src/composables/useExperimentSave.ts +5 -1
- package/src/composables/useFilePicker.ts +8 -1
- package/src/composables/useGroupAssignment.ts +5 -1
- package/src/composables/usePlatformFilePickerAdapter.ts +26 -4
- package/src/composables/useTemplateCollection.ts +5 -1
- package/src/index.ts +1 -1
- package/src/lcms.ts +11 -11
- package/src/styles/components/action-menu.css +10 -0
- package/src/styles/components/app-page-selector.css +61 -17
- package/src/styles/components/app-pill-nav.css +1 -1
- package/src/styles/components/app-top-bar.css +18 -38
- package/src/styles/components/empty-state.css +3 -2
- package/src/styles/components/experiment-selector-modal.css +3 -9
- package/src/styles/components/file-picker.css +23 -1
- package/src/styles/components/file-uploader.css +22 -55
- package/src/styles/components/resource-picker-list.css +70 -36
- package/src/styles/components/well-plate.css +18 -55
- package/src/types/auth.ts +0 -37
- package/src/types/components.ts +8 -0
- package/src/types/fileBrowserTypes.ts +15 -0
- package/src/types/filePicker.ts +8 -1
- package/src/types/index.ts +2 -5
- package/src/types/platform.ts +2 -0
- package/src/utils/rack.ts +16 -16
- package/dist/ResourcePickerListInternal-c3IT8PIu.js.map +0 -1
- package/dist/__tests__/composables/useAuth.test.d.ts +0 -1
- package/dist/__tests__/composables/usePasskey.test.d.ts +0 -1
- package/dist/components-CvY105kJ.js.map +0 -1
- package/dist/composables/useAuth.d.ts +0 -47
- package/dist/composables/usePasskey.d.ts +0 -10
- package/dist/composables/useWellPlateEditor.d.ts +0 -33
- package/dist/composables-DL4Vca7L.js +0 -1193
- package/dist/composables-DL4Vca7L.js.map +0 -1
- package/dist/plotly.min-D6Ge6qgX.js +0 -168582
- package/dist/plotly.min-D6Ge6qgX.js.map +0 -1
- package/dist/templates-Bj9hZ7iV.js.map +0 -1
- package/dist/validation-B4Y8_eEe.js.map +0 -1
- package/src/__tests__/composables/useAuth.test.ts +0 -290
- package/src/__tests__/composables/usePasskey.test.ts +0 -38
- package/src/composables/useAuth.ts +0 -470
- package/src/composables/usePasskey.ts +0 -216
- package/src/composables/useWellPlateEditor.ts +0 -415
|
@@ -1,1193 +0,0 @@
|
|
|
1
|
-
import { o as useSettingsStore } from "./settings-BLr8lzw6.js";
|
|
2
|
-
import { Sn as SAMPLE_COLOR_SCALE, ar as getInjectedPlatformContext } from "./templates-Bj9hZ7iV.js";
|
|
3
|
-
import { n as pickerFormat, st as useRequestSyncState } from "./validation-B4Y8_eEe.js";
|
|
4
|
-
import { a as useApi, o as useAuthStore } from "./experiment-DgllIc3I.js";
|
|
5
|
-
import { computed, getCurrentInstance, getCurrentScope, onBeforeUnmount, onMounted, onScopeDispose, onUnmounted, ref, shallowRef, toRaw, toValue, watch } from "vue";
|
|
6
|
-
import axios from "axios";
|
|
7
|
-
//#region src/composables/useAuth.ts
|
|
8
|
-
var TOKEN_REFRESH_MARGIN_MS = 3e5;
|
|
9
|
-
var TOKEN_REFRESH_CHECK_INTERVAL_MS = 6e4;
|
|
10
|
-
var _refreshPromise = null;
|
|
11
|
-
var _refreshTimerId = null;
|
|
12
|
-
var _mountedConsumerCount = 0;
|
|
13
|
-
/** Manages authentication state with login/logout/register and automatic JWT token refresh. */
|
|
14
|
-
function useAuth() {
|
|
15
|
-
const authStore = useAuthStore();
|
|
16
|
-
const settingsStore = useSettingsStore();
|
|
17
|
-
const isRefreshing = ref(false);
|
|
18
|
-
function getApiBaseUrl() {
|
|
19
|
-
return settingsStore.getApiBaseUrl();
|
|
20
|
-
}
|
|
21
|
-
async function fetchAuthConfig() {
|
|
22
|
-
try {
|
|
23
|
-
const response = await axios.get(`${getApiBaseUrl()}/setup/config/public`);
|
|
24
|
-
const config = {
|
|
25
|
-
authRequired: response.data.auth_required,
|
|
26
|
-
passkeyEnabled: response.data.passkey_enabled,
|
|
27
|
-
passkeyRegistered: response.data.passkey_registered ?? false,
|
|
28
|
-
registrationEnabled: response.data.registration_enabled ?? false,
|
|
29
|
-
databaseMode: response.data.database_mode ?? "none",
|
|
30
|
-
experimentVisibilityMode: response.data.access?.experiment_visibility_mode ?? response.data.access?.experimentVisibilityMode ?? "open"
|
|
31
|
-
};
|
|
32
|
-
authStore.setAuthConfig(config);
|
|
33
|
-
return config;
|
|
34
|
-
} catch (error) {
|
|
35
|
-
console.error("Failed to fetch auth config:", error);
|
|
36
|
-
return {
|
|
37
|
-
authRequired: false,
|
|
38
|
-
passkeyEnabled: false,
|
|
39
|
-
passkeyRegistered: false,
|
|
40
|
-
registrationEnabled: false,
|
|
41
|
-
databaseMode: "none",
|
|
42
|
-
experimentVisibilityMode: "open"
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
async function login(username, password) {
|
|
47
|
-
authStore.setLoading(true);
|
|
48
|
-
authStore.setError(null);
|
|
49
|
-
try {
|
|
50
|
-
const response = await axios.post(`${getApiBaseUrl()}/auth/login`, {
|
|
51
|
-
username,
|
|
52
|
-
password
|
|
53
|
-
});
|
|
54
|
-
authStore.setToken(response.data.access_token, response.data.expires_in);
|
|
55
|
-
authStore.setUsername(username);
|
|
56
|
-
await getCurrentUser();
|
|
57
|
-
scheduleTokenRefresh();
|
|
58
|
-
return true;
|
|
59
|
-
} catch (error) {
|
|
60
|
-
if (axios.isAxiosError(error) && error.response) authStore.setError(error.response.data.detail || "Login failed");
|
|
61
|
-
else authStore.setError("Network error. Please try again.");
|
|
62
|
-
return false;
|
|
63
|
-
} finally {
|
|
64
|
-
authStore.setLoading(false);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
async function register(username, password, email) {
|
|
68
|
-
authStore.setLoading(true);
|
|
69
|
-
authStore.setError(null);
|
|
70
|
-
try {
|
|
71
|
-
await axios.post(`${getApiBaseUrl()}/users/register`, {
|
|
72
|
-
username,
|
|
73
|
-
password,
|
|
74
|
-
email
|
|
75
|
-
});
|
|
76
|
-
return await login(username, password);
|
|
77
|
-
} catch (error) {
|
|
78
|
-
if (axios.isAxiosError(error) && error.response) authStore.setError(error.response.data.detail || "Registration failed");
|
|
79
|
-
else authStore.setError("Network error. Please try again.");
|
|
80
|
-
return false;
|
|
81
|
-
} finally {
|
|
82
|
-
authStore.setLoading(false);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
async function getCurrentUser() {
|
|
86
|
-
if (!authStore.token) return null;
|
|
87
|
-
try {
|
|
88
|
-
const response = await axios.get(`${getApiBaseUrl()}/users/me`, { headers: getAuthHeader() });
|
|
89
|
-
const userInfo = {
|
|
90
|
-
id: response.data.id,
|
|
91
|
-
username: response.data.username,
|
|
92
|
-
shortname: response.data.shortname,
|
|
93
|
-
email: response.data.email,
|
|
94
|
-
role: response.data.role,
|
|
95
|
-
roleObj: response.data.role_obj ?? null,
|
|
96
|
-
isActive: response.data.is_active
|
|
97
|
-
};
|
|
98
|
-
authStore.setUserInfo(userInfo);
|
|
99
|
-
return userInfo;
|
|
100
|
-
} catch {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
async function verifyToken() {
|
|
105
|
-
if (!authStore.token) return false;
|
|
106
|
-
try {
|
|
107
|
-
const response = await axios.get(`${getApiBaseUrl()}/auth/verify`, { headers: { Authorization: `Bearer ${authStore.token}` } });
|
|
108
|
-
if (response.data.valid && response.data.username) {
|
|
109
|
-
authStore.setUsername(response.data.username);
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
authStore.clearToken();
|
|
113
|
-
return false;
|
|
114
|
-
} catch {
|
|
115
|
-
authStore.clearToken();
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Refresh the authentication token.
|
|
121
|
-
* Called automatically before token expiration.
|
|
122
|
-
* Uses promise caching to prevent concurrent refresh requests.
|
|
123
|
-
*/
|
|
124
|
-
async function refreshToken() {
|
|
125
|
-
if (!authStore.token) return false;
|
|
126
|
-
if (_refreshPromise) return _refreshPromise;
|
|
127
|
-
_refreshPromise = (async () => {
|
|
128
|
-
isRefreshing.value = true;
|
|
129
|
-
const sentToken = authStore.token;
|
|
130
|
-
try {
|
|
131
|
-
const response = await axios.post(`${getApiBaseUrl()}/auth/refresh`, {}, { headers: getAuthHeader() });
|
|
132
|
-
if (authStore.token !== sentToken) return true;
|
|
133
|
-
authStore.setToken(response.data.access_token, response.data.expires_in);
|
|
134
|
-
scheduleTokenRefresh();
|
|
135
|
-
return true;
|
|
136
|
-
} catch (error) {
|
|
137
|
-
if (axios.isAxiosError(error) && error.response?.status === 401 && authStore.token === sentToken) {
|
|
138
|
-
console.warn("[Auth] Token refresh failed - session expired");
|
|
139
|
-
authStore.clearToken();
|
|
140
|
-
stopTokenRefresh();
|
|
141
|
-
}
|
|
142
|
-
return false;
|
|
143
|
-
} finally {
|
|
144
|
-
isRefreshing.value = false;
|
|
145
|
-
_refreshPromise = null;
|
|
146
|
-
}
|
|
147
|
-
})();
|
|
148
|
-
return _refreshPromise;
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Schedule automatic token refresh before expiration.
|
|
152
|
-
*/
|
|
153
|
-
function scheduleTokenRefresh() {
|
|
154
|
-
stopTokenRefresh();
|
|
155
|
-
if (!authStore.tokenExpires) return;
|
|
156
|
-
const refreshAt = authStore.tokenExpires.getTime() - TOKEN_REFRESH_MARGIN_MS;
|
|
157
|
-
const now = Date.now();
|
|
158
|
-
if (refreshAt <= now) {
|
|
159
|
-
refreshToken();
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const delay = refreshAt - now;
|
|
163
|
-
_refreshTimerId = window.setTimeout(() => {
|
|
164
|
-
refreshToken();
|
|
165
|
-
}, delay);
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Stop automatic token refresh.
|
|
169
|
-
*/
|
|
170
|
-
function stopTokenRefresh() {
|
|
171
|
-
if (_refreshTimerId !== null) {
|
|
172
|
-
window.clearTimeout(_refreshTimerId);
|
|
173
|
-
_refreshTimerId = null;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Check if token needs refresh and refresh if necessary.
|
|
178
|
-
* Called periodically as a safety net.
|
|
179
|
-
*/
|
|
180
|
-
function checkAndRefreshIfNeeded() {
|
|
181
|
-
if (!authStore.token || !authStore.tokenExpires) return;
|
|
182
|
-
const refreshAt = authStore.tokenExpires.getTime() - TOKEN_REFRESH_MARGIN_MS;
|
|
183
|
-
if (Date.now() >= refreshAt) refreshToken();
|
|
184
|
-
}
|
|
185
|
-
async function initializeAuth() {
|
|
186
|
-
authStore.initialize();
|
|
187
|
-
await fetchAuthConfig();
|
|
188
|
-
if (authStore.token) {
|
|
189
|
-
if (await verifyToken()) {
|
|
190
|
-
await getCurrentUser();
|
|
191
|
-
scheduleTokenRefresh();
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
function logout() {
|
|
196
|
-
stopTokenRefresh();
|
|
197
|
-
authStore.logout();
|
|
198
|
-
}
|
|
199
|
-
function getAuthHeader() {
|
|
200
|
-
if (authStore.token) return { Authorization: `Bearer ${authStore.token}` };
|
|
201
|
-
return {};
|
|
202
|
-
}
|
|
203
|
-
async function updateProfile(data) {
|
|
204
|
-
if (!authStore.token) return {
|
|
205
|
-
success: false,
|
|
206
|
-
error: "Not authenticated"
|
|
207
|
-
};
|
|
208
|
-
try {
|
|
209
|
-
const requestData = {};
|
|
210
|
-
if (data.email !== void 0) requestData.email = data.email;
|
|
211
|
-
if (data.shortname !== void 0) requestData.shortname = data.shortname;
|
|
212
|
-
if (data.currentPassword) requestData.current_password = data.currentPassword;
|
|
213
|
-
if (data.newPassword) requestData.new_password = data.newPassword;
|
|
214
|
-
const response = await axios.put(`${getApiBaseUrl()}/users/me`, requestData, { headers: getAuthHeader() });
|
|
215
|
-
if (response.data.access_token && response.data.expires_in) {
|
|
216
|
-
authStore.setToken(response.data.access_token, response.data.expires_in);
|
|
217
|
-
scheduleTokenRefresh();
|
|
218
|
-
}
|
|
219
|
-
const userInfo = {
|
|
220
|
-
id: response.data.id,
|
|
221
|
-
username: response.data.username,
|
|
222
|
-
shortname: response.data.shortname,
|
|
223
|
-
email: response.data.email,
|
|
224
|
-
role: response.data.role,
|
|
225
|
-
roleObj: response.data.role_obj ?? null,
|
|
226
|
-
isActive: response.data.is_active
|
|
227
|
-
};
|
|
228
|
-
authStore.setUserInfo(userInfo);
|
|
229
|
-
return { success: true };
|
|
230
|
-
} catch (error) {
|
|
231
|
-
if (axios.isAxiosError(error) && error.response) return {
|
|
232
|
-
success: false,
|
|
233
|
-
error: error.response.data.detail || "Update failed"
|
|
234
|
-
};
|
|
235
|
-
return {
|
|
236
|
-
success: false,
|
|
237
|
-
error: "Network error. Please try again."
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
let checkInterval = null;
|
|
242
|
-
if (getCurrentInstance()) {
|
|
243
|
-
onMounted(() => {
|
|
244
|
-
_mountedConsumerCount += 1;
|
|
245
|
-
checkInterval = window.setInterval(checkAndRefreshIfNeeded, TOKEN_REFRESH_CHECK_INTERVAL_MS);
|
|
246
|
-
});
|
|
247
|
-
onUnmounted(() => {
|
|
248
|
-
if (checkInterval !== null) {
|
|
249
|
-
window.clearInterval(checkInterval);
|
|
250
|
-
checkInterval = null;
|
|
251
|
-
}
|
|
252
|
-
_mountedConsumerCount = Math.max(0, _mountedConsumerCount - 1);
|
|
253
|
-
if (_mountedConsumerCount === 0) stopTokenRefresh();
|
|
254
|
-
});
|
|
255
|
-
watch(() => authStore.tokenExpires, (newExpires) => {
|
|
256
|
-
if (newExpires) scheduleTokenRefresh();
|
|
257
|
-
else stopTokenRefresh();
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
return {
|
|
261
|
-
login,
|
|
262
|
-
logout,
|
|
263
|
-
register,
|
|
264
|
-
verifyToken,
|
|
265
|
-
fetchAuthConfig,
|
|
266
|
-
initializeAuth,
|
|
267
|
-
getCurrentUser,
|
|
268
|
-
getAuthHeader,
|
|
269
|
-
updateProfile,
|
|
270
|
-
refreshToken,
|
|
271
|
-
isRefreshing
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
//#endregion
|
|
275
|
-
//#region src/composables/usePasskey.ts
|
|
276
|
-
async function loadWebAuthn() {
|
|
277
|
-
try {
|
|
278
|
-
return await import("@simplewebauthn/browser");
|
|
279
|
-
} catch {
|
|
280
|
-
throw new Error("@simplewebauthn/browser is required for passkey support. Install it: bun add @simplewebauthn/browser");
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
/** Registers and authenticates passkeys using WebAuthn, lazily loading the browser dependency. */
|
|
284
|
-
function usePasskey() {
|
|
285
|
-
const authStore = useAuthStore();
|
|
286
|
-
const settingsStore = useSettingsStore();
|
|
287
|
-
function getApiBaseUrl() {
|
|
288
|
-
return settingsStore.getApiBaseUrl();
|
|
289
|
-
}
|
|
290
|
-
async function isSupported() {
|
|
291
|
-
try {
|
|
292
|
-
const { browserSupportsWebAuthn } = await loadWebAuthn();
|
|
293
|
-
return browserSupportsWebAuthn();
|
|
294
|
-
} catch {
|
|
295
|
-
return false;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
async function registerPasskey(deviceName) {
|
|
299
|
-
const webauthn = await loadWebAuthn();
|
|
300
|
-
if (!webauthn.browserSupportsWebAuthn()) {
|
|
301
|
-
authStore.setError("WebAuthn is not supported in this browser");
|
|
302
|
-
return false;
|
|
303
|
-
}
|
|
304
|
-
authStore.setLoading(true);
|
|
305
|
-
authStore.setError(null);
|
|
306
|
-
try {
|
|
307
|
-
const optionsResponse = await axios.get(`${getApiBaseUrl()}/auth/passkey/register/options`, {
|
|
308
|
-
headers: { Authorization: `Bearer ${authStore.token}` },
|
|
309
|
-
withCredentials: true
|
|
310
|
-
});
|
|
311
|
-
const options = JSON.parse(optionsResponse.data.options);
|
|
312
|
-
const credential = await webauthn.startRegistration({ optionsJSON: options });
|
|
313
|
-
await axios.post(`${getApiBaseUrl()}/auth/passkey/register/verify`, {
|
|
314
|
-
credential: JSON.stringify(credential),
|
|
315
|
-
device_name: deviceName
|
|
316
|
-
}, {
|
|
317
|
-
headers: { Authorization: `Bearer ${authStore.token}` },
|
|
318
|
-
withCredentials: true
|
|
319
|
-
});
|
|
320
|
-
authStore.setAuthConfig({
|
|
321
|
-
...authStore.authConfig,
|
|
322
|
-
passkeyRegistered: true
|
|
323
|
-
});
|
|
324
|
-
return true;
|
|
325
|
-
} catch (error) {
|
|
326
|
-
if (axios.isAxiosError(error) && error.response) authStore.setError(error.response.data.detail || "Passkey registration failed");
|
|
327
|
-
else if (error instanceof Error) {
|
|
328
|
-
if (error.name === "NotAllowedError") authStore.setError("Registration was cancelled or timed out");
|
|
329
|
-
else if (error.name === "InvalidStateError") authStore.setError("This authenticator is already registered");
|
|
330
|
-
else authStore.setError(error.message);
|
|
331
|
-
} else authStore.setError("Passkey registration failed");
|
|
332
|
-
return false;
|
|
333
|
-
} finally {
|
|
334
|
-
authStore.setLoading(false);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
async function loginWithPasskey() {
|
|
338
|
-
const webauthn = await loadWebAuthn();
|
|
339
|
-
if (!webauthn.browserSupportsWebAuthn()) {
|
|
340
|
-
authStore.setError("WebAuthn is not supported in this browser");
|
|
341
|
-
return false;
|
|
342
|
-
}
|
|
343
|
-
authStore.setLoading(true);
|
|
344
|
-
authStore.setError(null);
|
|
345
|
-
try {
|
|
346
|
-
const optionsResponse = await axios.get(`${getApiBaseUrl()}/auth/passkey/login/options`, { withCredentials: true });
|
|
347
|
-
const options = JSON.parse(optionsResponse.data.options);
|
|
348
|
-
const credential = await webauthn.startAuthentication({ optionsJSON: options });
|
|
349
|
-
const response = await axios.post(`${getApiBaseUrl()}/auth/passkey/login/verify`, { credential: JSON.stringify(credential) }, { withCredentials: true });
|
|
350
|
-
authStore.setToken(response.data.access_token, response.data.expires_in);
|
|
351
|
-
return true;
|
|
352
|
-
} catch (error) {
|
|
353
|
-
if (axios.isAxiosError(error) && error.response) {
|
|
354
|
-
if (error.response.status === 404) authStore.setError("No passkeys registered. Please login with password first.");
|
|
355
|
-
else authStore.setError(error.response.data.detail || "Passkey login failed");
|
|
356
|
-
} else if (error instanceof Error) {
|
|
357
|
-
if (error.name === "NotAllowedError") authStore.setError("Authentication was cancelled or timed out");
|
|
358
|
-
else authStore.setError(error.message);
|
|
359
|
-
} else authStore.setError("Passkey login failed");
|
|
360
|
-
return false;
|
|
361
|
-
} finally {
|
|
362
|
-
authStore.setLoading(false);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
async function listCredentials() {
|
|
366
|
-
try {
|
|
367
|
-
return (await axios.get(`${getApiBaseUrl()}/auth/passkey/credentials`, { headers: { Authorization: `Bearer ${authStore.token}` } })).data.credentials;
|
|
368
|
-
} catch {
|
|
369
|
-
return [];
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
async function deleteCredential(credentialId) {
|
|
373
|
-
try {
|
|
374
|
-
await axios.delete(`${getApiBaseUrl()}/auth/passkey/credentials/${encodeURIComponent(credentialId)}`, { headers: { Authorization: `Bearer ${authStore.token}` } });
|
|
375
|
-
if ((await listCredentials()).length === 0) authStore.setAuthConfig({
|
|
376
|
-
...authStore.authConfig,
|
|
377
|
-
passkeyRegistered: false
|
|
378
|
-
});
|
|
379
|
-
return true;
|
|
380
|
-
} catch {
|
|
381
|
-
return false;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
async function deleteAllCredentials() {
|
|
385
|
-
try {
|
|
386
|
-
await axios.delete(`${getApiBaseUrl()}/auth/passkey/credentials`, { headers: { Authorization: `Bearer ${authStore.token}` } });
|
|
387
|
-
authStore.setAuthConfig({
|
|
388
|
-
...authStore.authConfig,
|
|
389
|
-
passkeyRegistered: false
|
|
390
|
-
});
|
|
391
|
-
return true;
|
|
392
|
-
} catch {
|
|
393
|
-
return false;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
return {
|
|
397
|
-
isSupported,
|
|
398
|
-
registerPasskey,
|
|
399
|
-
loginWithPasskey,
|
|
400
|
-
listCredentials,
|
|
401
|
-
deleteCredential,
|
|
402
|
-
deleteAllCredentials
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
//#endregion
|
|
406
|
-
//#region src/composables/pluginFrontendBase.ts
|
|
407
|
-
function normalizePluginRoutePrefix(prefix) {
|
|
408
|
-
const candidate = prefix?.trim();
|
|
409
|
-
if (!candidate) return void 0;
|
|
410
|
-
const path = candidate.split("/").filter(Boolean).join("/");
|
|
411
|
-
return path ? `/${path}/` : "/";
|
|
412
|
-
}
|
|
413
|
-
/**
|
|
414
|
-
* Resolve the Vue Router base for a plugin frontend.
|
|
415
|
-
*
|
|
416
|
-
* Integrated plugins use the platform-injected mount prefix. Standalone and
|
|
417
|
-
* development builds retain the caller's Vite base unchanged.
|
|
418
|
-
*/
|
|
419
|
-
function resolvePluginFrontendBase(fallbackBase = "/") {
|
|
420
|
-
const context = getInjectedPlatformContext();
|
|
421
|
-
if (!context?.isIntegrated) return fallbackBase;
|
|
422
|
-
return normalizePluginRoutePrefix(context.plugin?.route_prefix) ?? fallbackBase;
|
|
423
|
-
}
|
|
424
|
-
//#endregion
|
|
425
|
-
//#region src/composables/useWellPlateEditor.ts
|
|
426
|
-
var MAX_HISTORY = 50;
|
|
427
|
-
function createEmptyPlate(name, format, id) {
|
|
428
|
-
return {
|
|
429
|
-
id: id || `plate-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
430
|
-
name,
|
|
431
|
-
format,
|
|
432
|
-
wells: {}
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
function generateSampleId() {
|
|
436
|
-
return `sample-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
437
|
-
}
|
|
438
|
-
function cloneValue(value) {
|
|
439
|
-
return structuredClone(toRaw(value));
|
|
440
|
-
}
|
|
441
|
-
/** Manages multi-plate well-plate state with sample assignment, selection, and undo/redo history. */
|
|
442
|
-
function useWellPlateEditor(initialState, options = {}) {
|
|
443
|
-
const { maxHistory = MAX_HISTORY, defaultFormat = 96 } = options;
|
|
444
|
-
const defaultPlate = createEmptyPlate("Plate 1", defaultFormat);
|
|
445
|
-
const internalState = ref({
|
|
446
|
-
plates: initialState?.plates || [defaultPlate],
|
|
447
|
-
activePlateId: initialState?.activePlateId || defaultPlate.id,
|
|
448
|
-
samples: initialState?.samples || [],
|
|
449
|
-
selectedWells: initialState?.selectedWells || [],
|
|
450
|
-
activeSampleId: initialState?.activeSampleId
|
|
451
|
-
});
|
|
452
|
-
const history = ref([]);
|
|
453
|
-
const historyIndex = ref(-1);
|
|
454
|
-
const state = computed(() => internalState.value);
|
|
455
|
-
const plates = computed(() => internalState.value.plates);
|
|
456
|
-
const activePlate = computed(() => internalState.value.plates.find((p) => p.id === internalState.value.activePlateId));
|
|
457
|
-
const samples = computed(() => internalState.value.samples);
|
|
458
|
-
const selectedWells = computed(() => internalState.value.selectedWells);
|
|
459
|
-
const activeSampleId = computed(() => internalState.value.activeSampleId);
|
|
460
|
-
const canUndo = computed(() => historyIndex.value >= 0);
|
|
461
|
-
const canRedo = computed(() => historyIndex.value < history.value.length - 1);
|
|
462
|
-
function saveToHistory() {
|
|
463
|
-
const entry = {
|
|
464
|
-
plates: cloneValue(internalState.value.plates),
|
|
465
|
-
samples: cloneValue(internalState.value.samples)
|
|
466
|
-
};
|
|
467
|
-
if (historyIndex.value < history.value.length - 1) history.value = history.value.slice(0, historyIndex.value + 1);
|
|
468
|
-
history.value.push(entry);
|
|
469
|
-
if (history.value.length > maxHistory) history.value.shift();
|
|
470
|
-
else historyIndex.value++;
|
|
471
|
-
}
|
|
472
|
-
function setActivePlate(plateId) {
|
|
473
|
-
if (internalState.value.plates.some((p) => p.id === plateId)) {
|
|
474
|
-
internalState.value.activePlateId = plateId;
|
|
475
|
-
internalState.value.selectedWells = [];
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
function setActiveSample(sampleId) {
|
|
479
|
-
internalState.value.activeSampleId = sampleId;
|
|
480
|
-
}
|
|
481
|
-
function setSelectedWells(wellIds) {
|
|
482
|
-
internalState.value.selectedWells = wellIds;
|
|
483
|
-
}
|
|
484
|
-
function addPlate(name, format) {
|
|
485
|
-
saveToHistory();
|
|
486
|
-
const plateNumber = internalState.value.plates.length + 1;
|
|
487
|
-
const plate = createEmptyPlate(name || `Plate ${plateNumber}`, format || defaultFormat);
|
|
488
|
-
internalState.value.plates.push(plate);
|
|
489
|
-
internalState.value.activePlateId = plate.id;
|
|
490
|
-
internalState.value.selectedWells = [];
|
|
491
|
-
return plate;
|
|
492
|
-
}
|
|
493
|
-
function removePlate(plateId) {
|
|
494
|
-
if (internalState.value.plates.length <= 1) return;
|
|
495
|
-
saveToHistory();
|
|
496
|
-
const index = internalState.value.plates.findIndex((p) => p.id === plateId);
|
|
497
|
-
if (index === -1) return;
|
|
498
|
-
internalState.value.plates.splice(index, 1);
|
|
499
|
-
if (internalState.value.activePlateId === plateId) {
|
|
500
|
-
internalState.value.activePlateId = internalState.value.plates[0].id;
|
|
501
|
-
internalState.value.selectedWells = [];
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
function addSample(name, color) {
|
|
505
|
-
saveToHistory();
|
|
506
|
-
const sample = {
|
|
507
|
-
id: generateSampleId(),
|
|
508
|
-
name,
|
|
509
|
-
color: color || SAMPLE_COLOR_SCALE[internalState.value.samples.length % SAMPLE_COLOR_SCALE.length],
|
|
510
|
-
count: 0
|
|
511
|
-
};
|
|
512
|
-
internalState.value.samples.push(sample);
|
|
513
|
-
return sample;
|
|
514
|
-
}
|
|
515
|
-
function removeSample(sampleId) {
|
|
516
|
-
saveToHistory();
|
|
517
|
-
const index = internalState.value.samples.findIndex((s) => s.id === sampleId);
|
|
518
|
-
if (index === -1) return;
|
|
519
|
-
internalState.value.samples.splice(index, 1);
|
|
520
|
-
for (const plate of internalState.value.plates) for (const well of Object.values(plate.wells)) if (well.sampleType === sampleId) {
|
|
521
|
-
delete well.sampleType;
|
|
522
|
-
well.state = "empty";
|
|
523
|
-
}
|
|
524
|
-
if (internalState.value.activeSampleId === sampleId) internalState.value.activeSampleId = void 0;
|
|
525
|
-
updateSampleCounts();
|
|
526
|
-
}
|
|
527
|
-
function assignSample(wellIds, sampleId) {
|
|
528
|
-
if (wellIds.length === 0) return;
|
|
529
|
-
saveToHistory();
|
|
530
|
-
const plate = activePlate.value;
|
|
531
|
-
if (!plate) return;
|
|
532
|
-
for (const wellId of wellIds) {
|
|
533
|
-
const well = plate.wells[wellId] || {
|
|
534
|
-
id: wellId,
|
|
535
|
-
row: wellId.charCodeAt(0) - 65,
|
|
536
|
-
col: parseInt(wellId.slice(1)) - 1,
|
|
537
|
-
state: "empty"
|
|
538
|
-
};
|
|
539
|
-
if (sampleId) {
|
|
540
|
-
well.sampleType = sampleId;
|
|
541
|
-
well.state = "filled";
|
|
542
|
-
} else {
|
|
543
|
-
delete well.sampleType;
|
|
544
|
-
well.state = "empty";
|
|
545
|
-
}
|
|
546
|
-
plate.wells[wellId] = well;
|
|
547
|
-
}
|
|
548
|
-
updateSampleCounts();
|
|
549
|
-
}
|
|
550
|
-
function clearWells(wellIds) {
|
|
551
|
-
assignSample(wellIds, void 0);
|
|
552
|
-
}
|
|
553
|
-
function updateSampleCounts() {
|
|
554
|
-
const counts = {};
|
|
555
|
-
for (const plate of internalState.value.plates) for (const well of Object.values(plate.wells)) if (well.sampleType) counts[well.sampleType] = (counts[well.sampleType] || 0) + 1;
|
|
556
|
-
for (const sample of internalState.value.samples) sample.count = counts[sample.id] || 0;
|
|
557
|
-
}
|
|
558
|
-
function undo() {
|
|
559
|
-
if (!canUndo.value) return;
|
|
560
|
-
const entry = history.value[historyIndex.value];
|
|
561
|
-
historyIndex.value--;
|
|
562
|
-
internalState.value.plates = cloneValue(entry.plates);
|
|
563
|
-
internalState.value.samples = cloneValue(entry.samples);
|
|
564
|
-
if (!internalState.value.plates.some((p) => p.id === internalState.value.activePlateId)) internalState.value.activePlateId = internalState.value.plates[0]?.id || "";
|
|
565
|
-
internalState.value.selectedWells = [];
|
|
566
|
-
}
|
|
567
|
-
function redo() {
|
|
568
|
-
if (!canRedo.value) return;
|
|
569
|
-
historyIndex.value++;
|
|
570
|
-
const entry = history.value[historyIndex.value];
|
|
571
|
-
internalState.value.plates = cloneValue(entry.plates);
|
|
572
|
-
internalState.value.samples = cloneValue(entry.samples);
|
|
573
|
-
internalState.value.selectedWells = [];
|
|
574
|
-
}
|
|
575
|
-
function exportData(format) {
|
|
576
|
-
if (format === "json") return JSON.stringify({
|
|
577
|
-
plates: internalState.value.plates,
|
|
578
|
-
samples: internalState.value.samples
|
|
579
|
-
}, null, 2);
|
|
580
|
-
const sampleMap = new Map(internalState.value.samples.map((s) => [s.id, s.name]));
|
|
581
|
-
const rows = ["Plate,Well,Sample Type,Sample Name"];
|
|
582
|
-
for (const plate of internalState.value.plates) for (const [wellId, well] of Object.entries(plate.wells)) if (well.sampleType) {
|
|
583
|
-
const sampleName = sampleMap.get(well.sampleType) || "";
|
|
584
|
-
rows.push(`"${plate.name}","${wellId}","${well.sampleType}","${sampleName}"`);
|
|
585
|
-
}
|
|
586
|
-
return rows.join("\n");
|
|
587
|
-
}
|
|
588
|
-
function importData(data, format) {
|
|
589
|
-
try {
|
|
590
|
-
saveToHistory();
|
|
591
|
-
if (format === "json") {
|
|
592
|
-
const parsed = JSON.parse(data);
|
|
593
|
-
if (parsed.plates && Array.isArray(parsed.plates)) {
|
|
594
|
-
internalState.value.plates = parsed.plates;
|
|
595
|
-
internalState.value.activePlateId = parsed.plates[0]?.id || "";
|
|
596
|
-
}
|
|
597
|
-
if (parsed.samples && Array.isArray(parsed.samples)) internalState.value.samples = parsed.samples;
|
|
598
|
-
internalState.value.selectedWells = [];
|
|
599
|
-
updateSampleCounts();
|
|
600
|
-
return true;
|
|
601
|
-
}
|
|
602
|
-
const lines = data.trim().split("\n");
|
|
603
|
-
if (lines.length < 2) return false;
|
|
604
|
-
const plateMap = /* @__PURE__ */ new Map();
|
|
605
|
-
const sampleMap = /* @__PURE__ */ new Map();
|
|
606
|
-
for (let i = 1; i < lines.length; i++) {
|
|
607
|
-
const parts = lines[i].match(/(?:[^",]+|"[^"]*")+/g);
|
|
608
|
-
if (!parts || parts.length < 4) continue;
|
|
609
|
-
const plateName = parts[0].replace(/^"|"$/g, "");
|
|
610
|
-
const wellId = parts[1].replace(/^"|"$/g, "");
|
|
611
|
-
const sampleId = parts[2].replace(/^"|"$/g, "");
|
|
612
|
-
const sampleName = parts[3].replace(/^"|"$/g, "");
|
|
613
|
-
if (!plateMap.has(plateName)) plateMap.set(plateName, createEmptyPlate(plateName, defaultFormat));
|
|
614
|
-
if (sampleId && !sampleMap.has(sampleId)) sampleMap.set(sampleId, {
|
|
615
|
-
id: sampleId,
|
|
616
|
-
name: sampleName || sampleId,
|
|
617
|
-
color: SAMPLE_COLOR_SCALE[sampleMap.size % SAMPLE_COLOR_SCALE.length],
|
|
618
|
-
count: 0
|
|
619
|
-
});
|
|
620
|
-
const plate = plateMap.get(plateName);
|
|
621
|
-
plate.wells[wellId] = {
|
|
622
|
-
id: wellId,
|
|
623
|
-
row: wellId.charCodeAt(0) - 65,
|
|
624
|
-
col: parseInt(wellId.slice(1)) - 1,
|
|
625
|
-
state: sampleId ? "filled" : "empty",
|
|
626
|
-
sampleType: sampleId || void 0
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
internalState.value.plates = Array.from(plateMap.values());
|
|
630
|
-
internalState.value.samples = Array.from(sampleMap.values());
|
|
631
|
-
internalState.value.activePlateId = internalState.value.plates[0]?.id || "";
|
|
632
|
-
internalState.value.selectedWells = [];
|
|
633
|
-
updateSampleCounts();
|
|
634
|
-
return true;
|
|
635
|
-
} catch {
|
|
636
|
-
return false;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
function loadState(state) {
|
|
640
|
-
saveToHistory();
|
|
641
|
-
if (state.plates && state.plates.length > 0) {
|
|
642
|
-
internalState.value.plates = cloneValue(state.plates);
|
|
643
|
-
internalState.value.activePlateId = state.activePlateId ?? state.plates[0].id;
|
|
644
|
-
}
|
|
645
|
-
if (state.samples) internalState.value.samples = cloneValue(state.samples);
|
|
646
|
-
internalState.value.selectedWells = state.selectedWells ?? [];
|
|
647
|
-
internalState.value.activeSampleId = state.activeSampleId;
|
|
648
|
-
updateSampleCounts();
|
|
649
|
-
}
|
|
650
|
-
function reset() {
|
|
651
|
-
const plate = createEmptyPlate("Plate 1", defaultFormat);
|
|
652
|
-
internalState.value = {
|
|
653
|
-
plates: [plate],
|
|
654
|
-
activePlateId: plate.id,
|
|
655
|
-
samples: [],
|
|
656
|
-
selectedWells: [],
|
|
657
|
-
activeSampleId: void 0
|
|
658
|
-
};
|
|
659
|
-
history.value = [];
|
|
660
|
-
historyIndex.value = -1;
|
|
661
|
-
}
|
|
662
|
-
return {
|
|
663
|
-
state,
|
|
664
|
-
plates,
|
|
665
|
-
activePlate,
|
|
666
|
-
samples,
|
|
667
|
-
selectedWells,
|
|
668
|
-
activeSampleId,
|
|
669
|
-
canUndo,
|
|
670
|
-
canRedo,
|
|
671
|
-
setActivePlate,
|
|
672
|
-
setActiveSample,
|
|
673
|
-
setSelectedWells,
|
|
674
|
-
addPlate,
|
|
675
|
-
removePlate,
|
|
676
|
-
addSample,
|
|
677
|
-
removeSample,
|
|
678
|
-
assignSample,
|
|
679
|
-
clearWells,
|
|
680
|
-
undo,
|
|
681
|
-
redo,
|
|
682
|
-
exportData,
|
|
683
|
-
importData,
|
|
684
|
-
loadState,
|
|
685
|
-
reset
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
//#endregion
|
|
689
|
-
//#region src/composables/useManualLayoutResize.ts
|
|
690
|
-
function clamp(value, min, max) {
|
|
691
|
-
return Math.min(max, Math.max(min, value));
|
|
692
|
-
}
|
|
693
|
-
function availablePanelWidth(containerWidth, constraints) {
|
|
694
|
-
return Math.min(constraints.maxWidth, containerWidth - (constraints.mainMinWidth ?? 0) - (constraints.layoutGap ?? 0));
|
|
695
|
-
}
|
|
696
|
-
function resizedLeadingPanelWidth(startWidth, deltaX, containerWidth, constraints) {
|
|
697
|
-
return Math.round(clamp(startWidth + deltaX, constraints.minWidth, Math.max(constraints.minWidth, availablePanelWidth(containerWidth, constraints))));
|
|
698
|
-
}
|
|
699
|
-
function resizedTrailingPanelWidth(startWidth, deltaX, containerWidth, constraints) {
|
|
700
|
-
return resizedLeadingPanelWidth(startWidth, -deltaX, containerWidth, constraints);
|
|
701
|
-
}
|
|
702
|
-
function resizedVerticalSplitPercent(startPercent, deltaY, containerHeight, constraints) {
|
|
703
|
-
if (containerHeight <= 0) return startPercent;
|
|
704
|
-
return Math.round(clamp(startPercent + deltaY / containerHeight * 100, constraints.minPercent, constraints.maxPercent));
|
|
705
|
-
}
|
|
706
|
-
/** Coordinates pointer-driven resizing for one or more layout splitters. */
|
|
707
|
-
function useManualLayoutResize(options) {
|
|
708
|
-
const activeResize = shallowRef(null);
|
|
709
|
-
const isResizing = computed(() => activeResize.value !== null);
|
|
710
|
-
let previousBodyCursor = "";
|
|
711
|
-
let previousBodyUserSelect = "";
|
|
712
|
-
function stopResize() {
|
|
713
|
-
if (!activeResize.value) return;
|
|
714
|
-
activeResize.value = null;
|
|
715
|
-
window.removeEventListener("pointermove", handlePointerMove);
|
|
716
|
-
window.removeEventListener("pointerup", stopResize);
|
|
717
|
-
window.removeEventListener("pointercancel", stopResize);
|
|
718
|
-
if (typeof document !== "undefined") {
|
|
719
|
-
document.body.style.cursor = previousBodyCursor;
|
|
720
|
-
document.body.style.userSelect = previousBodyUserSelect;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
function handlePointerMove(event) {
|
|
724
|
-
const state = activeResize.value;
|
|
725
|
-
if (!state) return;
|
|
726
|
-
options.resolveTarget(state.kind)?.onResize({
|
|
727
|
-
kind: state.kind,
|
|
728
|
-
startValue: state.startValue,
|
|
729
|
-
deltaX: event.clientX - state.startClientX,
|
|
730
|
-
deltaY: event.clientY - state.startClientY,
|
|
731
|
-
containerRect: state.containerRect,
|
|
732
|
-
secondaryRect: state.secondaryRect
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
function startResize(kind, event) {
|
|
736
|
-
if (!toValue(options.enabled) || event.button !== 0) return;
|
|
737
|
-
const target = options.resolveTarget(kind);
|
|
738
|
-
const containerRect = target?.getContainerElement()?.getBoundingClientRect();
|
|
739
|
-
if (!target || !containerRect) return;
|
|
740
|
-
event.preventDefault();
|
|
741
|
-
if (event.currentTarget instanceof HTMLElement) event.currentTarget.setPointerCapture(event.pointerId);
|
|
742
|
-
const secondary = target.getSecondaryElement?.() ?? null;
|
|
743
|
-
activeResize.value = {
|
|
744
|
-
kind,
|
|
745
|
-
startClientX: event.clientX,
|
|
746
|
-
startClientY: event.clientY,
|
|
747
|
-
startValue: target.getStartValue(),
|
|
748
|
-
containerRect,
|
|
749
|
-
secondaryRect: secondary?.getBoundingClientRect() ?? null,
|
|
750
|
-
cursor: target.cursor ?? "col-resize"
|
|
751
|
-
};
|
|
752
|
-
if (typeof document !== "undefined") {
|
|
753
|
-
previousBodyCursor = document.body.style.cursor;
|
|
754
|
-
previousBodyUserSelect = document.body.style.userSelect;
|
|
755
|
-
document.body.style.cursor = activeResize.value.cursor;
|
|
756
|
-
document.body.style.userSelect = "none";
|
|
757
|
-
}
|
|
758
|
-
window.addEventListener("pointermove", handlePointerMove);
|
|
759
|
-
window.addEventListener("pointerup", stopResize);
|
|
760
|
-
window.addEventListener("pointercancel", stopResize);
|
|
761
|
-
}
|
|
762
|
-
function isResizingKind(kind) {
|
|
763
|
-
return activeResize.value?.kind === kind;
|
|
764
|
-
}
|
|
765
|
-
watch(() => toValue(options.enabled), (enabled) => {
|
|
766
|
-
if (!enabled) stopResize();
|
|
767
|
-
});
|
|
768
|
-
onBeforeUnmount(stopResize);
|
|
769
|
-
return {
|
|
770
|
-
activeResize,
|
|
771
|
-
isResizing,
|
|
772
|
-
isResizingKind,
|
|
773
|
-
startResize,
|
|
774
|
-
stopResize
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
|
-
//#endregion
|
|
778
|
-
//#region src/composables/useFileBrowser.ts
|
|
779
|
-
/**
|
|
780
|
-
* Browses the platform's `/api/filesystem` routes for a custom file UI. For the
|
|
781
|
-
* ready-made dialog, use `FilePicker` with `usePlatformFilePickerAdapter()`.
|
|
782
|
-
*
|
|
783
|
-
* Requires the `filesystem.browse` permission and a deployment that declares
|
|
784
|
-
* mounts; with none configured the routes are absent and `mounts` stays empty.
|
|
785
|
-
*/
|
|
786
|
-
function useFileBrowser(options = {}) {
|
|
787
|
-
const injectedContext = getInjectedPlatformContext();
|
|
788
|
-
const api = useApi({ baseUrl: options.apiBaseUrl ?? injectedContext?.platformApiUrl });
|
|
789
|
-
const sync = useRequestSyncState("Could not read the server mounts.");
|
|
790
|
-
const mounts = ref([]);
|
|
791
|
-
const mountId = shallowRef("");
|
|
792
|
-
const path = shallowRef("");
|
|
793
|
-
const parent = ref(null);
|
|
794
|
-
const entries = ref([]);
|
|
795
|
-
const breadcrumbs = ref([]);
|
|
796
|
-
const totalCount = ref(null);
|
|
797
|
-
const matchCount = ref(null);
|
|
798
|
-
const shownCount = ref(null);
|
|
799
|
-
const truncated = ref(false);
|
|
800
|
-
const search = shallowRef("");
|
|
801
|
-
const sort = shallowRef("name");
|
|
802
|
-
const selected = ref([]);
|
|
803
|
-
const mountsLoading = shallowRef(false);
|
|
804
|
-
const listingLoading = shallowRef(false);
|
|
805
|
-
const listingReady = shallowRef(false);
|
|
806
|
-
const isLoading = computed(() => mountsLoading.value || listingLoading.value);
|
|
807
|
-
let listToken = 0;
|
|
808
|
-
let mountsToken = 0;
|
|
809
|
-
let disposed = false;
|
|
810
|
-
let listingController;
|
|
811
|
-
let mountsController;
|
|
812
|
-
let searchTimer;
|
|
813
|
-
function resetListing() {
|
|
814
|
-
listingReady.value = false;
|
|
815
|
-
entries.value = [];
|
|
816
|
-
breadcrumbs.value = [];
|
|
817
|
-
parent.value = null;
|
|
818
|
-
totalCount.value = null;
|
|
819
|
-
matchCount.value = null;
|
|
820
|
-
shownCount.value = null;
|
|
821
|
-
truncated.value = false;
|
|
822
|
-
}
|
|
823
|
-
function typeRuleParams() {
|
|
824
|
-
return (toValue(options.typeRules) ?? []).map((rule) => `&type_rules=${encodeURIComponent(rule)}`).join("");
|
|
825
|
-
}
|
|
826
|
-
/** Prefer the API's own `detail` over axios's "Request failed with status N". */
|
|
827
|
-
function describe(cause, fallback) {
|
|
828
|
-
const detail = cause?.response?.data?.detail;
|
|
829
|
-
return typeof detail === "string" && detail ? detail : sync.readErrorMessage(cause, fallback);
|
|
830
|
-
}
|
|
831
|
-
function fail(cause, fallback) {
|
|
832
|
-
sync.setError(new Error(describe(cause, fallback)), fallback);
|
|
833
|
-
}
|
|
834
|
-
function statusOf(cause) {
|
|
835
|
-
return cause?.response?.status;
|
|
836
|
-
}
|
|
837
|
-
async function loadMounts() {
|
|
838
|
-
const token = ++mountsToken;
|
|
839
|
-
mountsController?.abort();
|
|
840
|
-
mountsController = new AbortController();
|
|
841
|
-
mountsLoading.value = true;
|
|
842
|
-
try {
|
|
843
|
-
const data = await api.get("/filesystem/mounts", { signal: mountsController.signal });
|
|
844
|
-
if (disposed || token !== mountsToken) return false;
|
|
845
|
-
mounts.value = data.mounts ?? [];
|
|
846
|
-
sync.clearError();
|
|
847
|
-
sync.markLoaded();
|
|
848
|
-
return true;
|
|
849
|
-
} catch (cause) {
|
|
850
|
-
if (disposed || token !== mountsToken) return false;
|
|
851
|
-
if (statusOf(cause) === 404) {
|
|
852
|
-
mounts.value = [];
|
|
853
|
-
sync.clearError();
|
|
854
|
-
sync.markLoaded();
|
|
855
|
-
return true;
|
|
856
|
-
}
|
|
857
|
-
mounts.value = [];
|
|
858
|
-
fail(cause, "Could not read the server mounts.");
|
|
859
|
-
return false;
|
|
860
|
-
} finally {
|
|
861
|
-
if (token === mountsToken) mountsLoading.value = false;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
async function navigate(location, refresh = false) {
|
|
865
|
-
if (disposed || !location.mount_id) return;
|
|
866
|
-
clearTimeout(searchTimer);
|
|
867
|
-
const token = ++listToken;
|
|
868
|
-
listingController?.abort();
|
|
869
|
-
listingController = new AbortController();
|
|
870
|
-
listingLoading.value = true;
|
|
871
|
-
mountId.value = location.mount_id;
|
|
872
|
-
path.value = location.path;
|
|
873
|
-
resetListing();
|
|
874
|
-
sync.clearError();
|
|
875
|
-
try {
|
|
876
|
-
const query = `mount_id=${encodeURIComponent(location.mount_id)}&path=${encodeURIComponent(location.path)}&sort=${sort.value}` + (refresh ? "&refresh=true" : "") + (search.value ? `&search=${encodeURIComponent(search.value)}` : "") + typeRuleParams();
|
|
877
|
-
const listing = await api.get(`/filesystem/browse?${query}`, { signal: listingController.signal });
|
|
878
|
-
if (disposed || token !== listToken) return;
|
|
879
|
-
mountId.value = listing.mount_id;
|
|
880
|
-
path.value = listing.path;
|
|
881
|
-
parent.value = listing.parent ?? null;
|
|
882
|
-
entries.value = listing.entries ?? [];
|
|
883
|
-
breadcrumbs.value = listing.breadcrumbs ?? [];
|
|
884
|
-
totalCount.value = listing.total_count ?? null;
|
|
885
|
-
matchCount.value = listing.match_count ?? null;
|
|
886
|
-
shownCount.value = listing.shown_count ?? listing.entries?.length ?? null;
|
|
887
|
-
truncated.value = listing.truncated ?? false;
|
|
888
|
-
listingReady.value = true;
|
|
889
|
-
sync.clearError();
|
|
890
|
-
sync.markLoaded();
|
|
891
|
-
} catch (cause) {
|
|
892
|
-
if (disposed || token !== listToken) return;
|
|
893
|
-
entries.value = [];
|
|
894
|
-
fail(cause, "Could not read that folder.");
|
|
895
|
-
} finally {
|
|
896
|
-
if (token === listToken) listingLoading.value = false;
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
async function init() {
|
|
900
|
-
if (disposed) return;
|
|
901
|
-
const navigationToken = listToken;
|
|
902
|
-
if (!await loadMounts() || disposed || navigationToken !== listToken) return;
|
|
903
|
-
const preferred = toValue(options.initialMountId);
|
|
904
|
-
const reachable = (mount) => mount.available !== false;
|
|
905
|
-
const target = mounts.value.find((mount) => mount.id === preferred && reachable(mount)) ?? mounts.value.find(reachable);
|
|
906
|
-
if (target) await navigate({
|
|
907
|
-
mount_id: target.id,
|
|
908
|
-
path: ""
|
|
909
|
-
});
|
|
910
|
-
else {
|
|
911
|
-
mountId.value = "";
|
|
912
|
-
path.value = "";
|
|
913
|
-
resetListing();
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
async function refresh() {
|
|
917
|
-
if (disposed) return;
|
|
918
|
-
if (mountId.value) await navigate({
|
|
919
|
-
mount_id: mountId.value,
|
|
920
|
-
path: path.value
|
|
921
|
-
}, true);
|
|
922
|
-
else await init();
|
|
923
|
-
}
|
|
924
|
-
function selectCurrentFolder() {
|
|
925
|
-
if (!mountId.value || !listingReady.value || isLoading.value || sync.error.value) return;
|
|
926
|
-
const crumb = breadcrumbs.value[breadcrumbs.value.length - 1];
|
|
927
|
-
const key = JSON.stringify([mountId.value, path.value]);
|
|
928
|
-
if (selected.value.some((item) => JSON.stringify([item.mount_id, item.path]) === key)) return;
|
|
929
|
-
selected.value = [...selected.value, {
|
|
930
|
-
mount_id: mountId.value,
|
|
931
|
-
path: path.value,
|
|
932
|
-
name: crumb?.label ?? path.value,
|
|
933
|
-
kind: "directory",
|
|
934
|
-
child_count: totalCount.value
|
|
935
|
-
}];
|
|
936
|
-
}
|
|
937
|
-
function clear() {
|
|
938
|
-
selected.value = [];
|
|
939
|
-
}
|
|
940
|
-
watch(search, () => {
|
|
941
|
-
if (!mountId.value) return;
|
|
942
|
-
clearTimeout(searchTimer);
|
|
943
|
-
searchTimer = setTimeout(() => {
|
|
944
|
-
navigate({
|
|
945
|
-
mount_id: mountId.value,
|
|
946
|
-
path: path.value
|
|
947
|
-
});
|
|
948
|
-
}, 250);
|
|
949
|
-
});
|
|
950
|
-
watch(sort, () => {
|
|
951
|
-
if (mountId.value) navigate({
|
|
952
|
-
mount_id: mountId.value,
|
|
953
|
-
path: path.value
|
|
954
|
-
});
|
|
955
|
-
});
|
|
956
|
-
watch(() => toValue(options.typeRules), () => {
|
|
957
|
-
if (mountId.value) navigate({
|
|
958
|
-
mount_id: mountId.value,
|
|
959
|
-
path: path.value
|
|
960
|
-
});
|
|
961
|
-
}, { deep: true });
|
|
962
|
-
if (getCurrentScope()) onScopeDispose(() => {
|
|
963
|
-
disposed = true;
|
|
964
|
-
++listToken;
|
|
965
|
-
++mountsToken;
|
|
966
|
-
clearTimeout(searchTimer);
|
|
967
|
-
listingController?.abort();
|
|
968
|
-
mountsController?.abort();
|
|
969
|
-
});
|
|
970
|
-
return {
|
|
971
|
-
mounts,
|
|
972
|
-
mountId,
|
|
973
|
-
path,
|
|
974
|
-
parent,
|
|
975
|
-
entries,
|
|
976
|
-
breadcrumbs,
|
|
977
|
-
totalCount,
|
|
978
|
-
matchCount,
|
|
979
|
-
shownCount,
|
|
980
|
-
truncated,
|
|
981
|
-
search,
|
|
982
|
-
sort,
|
|
983
|
-
selected,
|
|
984
|
-
isLoading,
|
|
985
|
-
error: sync.error,
|
|
986
|
-
init,
|
|
987
|
-
navigate,
|
|
988
|
-
refresh,
|
|
989
|
-
selectCurrentFolder,
|
|
990
|
-
clear
|
|
991
|
-
};
|
|
992
|
-
}
|
|
993
|
-
//#endregion
|
|
994
|
-
//#region src/composables/analysisArtifactTarget.ts
|
|
995
|
-
var EXPERIMENT_ID = "experiment_id";
|
|
996
|
-
var ARTIFACT_PLUGIN_ID = "artifact_plugin_id";
|
|
997
|
-
var ARTIFACT_KEY = "artifact_key";
|
|
998
|
-
var ARTIFACT_ID = "artifact_id";
|
|
999
|
-
function parsePositiveInteger(value) {
|
|
1000
|
-
if (value === null || !/^\d+$/.test(value)) return void 0;
|
|
1001
|
-
const parsed = Number(value);
|
|
1002
|
-
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
1003
|
-
}
|
|
1004
|
-
/** Build the canonical query used to open one analysis artifact in a plugin. */
|
|
1005
|
-
function buildAnalysisArtifactTargetQuery(target) {
|
|
1006
|
-
const query = new URLSearchParams();
|
|
1007
|
-
query.set(EXPERIMENT_ID, String(target.experimentId));
|
|
1008
|
-
query.set(ARTIFACT_PLUGIN_ID, target.artifactPluginId);
|
|
1009
|
-
query.set(ARTIFACT_KEY, target.artifactKey);
|
|
1010
|
-
if (target.artifactId !== void 0) query.set(ARTIFACT_ID, String(target.artifactId));
|
|
1011
|
-
return query;
|
|
1012
|
-
}
|
|
1013
|
-
/** Parse a complete canonical target. Incomplete or invalid input is not guessed. */
|
|
1014
|
-
function parseAnalysisArtifactTarget(query) {
|
|
1015
|
-
const experimentId = parsePositiveInteger(query.get(EXPERIMENT_ID));
|
|
1016
|
-
const artifactPluginId = query.get(ARTIFACT_PLUGIN_ID)?.trim();
|
|
1017
|
-
const artifactKey = query.get(ARTIFACT_KEY)?.trim();
|
|
1018
|
-
const rawArtifactId = query.get(ARTIFACT_ID);
|
|
1019
|
-
const artifactId = parsePositiveInteger(rawArtifactId);
|
|
1020
|
-
if (experimentId === void 0 || !artifactPluginId || !artifactKey || rawArtifactId !== null && artifactId === void 0) return null;
|
|
1021
|
-
return {
|
|
1022
|
-
experimentId,
|
|
1023
|
-
artifactPluginId,
|
|
1024
|
-
artifactKey,
|
|
1025
|
-
...artifactId === void 0 ? {} : { artifactId }
|
|
1026
|
-
};
|
|
1027
|
-
}
|
|
1028
|
-
//#endregion
|
|
1029
|
-
//#region src/composables/createFilePickerAdapter.ts
|
|
1030
|
-
/** Opaque picker identity; decode before passing a selection to a mount-scoped API. */
|
|
1031
|
-
function encodePlatformPickerPath(mountId, path = "") {
|
|
1032
|
-
return JSON.stringify([mountId, path]);
|
|
1033
|
-
}
|
|
1034
|
-
function decodePlatformPickerPath(path) {
|
|
1035
|
-
const decoded = JSON.parse(path);
|
|
1036
|
-
if (!Array.isArray(decoded) || decoded.length !== 2 || !decoded.every((item) => typeof item === "string") || !decoded[0]) throw new Error("Invalid platform picker path.");
|
|
1037
|
-
return {
|
|
1038
|
-
mountId: decoded[0],
|
|
1039
|
-
path: decoded[1]
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
function isRelativePath(path) {
|
|
1043
|
-
return path === "" || !/^[a-z]:/i.test(path) && !/[\\\x00-\x1f\x7f]/.test(path) && path.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
1044
|
-
}
|
|
1045
|
-
/** Mount browsing and search shared by platform and generated plugin clients. */
|
|
1046
|
-
function createFilePickerAdapter(transport, options = {}) {
|
|
1047
|
-
const root = options.rootLocation && { ...options.rootLocation };
|
|
1048
|
-
const mountLabels = /* @__PURE__ */ new Map();
|
|
1049
|
-
const listings = /* @__PURE__ */ new Map();
|
|
1050
|
-
let generation = 0;
|
|
1051
|
-
function invalidate() {
|
|
1052
|
-
generation++;
|
|
1053
|
-
listings.clear();
|
|
1054
|
-
mountLabels.clear();
|
|
1055
|
-
}
|
|
1056
|
-
function validateLocation(location) {
|
|
1057
|
-
if (!location.mount_id || !isRelativePath(location.path) || root && (location.mount_id !== root.mount_id || root.path !== "" && location.path !== root.path && !location.path.startsWith(`${root.path}/`))) throw new Error("Invalid file picker location.");
|
|
1058
|
-
}
|
|
1059
|
-
if (root) validateLocation(root);
|
|
1060
|
-
function begin(request) {
|
|
1061
|
-
request?.signal?.throwIfAborted();
|
|
1062
|
-
if (request?.refresh) invalidate();
|
|
1063
|
-
return generation;
|
|
1064
|
-
}
|
|
1065
|
-
function check(request, token) {
|
|
1066
|
-
request?.signal?.throwIfAborted();
|
|
1067
|
-
if (token !== generation) throw new DOMException("Cancelled", "AbortError");
|
|
1068
|
-
}
|
|
1069
|
-
async function read(path, request, token) {
|
|
1070
|
-
check(request, token);
|
|
1071
|
-
const decoded = path ? decodePlatformPickerPath(path) : void 0;
|
|
1072
|
-
const location = decoded ? {
|
|
1073
|
-
mount_id: decoded.mountId,
|
|
1074
|
-
path: decoded.path
|
|
1075
|
-
} : root;
|
|
1076
|
-
if (location) validateLocation(location);
|
|
1077
|
-
const key = location ? encodePlatformPickerPath(location.mount_id, location.path) : "";
|
|
1078
|
-
const cached = listings.get(key);
|
|
1079
|
-
if (cached) return cached;
|
|
1080
|
-
let nodes;
|
|
1081
|
-
if (!location) {
|
|
1082
|
-
const mounts = await transport.listMounts(request);
|
|
1083
|
-
check(request, token);
|
|
1084
|
-
nodes = mounts.map((mount) => {
|
|
1085
|
-
mountLabels.set(mount.id, mount.label);
|
|
1086
|
-
return {
|
|
1087
|
-
kind: "folder",
|
|
1088
|
-
path: encodePlatformPickerPath(mount.id),
|
|
1089
|
-
name: mount.label,
|
|
1090
|
-
parentPath: null,
|
|
1091
|
-
parentName: "Server mounts",
|
|
1092
|
-
unavailableReason: mount.available === false ? "This mount is offline." : void 0
|
|
1093
|
-
};
|
|
1094
|
-
});
|
|
1095
|
-
} else {
|
|
1096
|
-
const listing = await transport.browse(location, request);
|
|
1097
|
-
check(request, token);
|
|
1098
|
-
if (listing.truncated) throw new Error("This directory exceeds the server listing limit. Choose a smaller data directory.");
|
|
1099
|
-
const atRoot = root && location.mount_id === root.mount_id && location.path === root.path;
|
|
1100
|
-
nodes = (listing.entries ?? []).map((entry) => {
|
|
1101
|
-
if (!isRelativePath(entry.path) || !entry.name || entry.path !== `${location.path ? `${location.path}/` : ""}${entry.name}` || entry.name.includes("/")) throw new Error("Invalid file picker directory entry.");
|
|
1102
|
-
const common = {
|
|
1103
|
-
path: encodePlatformPickerPath(location.mount_id, entry.path),
|
|
1104
|
-
parentPath: atRoot ? null : key,
|
|
1105
|
-
parentName: `${mountLabels.get(location.mount_id) ?? location.mount_id}${location.path ? ` / ${location.path}` : ""}`,
|
|
1106
|
-
name: entry.name,
|
|
1107
|
-
bytes: entry.size_bytes,
|
|
1108
|
-
modifiedAt: entry.modified_at
|
|
1109
|
-
};
|
|
1110
|
-
return entry.kind === "directory" ? {
|
|
1111
|
-
...common,
|
|
1112
|
-
kind: "folder"
|
|
1113
|
-
} : {
|
|
1114
|
-
...common,
|
|
1115
|
-
kind: "file",
|
|
1116
|
-
format: pickerFormat(entry.name)
|
|
1117
|
-
};
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
listings.set(key, nodes);
|
|
1121
|
-
return nodes;
|
|
1122
|
-
}
|
|
1123
|
-
return {
|
|
1124
|
-
mounts: !root,
|
|
1125
|
-
mountOf(path) {
|
|
1126
|
-
try {
|
|
1127
|
-
return encodePlatformPickerPath(decodePlatformPickerPath(path).mountId);
|
|
1128
|
-
} catch {
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
},
|
|
1132
|
-
invalidate,
|
|
1133
|
-
listRoot: async (request) => read("", request, begin(request)),
|
|
1134
|
-
listChildren: async (path, request) => read(path, request, begin(request)),
|
|
1135
|
-
async search(query, scope, request) {
|
|
1136
|
-
const token = begin(request);
|
|
1137
|
-
const matches = [];
|
|
1138
|
-
const queue = [scope.path];
|
|
1139
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1140
|
-
const needle = query.toLowerCase();
|
|
1141
|
-
let currentRequest = request;
|
|
1142
|
-
while (queue.length) {
|
|
1143
|
-
check(request, token);
|
|
1144
|
-
const path = queue.shift();
|
|
1145
|
-
if (visited.has(path)) continue;
|
|
1146
|
-
visited.add(path);
|
|
1147
|
-
if (visited.size > 500) throw new Error("Search reached its folder limit. Search a smaller location or turn off Subfolders.");
|
|
1148
|
-
const nodes = await read(path, currentRequest, token);
|
|
1149
|
-
check(request, token);
|
|
1150
|
-
if (currentRequest?.refresh) {
|
|
1151
|
-
currentRequest = { ...currentRequest };
|
|
1152
|
-
delete currentRequest.refresh;
|
|
1153
|
-
}
|
|
1154
|
-
for (const node of nodes) {
|
|
1155
|
-
if (node.name.toLowerCase().includes(needle)) matches.push(node);
|
|
1156
|
-
if (scope.recursive && node.kind === "folder" && !node.unavailableReason) queue.push(node.path);
|
|
1157
|
-
}
|
|
1158
|
-
if (matches.length > 1e4) throw new Error("Too many search results. Use a more specific search.");
|
|
1159
|
-
}
|
|
1160
|
-
return matches;
|
|
1161
|
-
}
|
|
1162
|
-
};
|
|
1163
|
-
}
|
|
1164
|
-
//#endregion
|
|
1165
|
-
//#region src/composables/usePlatformFilePickerAdapter.ts
|
|
1166
|
-
/** Adapt the authenticated MINT mount API without exposing absolute host paths. */
|
|
1167
|
-
function usePlatformFilePickerAdapter(options = {}) {
|
|
1168
|
-
const api = useApi({ baseUrl: options.apiBaseUrl ?? getInjectedPlatformContext()?.platformApiUrl });
|
|
1169
|
-
return createFilePickerAdapter({
|
|
1170
|
-
async listMounts(request) {
|
|
1171
|
-
try {
|
|
1172
|
-
const url = `/filesystem/mounts${request?.refresh ? "?refresh=true" : ""}`;
|
|
1173
|
-
const { mounts } = await api.get(url, { signal: request?.signal });
|
|
1174
|
-
return mounts;
|
|
1175
|
-
} catch (cause) {
|
|
1176
|
-
if (cause.response?.status === 404) return [];
|
|
1177
|
-
throw cause;
|
|
1178
|
-
}
|
|
1179
|
-
},
|
|
1180
|
-
browse(location, request) {
|
|
1181
|
-
const query = new URLSearchParams({
|
|
1182
|
-
mount_id: location.mount_id,
|
|
1183
|
-
path: location.path
|
|
1184
|
-
});
|
|
1185
|
-
if (request?.refresh) query.set("refresh", "true");
|
|
1186
|
-
return api.get(`/filesystem/browse?${query}`, { signal: request?.signal });
|
|
1187
|
-
}
|
|
1188
|
-
}, options);
|
|
1189
|
-
}
|
|
1190
|
-
//#endregion
|
|
1191
|
-
export { buildAnalysisArtifactTargetQuery as a, resizedLeadingPanelWidth as c, useManualLayoutResize as d, useWellPlateEditor as f, useAuth as h, encodePlatformPickerPath as i, resizedTrailingPanelWidth as l, usePasskey as m, createFilePickerAdapter as n, parseAnalysisArtifactTarget as o, resolvePluginFrontendBase as p, decodePlatformPickerPath as r, useFileBrowser as s, usePlatformFilePickerAdapter as t, resizedVerticalSplitPercent as u };
|
|
1192
|
-
|
|
1193
|
-
//# sourceMappingURL=composables-DL4Vca7L.js.map
|