@morscherlab/mint-sdk 1.2.0-beta.9 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/{ExperimentPopover-oL0fg_A0.js → ExperimentPopover-CJdNlCZb.js} +21 -14
- package/dist/ExperimentPopover-CJdNlCZb.js.map +1 -0
- package/dist/{ExperimentSelectorModal-BeEOomw_.js → ExperimentSelectorModal-DHE9k8TP.js} +5 -5
- package/dist/{ExperimentSelectorModal-BeEOomw_.js.map → ExperimentSelectorModal-DHE9k8TP.js.map} +1 -1
- package/dist/{SettingsModal-Dphh3RrI.js → SettingsModal-DOcCf0Vo.js} +41 -18
- package/dist/SettingsModal-DOcCf0Vo.js.map +1 -0
- package/dist/{Skeleton-DxtwJ5gt.js → Skeleton-jhF9wUkU.js} +19 -9
- package/dist/Skeleton-jhF9wUkU.js.map +1 -0
- package/dist/__tests__/apiErrors.test.d.ts +1 -0
- package/dist/__tests__/components/FileBrowserModal.test.d.ts +1 -0
- package/dist/__tests__/components/FilePicker.state.test.d.ts +1 -0
- package/dist/__tests__/components/FilePicker.test.d.ts +1 -0
- package/dist/__tests__/components/FormField.test.d.ts +1 -0
- package/dist/__tests__/components/NumberInput.test.d.ts +1 -0
- package/dist/__tests__/components/PlotlyChart.test.d.ts +1 -0
- package/dist/__tests__/components/iconButtonIconSizing.test.d.ts +1 -0
- package/dist/__tests__/composables/localFiles.test.d.ts +1 -0
- package/dist/__tests__/composables/useFileBrowser.test.d.ts +1 -0
- package/dist/__tests__/composables/usePickerSelection.test.d.ts +1 -0
- package/dist/__tests__/composables/usePlatformFilePickerAdapter.test.d.ts +1 -0
- package/dist/__tests__/styles/tokenOnlyColors.test.d.ts +1 -0
- package/dist/apiErrors.d.ts +26 -0
- package/dist/components/AppSidebar.vue.d.ts +7 -7
- package/dist/components/BaseInput.vue.d.ts +1 -1
- package/dist/components/BasePill.vue.d.ts +3 -0
- package/dist/components/BaseSelect.vue.d.ts +1 -1
- package/dist/components/BaseTabs.vue.d.ts +1 -4
- package/dist/components/BaseTextarea.vue.d.ts +1 -1
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
- package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +2 -2
- package/dist/components/ConcentrationInput.vue.d.ts +1 -1
- package/dist/components/DatePicker.vue.d.ts +1 -1
- package/dist/components/DateTimePicker.vue.d.ts +1 -1
- package/dist/components/FileBrowserModal.vue.d.ts +99 -0
- package/dist/components/FilePicker.vue.d.ts +32 -0
- package/dist/components/FileUploader.vue.d.ts +8 -2
- package/dist/components/FormField.vue.d.ts +8 -1
- package/dist/components/FormulaInput.vue.d.ts +1 -1
- package/dist/components/GeneratedPlotlyResult.vue.d.ts +1 -3
- package/dist/components/MobileSupportGate.vue.d.ts +1 -1
- package/dist/components/MoleculeInput.vue.d.ts +1 -1
- package/dist/components/NumberInput.vue.d.ts +1 -1
- package/dist/components/PlotlyChart.vue.d.ts +59 -0
- package/dist/components/PluginWorkspaceView.props.d.ts +3 -3
- package/dist/components/ResourceCard.vue.d.ts +1 -1
- package/dist/components/SecretInput.vue.d.ts +1 -1
- package/dist/components/SequenceInput.vue.d.ts +1 -1
- package/dist/components/StatusIndicator.vue.d.ts +1 -1
- package/dist/components/TagsInput.vue.d.ts +1 -1
- package/dist/components/TimePicker.vue.d.ts +1 -1
- package/dist/components/TimeRangeInput.vue.d.ts +1 -1
- package/dist/components/UnitInput.vue.d.ts +1 -1
- package/dist/components/WellPlate.vue.d.ts +1 -1
- package/dist/components/fileBrowserPresentation.d.ts +10 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +8 -8
- package/dist/components/internal/FileBrowserDialogInternal.vue.d.ts +34 -0
- package/dist/components/internal/FileBrowserListingInternal.vue.d.ts +25 -0
- package/dist/components/internal/FileBrowserToolbarInternal.vue.d.ts +26 -0
- package/dist/components/internal/FilePickerPreviewInternal.vue.d.ts +18 -0
- package/dist/components/internal/FilePickerTreeInternal.vue.d.ts +31 -0
- package/dist/{components-CgAUzF8F.js → components-Cp8DcNrU.js} +4363 -1730
- package/dist/components-Cp8DcNrU.js.map +1 -0
- package/dist/composables/filePicker/localFiles.d.ts +3 -0
- package/dist/composables/filePicker/usePickerNavigation.d.ts +96 -0
- package/dist/composables/filePicker/usePickerSelection.d.ts +31 -0
- package/dist/composables/filePicker/validation.d.ts +5 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/index.js +10 -10
- package/dist/composables/pluginWorkspaceModel.d.ts +0 -3
- package/dist/composables/useApi.d.ts +14 -7
- package/dist/composables/useFileBrowser.d.ts +55 -0
- package/dist/composables/useFilePicker.d.ts +189 -0
- package/dist/composables/usePlatformFilePickerAdapter.d.ts +11 -0
- package/dist/composables/useToast.d.ts +22 -1
- package/dist/{composables-B_pia3hO.js → composables-D_4qqOlp.js} +300 -11
- package/dist/composables-D_4qqOlp.js.map +1 -0
- package/dist/{errors-_W0-LbA6.js → errors-BLuqMee5.js} +78 -15
- package/dist/errors-BLuqMee5.js.map +1 -0
- package/dist/{experiment-Cwl9AYxw.js → experiment-CVDRAnqp.js} +2 -2
- package/dist/{experiment-Cwl9AYxw.js.map → experiment-CVDRAnqp.js.map} +1 -1
- package/dist/{experiment-utils-DV3DrvE_.js → experiment-utils-CV2Wza3z.js} +1 -1
- package/dist/{experiment-utils-DV3DrvE_.js.map → experiment-utils-CV2Wza3z.js.map} +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -15
- package/dist/install.js +2 -2
- package/dist/stores/index.js +2 -2
- package/dist/styles.css +2957 -1708
- package/dist/templates/index.js +2 -2
- package/dist/{templates-Qq113pyZ.js → templates-Dd9bBSs6.js} +2 -2
- package/dist/{templates-Qq113pyZ.js.map → templates-Dd9bBSs6.js.map} +1 -1
- package/dist/types/components.d.ts +21 -0
- package/dist/types/fileBrowserTypes.d.ts +77 -0
- package/dist/types/filePicker.d.ts +77 -0
- package/dist/types/form-builder.d.ts +2 -0
- package/dist/types/generated-ui.d.ts +2 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/{useControlSchema-aMZlb4Sc.js → useControlSchema-BB2GRPhW.js} +190 -105
- package/dist/useControlSchema-BB2GRPhW.js.map +1 -0
- package/dist/{useDropdownState-DitCtrTT.js → useDropdownState-C7hRiVQf.js} +2 -2
- package/dist/{useDropdownState-DitCtrTT.js.map → useDropdownState-C7hRiVQf.js.map} +1 -1
- package/dist/{useExperimentSelector-I8HC8hUu.js → useExperimentSelector-G_CAOFTX.js} +4 -4
- package/dist/{useExperimentSelector-I8HC8hUu.js.map → useExperimentSelector-G_CAOFTX.js.map} +1 -1
- package/dist/{useFormBuilder-TCxNnqeb.js → useFormBuilder-BnGSdSRG.js} +2 -2
- package/dist/{useFormBuilder-TCxNnqeb.js.map → useFormBuilder-BnGSdSRG.js.map} +1 -1
- package/dist/{useProtocolTemplates-BmX_iVBR.js → validation-CTxuzLIX.js} +70 -13
- package/dist/validation-CTxuzLIX.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/apiErrors.test.ts +81 -0
- package/src/__tests__/components/AppSidebar.test.ts +84 -94
- package/src/__tests__/components/AppToastContainer.test.ts +55 -1
- package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +0 -21
- package/src/__tests__/components/ControlWorkspaceView.test.ts +0 -2
- package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +0 -2
- package/src/__tests__/components/ExperimentPopover.test.ts +30 -0
- package/src/__tests__/components/FileBrowserModal.test.ts +460 -0
- package/src/__tests__/components/FilePicker.state.test.ts +287 -0
- package/src/__tests__/components/FilePicker.test.ts +487 -0
- package/src/__tests__/components/FormBuilder.test.ts +26 -0
- package/src/__tests__/components/FormField.test.ts +26 -0
- package/src/__tests__/components/GeneratedPathInput.test.ts +18 -0
- package/src/__tests__/components/GeneratedPlotlyResult.test.ts +52 -5
- package/src/__tests__/components/NumberInput.test.ts +105 -0
- package/src/__tests__/components/PlotlyChart.test.ts +305 -0
- package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +1 -2
- package/src/__tests__/components/PluginWorkspaceView.test.ts +1 -2
- package/src/__tests__/components/SegmentedControl.test.ts +19 -0
- package/src/__tests__/components/iconButtonIconSizing.test.ts +18 -0
- package/src/__tests__/composables/localFiles.test.ts +57 -0
- package/src/__tests__/composables/useApi.test.ts +92 -9
- package/src/__tests__/composables/useFileBrowser.test.ts +305 -0
- package/src/__tests__/composables/useGeneratedAnalysis.test.ts +7 -2
- package/src/__tests__/composables/usePickerSelection.test.ts +98 -0
- package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +67 -0
- package/src/__tests__/composables/usePluginClient.test.ts +3 -0
- package/src/__tests__/generatedUi.test.ts +111 -0
- package/src/__tests__/styles/tokenOnlyColors.test.ts +16 -0
- package/src/apiErrors.ts +85 -0
- package/src/components/AppLayout.story.vue +1 -1
- package/src/components/AppSidebar.story.vue +7 -10
- package/src/components/AppSidebar.vue +178 -58
- package/src/components/AppToastContainer.vue +65 -23
- package/src/components/BasePill.story.vue +2 -0
- package/src/components/BasePill.vue +4 -0
- package/src/components/BaseTabs.story.vue +0 -42
- package/src/components/BaseTabs.vue +3 -6
- package/src/components/ExperimentPopover.vue +10 -2
- package/src/components/FileBrowserModal.story.vue +203 -0
- package/src/components/FileBrowserModal.vue +384 -0
- package/src/components/FilePicker.story.vue +44 -0
- package/src/components/FilePicker.vue +284 -0
- package/src/components/FileUploader.vue +42 -15
- package/src/components/FormField.vue +10 -3
- package/src/components/GeneratedPathInput.vue +11 -7
- package/src/components/GeneratedPlotlyResult.vue +34 -69
- package/src/components/GeneratedResultRenderer.vue +0 -5
- package/src/components/NumberInput.story.vue +32 -13
- package/src/components/NumberInput.vue +116 -55
- package/src/components/PlotlyChart.story.vue +61 -0
- package/src/components/PlotlyChart.vue +233 -0
- package/src/components/PluginWorkspaceView.props.ts +3 -3
- package/src/components/SegmentedControl.vue +2 -2
- package/src/components/fileBrowserPresentation.ts +39 -0
- package/src/components/index.ts +5 -0
- package/src/components/internal/FileBrowserDialogInternal.vue +75 -0
- package/src/components/internal/FileBrowserListingInternal.vue +197 -0
- package/src/components/internal/FileBrowserToolbarInternal.vue +136 -0
- package/src/components/internal/FilePickerPreviewInternal.vue +100 -0
- package/src/components/internal/FilePickerTreeInternal.vue +238 -0
- package/src/components/internal/FormFieldRendererInternal.vue +17 -0
- package/src/components/internal/WorkspaceSidebarInternal.vue +1 -2
- package/src/composables/filePicker/localFiles.ts +65 -0
- package/src/composables/filePicker/usePickerNavigation.ts +220 -0
- package/src/composables/filePicker/usePickerSelection.ts +270 -0
- package/src/composables/filePicker/validation.ts +41 -0
- package/src/composables/index.ts +7 -0
- package/src/composables/pluginWorkspaceModel.ts +0 -3
- package/src/composables/useApi.ts +57 -13
- package/src/composables/useFileBrowser.ts +314 -0
- package/src/composables/useFilePicker.ts +366 -0
- package/src/composables/useGeneratedAnalysis.ts +10 -17
- package/src/composables/usePlatformFilePickerAdapter.ts +104 -0
- package/src/composables/usePluginClient.ts +3 -0
- package/src/composables/usePluginWorkspace.ts +1 -2
- package/src/composables/useToast.ts +21 -5
- package/src/generated/schema.ts +58 -18
- package/src/index.ts +3 -0
- package/src/styles/components/app-sidebar.css +282 -121
- package/src/styles/components/file-browser-modal.css +489 -0
- package/src/styles/components/file-picker.css +502 -0
- package/src/styles/components/form-builder.css +1 -1
- package/src/styles/components/form-field.css +28 -0
- package/src/styles/components/icon-button.css +9 -0
- package/src/styles/components/number-input.css +73 -91
- package/src/styles/components/pill.css +49 -51
- package/src/styles/components/segmented-control.css +16 -11
- package/src/styles/components/tabs.css +18 -34
- package/src/styles/components/toast.css +136 -27
- package/src/styles/index.css +3 -0
- package/src/styles/variables.css +25 -5
- package/src/types/components.ts +23 -0
- package/src/types/fileBrowserTypes.ts +85 -0
- package/src/types/filePicker.ts +72 -0
- package/src/types/form-builder.ts +2 -0
- package/src/types/generated-ui.ts +2 -0
- package/src/types/index.ts +16 -0
- package/dist/ExperimentPopover-oL0fg_A0.js.map +0 -1
- package/dist/SettingsModal-Dphh3RrI.js.map +0 -1
- package/dist/Skeleton-DxtwJ5gt.js.map +0 -1
- package/dist/components-CgAUzF8F.js.map +0 -1
- package/dist/composables-B_pia3hO.js.map +0 -1
- package/dist/errors-_W0-LbA6.js.map +0 -1
- package/dist/useControlSchema-aMZlb4Sc.js.map +0 -1
- package/dist/useProtocolTemplates-BmX_iVBR.js.map +0 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { FileEntry } from '../types/fileBrowserTypes'
|
|
2
|
+
|
|
3
|
+
export const FILE_BROWSER_ICONS = {
|
|
4
|
+
disk: 'M4 5h16v14H4zM8 9h.01M4 12h16',
|
|
5
|
+
folder: 'M4 7h6l2 2h8v10H4z',
|
|
6
|
+
file: 'M14 3v5h5M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatCount(value: number): string {
|
|
10
|
+
// Thin spaces group digits without the comma/period ambiguity across locales.
|
|
11
|
+
return value.toLocaleString('en-US').replace(/,/g, ' ')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatSize(entry: FileEntry): string {
|
|
15
|
+
if (entry.kind === 'directory') {
|
|
16
|
+
return entry.child_count === null || entry.child_count === undefined
|
|
17
|
+
? '—'
|
|
18
|
+
: `${entry.child_count} item${entry.child_count === 1 ? '' : 's'}`
|
|
19
|
+
}
|
|
20
|
+
return formatBytes(entry.size_bytes ?? null)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatBytes(bytes: number | null): string {
|
|
24
|
+
if (bytes === null) return '—'
|
|
25
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
26
|
+
let value = bytes
|
|
27
|
+
let unit = 0
|
|
28
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
29
|
+
value /= 1024
|
|
30
|
+
unit += 1
|
|
31
|
+
}
|
|
32
|
+
const rounded = value >= 100 || unit === 0 ? Math.round(value) : Number(value.toFixed(1))
|
|
33
|
+
return `${rounded} ${units[unit]}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function entryType(entry: FileEntry): string {
|
|
37
|
+
if (entry.kind === 'directory') return 'Folder'
|
|
38
|
+
return entry.extension ? entry.extension.replace('.', '').toUpperCase() : 'File'
|
|
39
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -99,6 +99,7 @@ export { default as SampleHierarchyTree } from './SampleHierarchyTree.vue'
|
|
|
99
99
|
export { default as ProtocolStepEditor } from './ProtocolStepEditor.vue'
|
|
100
100
|
|
|
101
101
|
// Scientific display components
|
|
102
|
+
export { default as PlotlyChart } from './PlotlyChart.vue'
|
|
102
103
|
export { default as ScientificNumber } from './ScientificNumber.vue'
|
|
103
104
|
/** Renders a chemical formula string (H₂O, C₆H₁₂O₆, Ca(OH)₂) with typographically correct subscripts and superscripts for charges. @deprecated Unused by the MINT platform and scheduled for removal in MINT 1.3; no platform replacement is planned. */
|
|
104
105
|
export { default as ChemicalFormula } from './ChemicalFormula.vue'
|
|
@@ -142,7 +143,11 @@ export { default as SectionCard } from './SectionCard.vue'
|
|
|
142
143
|
// Experiment / analysis components
|
|
143
144
|
export { default as ExperimentSelectorModal } from './ExperimentSelectorModal.vue'
|
|
144
145
|
export { default as ArtifactSelectorModal } from './ArtifactSelectorModal.vue'
|
|
146
|
+
/** Modal picker over the read-only server mounts, recording paths rather than copying data. */
|
|
147
|
+
export { default as FileBrowserModal } from './FileBrowserModal.vue'
|
|
145
148
|
export { default as GeneratedPathInput } from './GeneratedPathInput.vue'
|
|
146
149
|
export { default as ArtifactSaveDialog } from './ArtifactSaveDialog.vue'
|
|
147
150
|
export { default as ExperimentPopover } from './ExperimentPopover.vue'
|
|
148
151
|
export { default as FitPanel } from './FitPanel.vue'
|
|
152
|
+
|
|
153
|
+
export { default as FilePicker } from './FilePicker.vue'
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onBeforeUnmount, onMounted, shallowRef, watch } from 'vue'
|
|
3
|
+
import type { ModalSize } from '../../types'
|
|
4
|
+
import { useFocusTrap } from '../../composables/useFocusTrap'
|
|
5
|
+
import IconButton from '../IconButton.vue'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{ open: boolean; label: string; size: ModalSize; variant?: 'picker' }>()
|
|
8
|
+
const emit = defineEmits<{ close: [] }>()
|
|
9
|
+
const containerRef = shallowRef<HTMLElement | null>(null)
|
|
10
|
+
const active = shallowRef(false)
|
|
11
|
+
useFocusTrap({ containerRef, active })
|
|
12
|
+
|
|
13
|
+
// Activate after mounting so an initially open picker traps focus too.
|
|
14
|
+
onMounted(() => {
|
|
15
|
+
active.value = props.open
|
|
16
|
+
})
|
|
17
|
+
watch(
|
|
18
|
+
() => props.open,
|
|
19
|
+
(open) => {
|
|
20
|
+
active.value = open
|
|
21
|
+
},
|
|
22
|
+
)
|
|
23
|
+
watch(
|
|
24
|
+
active,
|
|
25
|
+
(open, _, onCleanup) => {
|
|
26
|
+
if (!open) return
|
|
27
|
+
const overflow = document.body.style.overflow
|
|
28
|
+
document.body.style.overflow = 'hidden'
|
|
29
|
+
onCleanup(() => {
|
|
30
|
+
document.body.style.overflow = overflow
|
|
31
|
+
})
|
|
32
|
+
},
|
|
33
|
+
{ flush: 'sync' },
|
|
34
|
+
)
|
|
35
|
+
onBeforeUnmount(() => {
|
|
36
|
+
active.value = false
|
|
37
|
+
})
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<Teleport to="body">
|
|
42
|
+
<div v-if="open" class="mint-file-browser-overlay">
|
|
43
|
+
<section
|
|
44
|
+
ref="containerRef"
|
|
45
|
+
class="mint-file-browser-dialog"
|
|
46
|
+
:class="[
|
|
47
|
+
`mint-file-browser-dialog--${size}`,
|
|
48
|
+
variant === 'picker' ? 'mint-picker-dialog' : '',
|
|
49
|
+
]"
|
|
50
|
+
role="dialog"
|
|
51
|
+
aria-modal="true"
|
|
52
|
+
:aria-label="label"
|
|
53
|
+
tabindex="-1"
|
|
54
|
+
@keydown.esc.stop.prevent="emit('close')"
|
|
55
|
+
>
|
|
56
|
+
<header class="mint-file-browser-dialog__header">
|
|
57
|
+
<slot name="header" />
|
|
58
|
+
<IconButton size="sm" label="Close modal" @click="emit('close')">
|
|
59
|
+
<svg
|
|
60
|
+
viewBox="0 0 24 24"
|
|
61
|
+
fill="none"
|
|
62
|
+
stroke="currentColor"
|
|
63
|
+
stroke-width="2"
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
>
|
|
66
|
+
<path d="M6 6l12 12M18 6L6 18" />
|
|
67
|
+
</svg>
|
|
68
|
+
</IconButton>
|
|
69
|
+
</header>
|
|
70
|
+
<slot />
|
|
71
|
+
<footer class="mint-file-browser-dialog__footer"><slot name="footer" /></footer>
|
|
72
|
+
</section>
|
|
73
|
+
</div>
|
|
74
|
+
</Teleport>
|
|
75
|
+
</template>
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type { FileEntry, FileSelection } from '../../types/fileBrowserTypes'
|
|
4
|
+
import {
|
|
5
|
+
FILE_BROWSER_ICONS as ICON,
|
|
6
|
+
formatCount,
|
|
7
|
+
formatSize,
|
|
8
|
+
entryType,
|
|
9
|
+
} from '../fileBrowserPresentation'
|
|
10
|
+
import EmptyState from '../EmptyState.vue'
|
|
11
|
+
import BaseButton from '../BaseButton.vue'
|
|
12
|
+
import IconButton from '../IconButton.vue'
|
|
13
|
+
import Skeleton from '../Skeleton.vue'
|
|
14
|
+
const props = defineProps<{
|
|
15
|
+
mountId: string
|
|
16
|
+
entries: FileEntry[]
|
|
17
|
+
selected: FileSelection[]
|
|
18
|
+
loading: boolean
|
|
19
|
+
error?: string | null
|
|
20
|
+
dimNonMatching: boolean
|
|
21
|
+
totalCount: number | null
|
|
22
|
+
matchCount: number | null
|
|
23
|
+
truncated: boolean
|
|
24
|
+
hasMounts: boolean
|
|
25
|
+
hasAvailableMounts: boolean
|
|
26
|
+
search: string
|
|
27
|
+
}>()
|
|
28
|
+
const emit = defineEmits<{ toggle: [entry: FileEntry]; open: [entry: FileEntry]; refresh: [] }>()
|
|
29
|
+
const emptyTitle = computed(() =>
|
|
30
|
+
!props.hasMounts
|
|
31
|
+
? 'No server mounts configured'
|
|
32
|
+
: !props.hasAvailableMounts
|
|
33
|
+
? 'Server mounts are offline'
|
|
34
|
+
: props.search
|
|
35
|
+
? 'No matching files or folders'
|
|
36
|
+
: 'This folder is empty',
|
|
37
|
+
)
|
|
38
|
+
const emptyDescription = computed(() =>
|
|
39
|
+
!props.hasMounts
|
|
40
|
+
? 'Ask your administrator to configure a read-only data mount.'
|
|
41
|
+
: !props.hasAvailableMounts
|
|
42
|
+
? 'Refresh when the data mount is available again.'
|
|
43
|
+
: props.search
|
|
44
|
+
? 'Try a different search or clear the search field.'
|
|
45
|
+
: 'Choose another folder from the mount list or breadcrumbs.',
|
|
46
|
+
)
|
|
47
|
+
function isSelected(entry: FileEntry) {
|
|
48
|
+
return props.selected.some((item) => item.mount_id === props.mountId && item.path === entry.path)
|
|
49
|
+
}
|
|
50
|
+
function isDimmed(entry: FileEntry) {
|
|
51
|
+
return props.dimNonMatching && entry.matches_type_rules === false
|
|
52
|
+
}
|
|
53
|
+
function entryIcon(entry: FileEntry) {
|
|
54
|
+
return entry.kind === 'directory' ? ICON.folder : ICON.file
|
|
55
|
+
}
|
|
56
|
+
function handleRowClick(entry: FileEntry) {
|
|
57
|
+
if (!props.loading && !props.error && props.mountId && entry.kind === 'directory')
|
|
58
|
+
emit('open', entry)
|
|
59
|
+
}
|
|
60
|
+
function toggleEntry(entry: FileEntry) {
|
|
61
|
+
if (!props.loading && !props.error && props.mountId) emit('toggle', entry)
|
|
62
|
+
}
|
|
63
|
+
function folderNote(entry: FileEntry) {
|
|
64
|
+
if (entry.kind !== 'directory' || !isSelected(entry)) return ''
|
|
65
|
+
return entry.child_count == null
|
|
66
|
+
? 'whole folder'
|
|
67
|
+
: `whole folder, ${entry.child_count} item${entry.child_count === 1 ? '' : 's'}`
|
|
68
|
+
}
|
|
69
|
+
const statusLine = computed(() => {
|
|
70
|
+
if (props.loading) return 'Loading folder…'
|
|
71
|
+
if (props.error || !props.mountId) return ''
|
|
72
|
+
const total = props.totalCount ?? props.entries.length
|
|
73
|
+
const parts = [`${formatCount(total)} item${total === 1 ? '' : 's'}`]
|
|
74
|
+
if (props.entries.length !== total) parts.push(`${formatCount(props.entries.length)} shown`)
|
|
75
|
+
if (props.matchCount !== null && props.matchCount !== total)
|
|
76
|
+
parts.push(`${formatCount(props.matchCount)} match the type rules`)
|
|
77
|
+
if (props.truncated) parts.push('listing capped')
|
|
78
|
+
return parts.join(' · ')
|
|
79
|
+
})
|
|
80
|
+
</script>
|
|
81
|
+
<template>
|
|
82
|
+
<div class="mint-file-browser__listing" :aria-busy="loading">
|
|
83
|
+
<div class="mint-file-browser__head">
|
|
84
|
+
<span class="mint-file-browser__col-check" />
|
|
85
|
+
<span class="mint-file-browser__col-icon" />
|
|
86
|
+
<span class="mint-file-browser__col-name">Name</span>
|
|
87
|
+
<span class="mint-file-browser__col-size">Size</span>
|
|
88
|
+
<span class="mint-file-browser__col-type">Type</span>
|
|
89
|
+
<span class="mint-file-browser__col-open" />
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div v-if="loading" class="mint-file-browser__skeleton">
|
|
93
|
+
<div v-for="n in 6" :key="n" class="mint-file-browser__skeleton-row">
|
|
94
|
+
<Skeleton width="60%" height="14px" />
|
|
95
|
+
<Skeleton width="60px" height="10px" />
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div v-else-if="error" class="mint-file-browser__error" role="alert">
|
|
100
|
+
<span>{{ error }}</span>
|
|
101
|
+
<BaseButton
|
|
102
|
+
variant="secondary"
|
|
103
|
+
size="sm"
|
|
104
|
+
class="mint-file-browser__retry"
|
|
105
|
+
aria-label="Retry folder listing"
|
|
106
|
+
@click="emit('refresh')"
|
|
107
|
+
>
|
|
108
|
+
Try again
|
|
109
|
+
</BaseButton>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<EmptyState
|
|
113
|
+
v-else-if="!entries.length"
|
|
114
|
+
:title="emptyTitle"
|
|
115
|
+
:description="emptyDescription"
|
|
116
|
+
size="sm"
|
|
117
|
+
/>
|
|
118
|
+
|
|
119
|
+
<div v-else class="mint-file-browser__rows">
|
|
120
|
+
<div
|
|
121
|
+
v-for="entry in entries"
|
|
122
|
+
:key="entry.path"
|
|
123
|
+
class="mint-file-browser__row"
|
|
124
|
+
:class="{
|
|
125
|
+
'mint-file-browser__row--selected': isSelected(entry),
|
|
126
|
+
'mint-file-browser__row--dimmed': isDimmed(entry),
|
|
127
|
+
'mint-file-browser__row--dir': entry.kind === 'directory',
|
|
128
|
+
}"
|
|
129
|
+
@click="handleRowClick(entry)"
|
|
130
|
+
>
|
|
131
|
+
<input
|
|
132
|
+
class="mint-file-browser__checkbox"
|
|
133
|
+
type="checkbox"
|
|
134
|
+
:checked="isSelected(entry)"
|
|
135
|
+
:title="entry.kind === 'directory' ? 'Select the whole folder' : 'Select this file'"
|
|
136
|
+
:aria-label="
|
|
137
|
+
entry.kind === 'directory'
|
|
138
|
+
? `Select the whole ${entry.name} folder`
|
|
139
|
+
: `Select ${entry.name}`
|
|
140
|
+
"
|
|
141
|
+
@click.stop
|
|
142
|
+
@change="toggleEntry(entry)"
|
|
143
|
+
/>
|
|
144
|
+
<span
|
|
145
|
+
class="mint-file-browser__row-icon"
|
|
146
|
+
:class="`mint-file-browser__row-icon--${entry.kind}`"
|
|
147
|
+
>
|
|
148
|
+
<svg
|
|
149
|
+
class="mint-file-browser__icon"
|
|
150
|
+
viewBox="0 0 24 24"
|
|
151
|
+
fill="none"
|
|
152
|
+
stroke="currentColor"
|
|
153
|
+
stroke-width="1.8"
|
|
154
|
+
stroke-linecap="round"
|
|
155
|
+
stroke-linejoin="round"
|
|
156
|
+
>
|
|
157
|
+
<path :d="entryIcon(entry)" />
|
|
158
|
+
</svg>
|
|
159
|
+
</span>
|
|
160
|
+
<span class="mint-file-browser__name-cell">
|
|
161
|
+
<span class="mint-file-browser__name" :title="entry.name">{{ entry.name }}</span>
|
|
162
|
+
<span v-if="folderNote(entry)" class="mint-file-browser__note-pill">Whole folder</span>
|
|
163
|
+
</span>
|
|
164
|
+
<span class="mint-file-browser__size">{{ formatSize(entry) }}</span>
|
|
165
|
+
<span class="mint-file-browser__type">{{ entryType(entry) }}</span>
|
|
166
|
+
<!-- The chevron is the keyboard route into a folder: the row
|
|
167
|
+
itself is a div, so this button is what Tab can reach. -->
|
|
168
|
+
<IconButton
|
|
169
|
+
v-if="entry.kind === 'directory'"
|
|
170
|
+
size="sm"
|
|
171
|
+
class="mint-file-browser__open"
|
|
172
|
+
:label="`Open ${entry.name}`"
|
|
173
|
+
@click.stop="handleRowClick(entry)"
|
|
174
|
+
>
|
|
175
|
+
<svg
|
|
176
|
+
class="mint-file-browser__icon"
|
|
177
|
+
viewBox="0 0 24 24"
|
|
178
|
+
fill="none"
|
|
179
|
+
stroke="currentColor"
|
|
180
|
+
stroke-width="2.5"
|
|
181
|
+
stroke-linecap="round"
|
|
182
|
+
stroke-linejoin="round"
|
|
183
|
+
>
|
|
184
|
+
<path d="M9 6l6 6-6 6" />
|
|
185
|
+
</svg>
|
|
186
|
+
</IconButton>
|
|
187
|
+
<span v-else class="mint-file-browser__open" aria-hidden="true" />
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div class="mint-file-browser__status" role="status" aria-live="polite">
|
|
192
|
+
<span>{{ statusLine }}</span>
|
|
193
|
+
<div class="mint-file-browser__status-spacer" />
|
|
194
|
+
<span class="mint-file-browser__legend">Read-only · Select paths, not copies</span>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</template>
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import BaseButton from '../BaseButton.vue'
|
|
4
|
+
import BaseInput from '../BaseInput.vue'
|
|
5
|
+
import IconButton from '../IconButton.vue'
|
|
6
|
+
import type {
|
|
7
|
+
FileBrowserSortKey,
|
|
8
|
+
FilePathCrumb,
|
|
9
|
+
FileBrowserLocation,
|
|
10
|
+
} from '../../types/fileBrowserTypes'
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
mountId: string
|
|
13
|
+
path: string
|
|
14
|
+
parent: string | null
|
|
15
|
+
breadcrumbs: FilePathCrumb[]
|
|
16
|
+
search: string
|
|
17
|
+
sort: FileBrowserSortKey
|
|
18
|
+
typeRules: string[]
|
|
19
|
+
loading: boolean
|
|
20
|
+
error?: string | null
|
|
21
|
+
}>()
|
|
22
|
+
const emit = defineEmits<{
|
|
23
|
+
navigate: [location: FileBrowserLocation]
|
|
24
|
+
selectFolder: [location: FileBrowserLocation]
|
|
25
|
+
'update:search': [value: string]
|
|
26
|
+
'update:sort': [value: FileBrowserSortKey]
|
|
27
|
+
refresh: []
|
|
28
|
+
}>()
|
|
29
|
+
const typeRuleLabel = computed(() => props.typeRules.join(' '))
|
|
30
|
+
function handleBack() {
|
|
31
|
+
if (props.parent !== null && !props.loading && !props.error)
|
|
32
|
+
emit('navigate', { mount_id: props.mountId, path: props.parent })
|
|
33
|
+
}
|
|
34
|
+
function handleCrumb(crumb: FilePathCrumb) {
|
|
35
|
+
emit('navigate', { mount_id: props.mountId, path: crumb.path })
|
|
36
|
+
}
|
|
37
|
+
function cycleSort() {
|
|
38
|
+
const order: FileBrowserSortKey[] = ['name', 'size', 'modified']
|
|
39
|
+
emit('update:sort', order[(order.indexOf(props.sort) + 1) % order.length]!)
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
<template>
|
|
43
|
+
<div class="mint-file-browser__toolbar">
|
|
44
|
+
<div class="mint-file-browser__pathbar">
|
|
45
|
+
<IconButton
|
|
46
|
+
size="sm"
|
|
47
|
+
class="mint-file-browser__back"
|
|
48
|
+
label="Go to parent folder"
|
|
49
|
+
:disabled="parent === null || loading || !!error"
|
|
50
|
+
@click="handleBack"
|
|
51
|
+
>
|
|
52
|
+
<svg
|
|
53
|
+
viewBox="0 0 24 24"
|
|
54
|
+
fill="none"
|
|
55
|
+
stroke="currentColor"
|
|
56
|
+
stroke-width="2"
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
>
|
|
59
|
+
<path d="M12 19V5m-6 6 6-6 6 6" />
|
|
60
|
+
</svg>
|
|
61
|
+
</IconButton>
|
|
62
|
+
<nav class="mint-file-browser__crumbs" aria-label="Breadcrumb">
|
|
63
|
+
<template v-for="(crumb, index) in breadcrumbs" :key="crumb.path">
|
|
64
|
+
<span v-if="index" class="mint-file-browser__crumb-sep" aria-hidden="true">/</span>
|
|
65
|
+
<BaseButton
|
|
66
|
+
variant="ghost"
|
|
67
|
+
size="sm"
|
|
68
|
+
class="mint-file-browser__crumb"
|
|
69
|
+
:class="{ 'mint-file-browser__crumb--current': index === breadcrumbs.length - 1 }"
|
|
70
|
+
:aria-current="index === breadcrumbs.length - 1 ? 'location' : undefined"
|
|
71
|
+
@click="handleCrumb(crumb)"
|
|
72
|
+
>{{ crumb.label }}</BaseButton
|
|
73
|
+
>
|
|
74
|
+
</template>
|
|
75
|
+
<span v-if="!breadcrumbs.length" class="mint-file-browser__selection-hint">{{
|
|
76
|
+
path || 'Choose a location'
|
|
77
|
+
}}</span>
|
|
78
|
+
</nav>
|
|
79
|
+
<IconButton
|
|
80
|
+
size="sm"
|
|
81
|
+
label="Refresh folder listing"
|
|
82
|
+
:disabled="loading"
|
|
83
|
+
@click="emit('refresh')"
|
|
84
|
+
>
|
|
85
|
+
<svg
|
|
86
|
+
viewBox="0 0 24 24"
|
|
87
|
+
fill="none"
|
|
88
|
+
stroke="currentColor"
|
|
89
|
+
stroke-width="2"
|
|
90
|
+
aria-hidden="true"
|
|
91
|
+
>
|
|
92
|
+
<path d="M20 7v5h-5M4 17v-5h5M6 8a7 7 0 0 1 12-2l2 3M4 15l2 3a7 7 0 0 0 12-2" />
|
|
93
|
+
</svg>
|
|
94
|
+
</IconButton>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="mint-file-browser__tools">
|
|
97
|
+
<BaseInput
|
|
98
|
+
:model-value="search"
|
|
99
|
+
type="search"
|
|
100
|
+
size="sm"
|
|
101
|
+
class="mint-file-browser__search"
|
|
102
|
+
placeholder="Search this folder…"
|
|
103
|
+
aria-label="Search this folder"
|
|
104
|
+
@update:model-value="emit('update:search', String($event ?? ''))"
|
|
105
|
+
/>
|
|
106
|
+
<span v-if="typeRuleLabel" class="mint-file-browser__rules" title="Matching file types">{{
|
|
107
|
+
typeRuleLabel
|
|
108
|
+
}}</span>
|
|
109
|
+
<IconButton
|
|
110
|
+
size="sm"
|
|
111
|
+
class="mint-file-browser__sort"
|
|
112
|
+
:label="`Sorted by ${sort}. Click to change.`"
|
|
113
|
+
@click="cycleSort"
|
|
114
|
+
>
|
|
115
|
+
<svg
|
|
116
|
+
viewBox="0 0 24 24"
|
|
117
|
+
fill="none"
|
|
118
|
+
stroke="currentColor"
|
|
119
|
+
stroke-width="2"
|
|
120
|
+
aria-hidden="true"
|
|
121
|
+
>
|
|
122
|
+
<path d="M4 6h10M4 12h16M4 18h7" />
|
|
123
|
+
</svg>
|
|
124
|
+
</IconButton>
|
|
125
|
+
<BaseButton
|
|
126
|
+
variant="secondary"
|
|
127
|
+
size="sm"
|
|
128
|
+
class="mint-file-browser__select-folder"
|
|
129
|
+
:title="`Select ${path || 'this folder'} itself as the input`"
|
|
130
|
+
:disabled="!mountId || loading || !!error"
|
|
131
|
+
@click="emit('selectFolder', { mount_id: mountId, path })"
|
|
132
|
+
>Select this folder</BaseButton
|
|
133
|
+
>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</template>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import BaseButton from '../BaseButton.vue'
|
|
3
|
+
import { FILE_BROWSER_ICONS as ICON, formatBytes } from '../fileBrowserPresentation'
|
|
4
|
+
import type { PickerNode, PickerExcluded, PickerSource } from '../../types/filePicker'
|
|
5
|
+
import { pickerReason } from '../../composables/filePicker/validation'
|
|
6
|
+
|
|
7
|
+
defineProps<{
|
|
8
|
+
totalCount?: number | null
|
|
9
|
+
node: PickerNode | null
|
|
10
|
+
source: PickerSource
|
|
11
|
+
exclusions: PickerExcluded[]
|
|
12
|
+
busy: boolean
|
|
13
|
+
error: string
|
|
14
|
+
reviewing: boolean
|
|
15
|
+
}>()
|
|
16
|
+
const emit = defineEmits<{ review: []; retry: [] }>()
|
|
17
|
+
</script>
|
|
18
|
+
<template>
|
|
19
|
+
<aside class="mint-picker__preview" aria-label="Selection preview">
|
|
20
|
+
<div class="mint-picker__preview-heading">{{ reviewing ? 'Skipped files' : 'Preview' }}</div>
|
|
21
|
+
<div v-if="reviewing" class="mint-picker__preview-content">
|
|
22
|
+
<BaseButton variant="ghost" size="sm" @click="emit('review')">Back to preview</BaseButton>
|
|
23
|
+
<div
|
|
24
|
+
v-for="item in exclusions"
|
|
25
|
+
:key="item.path"
|
|
26
|
+
class="mint-picker__exclusion"
|
|
27
|
+
:title="item.path"
|
|
28
|
+
>
|
|
29
|
+
<strong>{{ item.name }}</strong
|
|
30
|
+
><span>{{ pickerReason(item) }}</span>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div v-else-if="node" class="mint-picker__preview-content">
|
|
34
|
+
<div class="mint-picker__preview-icon">
|
|
35
|
+
<svg
|
|
36
|
+
viewBox="0 0 24 24"
|
|
37
|
+
fill="none"
|
|
38
|
+
stroke="currentColor"
|
|
39
|
+
stroke-width="1.7"
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
>
|
|
42
|
+
<path :d="node.kind === 'folder' ? ICON.folder : ICON.file" />
|
|
43
|
+
</svg>
|
|
44
|
+
</div>
|
|
45
|
+
<strong class="mint-picker__preview-name">{{ node.name }}</strong>
|
|
46
|
+
<dl class="mint-picker__metadata">
|
|
47
|
+
<div>
|
|
48
|
+
<dt>Location</dt>
|
|
49
|
+
<dd :title="node.parentName || node.path">{{ node.parentName || node.path }}</dd>
|
|
50
|
+
</div>
|
|
51
|
+
<div>
|
|
52
|
+
<dt>Size</dt>
|
|
53
|
+
<dd>{{ node.bytes == null ? 'Not available' : formatBytes(node.bytes) }}</dd>
|
|
54
|
+
</div>
|
|
55
|
+
<div v-if="node.kind === 'folder'">
|
|
56
|
+
<dt>Files</dt>
|
|
57
|
+
<dd>
|
|
58
|
+
{{ node.fileCount ?? 'Not counted yet'
|
|
59
|
+
}}<template v-if="totalCount != null && totalCount !== node.fileCount">
|
|
60
|
+
of {{ totalCount }}</template
|
|
61
|
+
>
|
|
62
|
+
</dd>
|
|
63
|
+
</div>
|
|
64
|
+
<div>
|
|
65
|
+
<dt>Format</dt>
|
|
66
|
+
<dd v-if="node.kind === 'file'">{{ node.format.toUpperCase() || 'Unknown' }}</dd>
|
|
67
|
+
<dd v-else>
|
|
68
|
+
<span v-for="(count, format) in node.formats" :key="format"
|
|
69
|
+
>{{ format.toUpperCase() }} ×{{ count }}<br /></span
|
|
70
|
+
><span v-if="!node.formats">Not counted yet</span>
|
|
71
|
+
</dd>
|
|
72
|
+
</div>
|
|
73
|
+
<div>
|
|
74
|
+
<dt>Source</dt>
|
|
75
|
+
<dd>
|
|
76
|
+
{{ source === 'localFile' ? 'Local files' : 'Server' }}
|
|
77
|
+
</dd>
|
|
78
|
+
</div>
|
|
79
|
+
</dl>
|
|
80
|
+
<BaseButton
|
|
81
|
+
v-if="exclusions.length"
|
|
82
|
+
variant="ghost"
|
|
83
|
+
size="sm"
|
|
84
|
+
class="mint-picker__skipped"
|
|
85
|
+
@click="emit('review')"
|
|
86
|
+
>{{ exclusions.length }} skipped · Review</BaseButton
|
|
87
|
+
>
|
|
88
|
+
<div v-if="error" class="mint-picker__preview-error" role="alert">
|
|
89
|
+
{{ error
|
|
90
|
+
}}<BaseButton variant="secondary" size="sm" @click="emit('retry')">Try again</BaseButton>
|
|
91
|
+
</div>
|
|
92
|
+
<span v-else class="mint-picker__ready" role="status">{{
|
|
93
|
+
busy ? 'Checking selection…' : '✓ Metadata available'
|
|
94
|
+
}}</span>
|
|
95
|
+
</div>
|
|
96
|
+
<div v-else class="mint-picker__preview-empty">
|
|
97
|
+
Select a file or folder to preview its details.
|
|
98
|
+
</div>
|
|
99
|
+
</aside>
|
|
100
|
+
</template>
|