@morscherlab/mint-sdk 1.2.0-beta.14 → 1.2.0-beta.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +30 -0
  2. package/dist/{ExperimentPopover-BIZKoRTT.js → ExperimentPopover-CJdNlCZb.js} +3 -3
  3. package/dist/{ExperimentPopover-BIZKoRTT.js.map → ExperimentPopover-CJdNlCZb.js.map} +1 -1
  4. package/dist/{ExperimentSelectorModal-BeEOomw_.js → ExperimentSelectorModal-CZ1f_K9I.js} +5 -5
  5. package/dist/{ExperimentSelectorModal-BeEOomw_.js.map → ExperimentSelectorModal-CZ1f_K9I.js.map} +1 -1
  6. package/dist/{SettingsModal-Dphh3RrI.js → SettingsModal-DQUcpT4T.js} +5 -5
  7. package/dist/{SettingsModal-Dphh3RrI.js.map → SettingsModal-DQUcpT4T.js.map} +1 -1
  8. package/dist/{Skeleton-DxtwJ5gt.js → Skeleton-JaDtC8-9.js} +3 -3
  9. package/dist/{Skeleton-DxtwJ5gt.js.map → Skeleton-JaDtC8-9.js.map} +1 -1
  10. package/dist/__tests__/apiErrors.test.d.ts +1 -0
  11. package/dist/__tests__/components/PlotlyChart.test.d.ts +1 -0
  12. package/dist/__tests__/components/iconButtonIconSizing.test.d.ts +1 -0
  13. package/dist/apiErrors.d.ts +26 -0
  14. package/dist/components/BaseInput.vue.d.ts +1 -1
  15. package/dist/components/BaseSelect.vue.d.ts +1 -1
  16. package/dist/components/BaseTextarea.vue.d.ts +1 -1
  17. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  18. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +2 -2
  19. package/dist/components/ConcentrationInput.vue.d.ts +1 -1
  20. package/dist/components/DatePicker.vue.d.ts +1 -1
  21. package/dist/components/DateTimePicker.vue.d.ts +1 -1
  22. package/dist/components/FileBrowserModal.vue.d.ts +1 -1
  23. package/dist/components/FilePicker.vue.d.ts +3 -3
  24. package/dist/components/FileUploader.vue.d.ts +5 -2
  25. package/dist/components/FormulaInput.vue.d.ts +1 -1
  26. package/dist/components/GeneratedPlotlyResult.vue.d.ts +1 -3
  27. package/dist/components/MobileSupportGate.vue.d.ts +1 -1
  28. package/dist/components/MoleculeInput.vue.d.ts +1 -1
  29. package/dist/components/NumberInput.vue.d.ts +1 -1
  30. package/dist/components/PlotlyChart.vue.d.ts +59 -0
  31. package/dist/components/ResourceCard.vue.d.ts +1 -1
  32. package/dist/components/SecretInput.vue.d.ts +1 -1
  33. package/dist/components/SequenceInput.vue.d.ts +1 -1
  34. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  35. package/dist/components/TagsInput.vue.d.ts +1 -1
  36. package/dist/components/TimePicker.vue.d.ts +1 -1
  37. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  38. package/dist/components/UnitInput.vue.d.ts +1 -1
  39. package/dist/components/WellPlate.vue.d.ts +1 -1
  40. package/dist/components/index.d.ts +1 -0
  41. package/dist/components/index.js +8 -8
  42. package/dist/{components-6IIxCSI8.js → components-DT6ggNrt.js} +580 -366
  43. package/dist/components-DT6ggNrt.js.map +1 -0
  44. package/dist/composables/filePicker/localFiles.d.ts +1 -1
  45. package/dist/composables/filePicker/usePickerNavigation.d.ts +4 -3
  46. package/dist/composables/index.js +9 -9
  47. package/dist/composables/useApi.d.ts +2 -0
  48. package/dist/composables/useFilePicker.d.ts +5 -6
  49. package/dist/{composables-hY2Q2xPI.js → composables-DmF_8iyX.js} +13 -10
  50. package/dist/{composables-hY2Q2xPI.js.map → composables-DmF_8iyX.js.map} +1 -1
  51. package/dist/{errors-_W0-LbA6.js → errors-DZDb1cIb.js} +66 -5
  52. package/dist/errors-DZDb1cIb.js.map +1 -0
  53. package/dist/{experiment-Cwl9AYxw.js → experiment-CHm_TwJy.js} +2 -2
  54. package/dist/{experiment-Cwl9AYxw.js.map → experiment-CHm_TwJy.js.map} +1 -1
  55. package/dist/{experiment-utils-DV3DrvE_.js → experiment-utils-CV2Wza3z.js} +1 -1
  56. package/dist/{experiment-utils-DV3DrvE_.js.map → experiment-utils-CV2Wza3z.js.map} +1 -1
  57. package/dist/index.d.ts +1 -0
  58. package/dist/index.js +15 -15
  59. package/dist/install.js +2 -2
  60. package/dist/stores/index.js +2 -2
  61. package/dist/styles.css +37 -18
  62. package/dist/templates/index.js +2 -2
  63. package/dist/{templates-Qq113pyZ.js → templates-CXARYr6_.js} +2 -2
  64. package/dist/{templates-Qq113pyZ.js.map → templates-CXARYr6_.js.map} +1 -1
  65. package/dist/types/filePicker.d.ts +9 -1
  66. package/dist/{useControlSchema-aMZlb4Sc.js → useControlSchema-BAzmP1_y.js} +42 -37
  67. package/dist/useControlSchema-BAzmP1_y.js.map +1 -0
  68. package/dist/{useDropdownState-DitCtrTT.js → useDropdownState-C7hRiVQf.js} +2 -2
  69. package/dist/{useDropdownState-DitCtrTT.js.map → useDropdownState-C7hRiVQf.js.map} +1 -1
  70. package/dist/{useExperimentSelector-I8HC8hUu.js → useExperimentSelector-D_vobhnW.js} +4 -4
  71. package/dist/{useExperimentSelector-I8HC8hUu.js.map → useExperimentSelector-D_vobhnW.js.map} +1 -1
  72. package/dist/{useFormBuilder-TCxNnqeb.js → useFormBuilder-DEHejpiL.js} +2 -2
  73. package/dist/{useFormBuilder-TCxNnqeb.js.map → useFormBuilder-DEHejpiL.js.map} +1 -1
  74. package/dist/{validation-nBbSn1TI.js → validation-OuEr0iAS.js} +9 -8
  75. package/dist/{validation-nBbSn1TI.js.map → validation-OuEr0iAS.js.map} +1 -1
  76. package/package.json +1 -1
  77. package/src/__tests__/apiErrors.test.ts +81 -0
  78. package/src/__tests__/components/FilePicker.state.test.ts +128 -1
  79. package/src/__tests__/components/FilePicker.test.ts +146 -1
  80. package/src/__tests__/components/GeneratedPlotlyResult.test.ts +52 -5
  81. package/src/__tests__/components/PlotlyChart.test.ts +305 -0
  82. package/src/__tests__/components/SegmentedControl.test.ts +19 -0
  83. package/src/__tests__/components/iconButtonIconSizing.test.ts +18 -0
  84. package/src/__tests__/composables/useApi.test.ts +82 -9
  85. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +7 -2
  86. package/src/__tests__/composables/usePluginClient.test.ts +3 -0
  87. package/src/apiErrors.ts +85 -0
  88. package/src/components/FilePicker.story.vue +44 -0
  89. package/src/components/FilePicker.vue +23 -40
  90. package/src/components/FileUploader.vue +32 -14
  91. package/src/components/GeneratedPlotlyResult.vue +34 -69
  92. package/src/components/GeneratedResultRenderer.vue +0 -5
  93. package/src/components/PlotlyChart.story.vue +61 -0
  94. package/src/components/PlotlyChart.vue +233 -0
  95. package/src/components/SegmentedControl.vue +2 -2
  96. package/src/components/index.ts +1 -0
  97. package/src/composables/filePicker/localFiles.ts +6 -3
  98. package/src/composables/filePicker/usePickerNavigation.ts +29 -8
  99. package/src/composables/filePicker/usePickerSelection.ts +17 -4
  100. package/src/composables/filePicker/validation.ts +1 -0
  101. package/src/composables/useApi.ts +33 -4
  102. package/src/composables/useFilePicker.ts +41 -22
  103. package/src/composables/useGeneratedAnalysis.ts +10 -17
  104. package/src/composables/usePluginClient.ts +3 -0
  105. package/src/index.ts +3 -0
  106. package/src/styles/components/file-picker.css +6 -5
  107. package/src/styles/components/icon-button.css +9 -0
  108. package/src/types/filePicker.ts +9 -2
  109. package/dist/components-6IIxCSI8.js.map +0 -1
  110. package/dist/errors-_W0-LbA6.js.map +0 -1
  111. package/dist/useControlSchema-aMZlb4Sc.js.map +0 -1
@@ -0,0 +1,44 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import BaseButton from './BaseButton.vue'
4
+ import FilePicker from './FilePicker.vue'
5
+ import type { PickerAdapter, PickerNode } from '../types/filePicker'
6
+
7
+ const open = ref(true)
8
+ const hideRestricted = ref(true)
9
+ const nodes: PickerNode[] = [
10
+ { kind: 'folder', path: 'shared', name: 'Shared data' },
11
+ { kind: 'folder', path: 'restricted', name: 'Restricted data' },
12
+ { kind: 'file', path: 'shared/run.raw', parentPath: 'shared', name: 'run.raw', format: 'raw', bytes: 4096 },
13
+ { kind: 'file', path: 'restricted/private.raw', parentPath: 'restricted', name: 'private.raw', format: 'raw', bytes: 2048 },
14
+ ]
15
+ const adapter: PickerAdapter = {
16
+ listRoot: async () => nodes.filter((node) => !node.parentPath),
17
+ listChildren: async (path) => nodes.filter((node) => node.parentPath === path),
18
+ search: async (query, scope) => nodes.filter((node) =>
19
+ (scope.recursive ? !scope.path || node.path.startsWith(`${scope.path}/`) : (node.parentPath ?? '') === scope.path)
20
+ && node.name.toLowerCase().includes(query.toLowerCase()),
21
+ ),
22
+ }
23
+ // The host owns this rule; its server must also enforce access independently.
24
+ const systemFilter = (node: PickerNode) => !hideRestricted.value || !node.path.startsWith('restricted')
25
+ </script>
26
+
27
+ <template>
28
+ <Story title="Forms/FilePicker">
29
+ <Variant title="Local upload and system filter">
30
+ <div style="padding: 2rem; max-width: 600px;">
31
+ <BaseButton @click="open = true">Choose data</BaseButton>
32
+ <FilePicker
33
+ v-model:open="open"
34
+ :adapter="adapter"
35
+ :sources="['localFile', 'server']"
36
+ :system-filter="systemFilter"
37
+ />
38
+ </div>
39
+ <template #controls>
40
+ <HstCheckbox v-model="hideRestricted" title="Hide restricted server data" />
41
+ </template>
42
+ </Variant>
43
+ </Story>
44
+ </template>
@@ -1,8 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  /** Adapter-driven file picker with Finder-style selection, disclosure and metadata preview. */
3
+ import { ref } from 'vue'
3
4
  import BaseButton from './BaseButton.vue'
4
5
  import BaseInput from './BaseInput.vue'
5
6
  import IconButton from './IconButton.vue'
7
+ import FileUploader from './FileUploader.vue'
6
8
  import FileBrowserDialogInternal from './internal/FileBrowserDialogInternal.vue'
7
9
  import FilePickerTreeInternal from './internal/FilePickerTreeInternal.vue'
8
10
  import FilePickerPreviewInternal from './internal/FilePickerPreviewInternal.vue'
@@ -12,6 +14,7 @@ import { pickerReason } from '../composables/filePicker/validation'
12
14
  import type {
13
15
  PickerAdapter,
14
16
  PickerCapabilities,
17
+ PickerNode,
15
18
  PickerSelection,
16
19
  PickerSelectionMode,
17
20
  PickerSource,
@@ -26,6 +29,8 @@ interface Props {
26
29
  initialPath?: string
27
30
  initialSelection?: PickerSelection | null
28
31
  title?: string
32
+ /** Host-owned visibility/selection filter; backend must still enforce access. */
33
+ systemFilter?: (node: PickerNode, source: PickerSource) => boolean
29
34
  }
30
35
  const props = withDefaults(defineProps<Props>(), {
31
36
  selectionMode: 'folder+files',
@@ -40,6 +45,7 @@ const emit = defineEmits<{
40
45
  select: [selection: PickerSelection]
41
46
  cancel: []
42
47
  }>()
48
+ const uploadError = ref('')
43
49
  const {
44
50
  previewNode,
45
51
  sourceNames,
@@ -63,10 +69,7 @@ const {
63
69
  focused,
64
70
  resolving,
65
71
  resolutionError,
66
- filesInput,
67
- folderInput,
68
72
  searchArea,
69
- accept,
70
73
  confirmLabel,
71
74
  folderCount,
72
75
  canConfirm,
@@ -92,7 +95,7 @@ const {
92
95
  @close="cancel"
93
96
  >
94
97
  <template #header>
95
- <div ref="searchArea" class="mint-picker__search">
98
+ <div v-if="source === 'server'" ref="searchArea" class="mint-picker__search">
96
99
  <svg
97
100
  viewBox="0 0 24 24"
98
101
  fill="none"
@@ -110,6 +113,7 @@ const {
110
113
  />
111
114
  <kbd>/</kbd>
112
115
  </div>
116
+ <h2 v-else class="mint-file-browser__title mint-picker__local-title">{{ title }}</h2>
113
117
  <div
114
118
  v-if="allowedSources.length > 1"
115
119
  class="mint-picker__sources"
@@ -140,7 +144,7 @@ const {
140
144
  </div>
141
145
  </template>
142
146
  <div :key="source" class="mint-picker mint-picker__source-panel" @keydown="keyboard">
143
- <div class="mint-picker__pathbar">
147
+ <div v-if="source === 'server'" class="mint-picker__pathbar">
144
148
  <IconButton
145
149
  label="Up one level"
146
150
  size="sm"
@@ -190,47 +194,25 @@ const {
190
194
  No data sources are enabled.
191
195
  </div>
192
196
  <template v-else>
193
- <div v-if="source === 'localFile'" class="mint-picker__local-actions">
194
- <input
195
- ref="filesInput"
196
- type="file"
197
+ <div v-if="source === 'localFile'" class="mint-picker__local-upload">
198
+ <FileUploader
199
+ size="lg"
200
+ :mode="selectionMode === 'folder' ? 'folder' : selectionMode === 'single-file' ? 'file' : 'file+folder'"
197
201
  :multiple="selectionMode !== 'single-file'"
198
- :accept="accept"
199
- hidden
200
- @change="chooseLocal"
202
+ :show-files="false"
203
+ @upload="(...args) => { uploadError = ''; chooseLocal(...args) }"
204
+ @error="uploadError = $event"
201
205
  />
202
- <input
203
- ref="folderInput"
204
- type="file"
205
- webkitdirectory
206
- multiple
207
- hidden
208
- @change="chooseLocal"
209
- />
210
- <BaseButton
211
- v-if="selectionMode !== 'folder'"
212
- variant="secondary"
213
- size="sm"
214
- @click="filesInput?.click()"
215
- >Choose files…</BaseButton
216
- >
217
- <BaseButton
218
- v-if="selectionMode !== 'single-file'"
219
- variant="secondary"
220
- size="sm"
221
- @click="folderInput?.click()"
222
- >Choose folder…</BaseButton
223
- >
224
- <span>Files stay in this browser.</span>
206
+ <p v-if="uploadError || error || resolutionError" role="alert">
207
+ {{ uploadError || error || resolutionError }}
208
+ </p>
225
209
  </div>
226
- <div class="mint-picker__workspace">
210
+ <div v-else class="mint-picker__workspace">
227
211
  <FilePickerTreeInternal
228
212
  :empty-title="
229
213
  path
230
214
  ? 'No readable files'
231
- : source === 'localFile'
232
- ? 'No files chosen'
233
- : 'No server locations configured'
215
+ : 'No server locations configured'
234
216
  "
235
217
  :rows="rows"
236
218
  :loading="loading"
@@ -274,12 +256,13 @@ const {
274
256
  <span v-if="roots.length">{{ resolving ? 'Checking…' : formatBytes(chosen.bytes) }}</span>
275
257
  <span v-else>No selection</span>
276
258
  <BaseButton
277
- v-if="chosen.excluded.length"
259
+ v-if="chosen.excluded.length && source === 'server'"
278
260
  variant="ghost"
279
261
  size="sm"
280
262
  @click="reviewing = !reviewing"
281
263
  >{{ chosen.excluded.length }} skipped</BaseButton
282
264
  >
265
+ <span v-else-if="chosen.excluded.length">{{ chosen.excluded.length }} skipped</span>
283
266
  <BaseButton
284
267
  v-if="capability?.isBlank"
285
268
  size="sm"
@@ -8,7 +8,9 @@ interface Props {
8
8
  maxSize?: number
9
9
  disabled?: boolean
10
10
  size?: 'sm' | 'md' | 'lg'
11
- mode?: 'file' | 'folder'
11
+ mode?: 'file' | 'folder' | 'file+folder'
12
+ /** Hide the selection list when a parent presents the files itself. */
13
+ showFiles?: boolean
12
14
  }
13
15
 
14
16
  const props = withDefaults(defineProps<Props>(), {
@@ -16,6 +18,7 @@ const props = withDefaults(defineProps<Props>(), {
16
18
  disabled: false,
17
19
  size: 'md',
18
20
  mode: 'file',
21
+ showFiles: true,
19
22
  })
20
23
 
21
24
  const emit = defineEmits<{
@@ -30,9 +33,10 @@ const selectedFiles = ref<File[]>([])
30
33
  const folderName = ref<string | null>(null)
31
34
 
32
35
  const isFolder = computed(() => props.mode === 'folder')
36
+ const isMixed = computed(() => props.mode === 'file+folder')
33
37
 
34
38
  const acceptLabel = computed(() => {
35
- if (!props.accept) return 'any file'
39
+ if (!props.accept) return isFolder.value ? 'a folder' : 'any file'
36
40
  return props.accept.split(',').map(a => a.trim().replace('.', '').toUpperCase()).join(', ')
37
41
  })
38
42
 
@@ -154,15 +158,19 @@ function extractFolderName(files: File[]): string | null {
154
158
  return null
155
159
  }
156
160
 
157
- function handleDrop(event: DragEvent) {
161
+ async function handleDrop(event: DragEvent) {
158
162
  event.preventDefault()
159
163
  isDragOver.value = false
160
164
  if (props.disabled) return
161
165
 
162
- if (isFolder.value && event.dataTransfer?.items) {
163
- handleFolderDrop(event.dataTransfer.items)
164
- } else if (event.dataTransfer?.files) {
165
- handleFiles(event.dataTransfer.files)
166
+ try {
167
+ if (props.mode !== 'file' && event.dataTransfer?.items?.length) {
168
+ await handleFolderDrop(event.dataTransfer.items)
169
+ } else if (event.dataTransfer?.files) {
170
+ handleFiles(event.dataTransfer.files)
171
+ }
172
+ } catch (cause) {
173
+ emit('error', cause instanceof Error ? cause.message : 'Could not read the dropped folder.')
166
174
  }
167
175
  }
168
176
 
@@ -171,8 +179,12 @@ async function handleFolderDrop(items: DataTransferItemList) {
171
179
  const relativePaths: string[] = []
172
180
  let folderNameFromDrop: string | undefined
173
181
 
174
- for (const item of Array.from(items)) {
182
+ // Capture live drag data before awaiting: the browser protects it after the drop handler.
183
+ const dropped = Array.from(items, (item) => {
175
184
  const entry = item.webkitGetAsEntry?.()
185
+ return { entry, file: entry ? null : item.getAsFile?.() }
186
+ })
187
+ for (const { entry, file: droppedFile } of dropped) {
176
188
  if (entry?.isDirectory) {
177
189
  folderNameFromDrop = entry.name
178
190
  await readDirectory(
@@ -181,8 +193,10 @@ async function handleFolderDrop(items: DataTransferItemList) {
181
193
  relativePaths,
182
194
  entry.name,
183
195
  )
184
- } else if (entry?.isFile) {
185
- const file = await getFile(entry as FileSystemFileEntry)
196
+ } else {
197
+ const file = entry?.isFile
198
+ ? await getFile(entry as FileSystemFileEntry)
199
+ : droppedFile
186
200
  if (file) {
187
201
  files.push(file)
188
202
  relativePaths.push(file.name)
@@ -291,7 +305,11 @@ function formatFileSize(bytes: number): string {
291
305
  <div
292
306
  role="button"
293
307
  tabindex="0"
294
- :aria-label="disabled ? 'File upload disabled' : 'Click or drag files to upload'"
308
+ :aria-label="disabled
309
+ ? 'File upload disabled'
310
+ : isMixed
311
+ ? 'Drop files or a folder, or click to choose files'
312
+ : isFolder ? 'Click or drag a folder to upload' : 'Click or drag files to upload'"
295
313
  :aria-disabled="disabled"
296
314
  :class="[
297
315
  'mint-file-uploader__dropzone',
@@ -345,7 +363,7 @@ function formatFileSize(bytes: number): string {
345
363
  </svg>
346
364
 
347
365
  <p class="mint-file-uploader__text">
348
- Drop {{ acceptLabel }} or click
366
+ {{ isMixed ? 'Drop files or a folder, or click to choose files' : `Drop ${acceptLabel} or click` }}
349
367
  </p>
350
368
  </div>
351
369
  </div>
@@ -356,7 +374,7 @@ function formatFileSize(bytes: number): string {
356
374
  </div>
357
375
 
358
376
  <!-- Folder summary display -->
359
- <div v-if="isFolder && folderName && selectedFiles.length > 0" class="mint-file-uploader__folder-summary">
377
+ <div v-if="showFiles && isFolder && folderName && selectedFiles.length > 0" class="mint-file-uploader__folder-summary">
360
378
  <div class="mint-file-uploader__folder-info">
361
379
  <div class="mint-file-uploader__folder-icon-wrapper">
362
380
  <svg class="mint-file-uploader__folder-icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
@@ -382,7 +400,7 @@ function formatFileSize(bytes: number): string {
382
400
 
383
401
  <!-- File list display -->
384
402
  <TransitionGroup
385
- v-if="!isFolder && selectedFiles.length > 0"
403
+ v-if="showFiles && !isFolder && selectedFiles.length > 0"
386
404
  tag="ul"
387
405
  name="mint-file-uploader-item"
388
406
  class="mint-file-uploader__list"
@@ -1,87 +1,52 @@
1
1
  <script setup lang="ts">
2
- import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
3
2
  import type * as PlotlyType from 'plotly.js'
3
+ import { computed, toRaw } from 'vue'
4
4
 
5
- import ChartContainer from './ChartContainer.vue'
5
+ import PlotlyChart from './PlotlyChart.vue'
6
6
 
7
7
  const props = defineProps<{
8
8
  spec: Record<string, unknown>
9
9
  fallback?: { media_type: string; data: string }
10
10
  }>()
11
11
 
12
- const chartElement = ref<HTMLDivElement | null>(null)
13
- const failed = ref(false)
14
- let plotly: typeof PlotlyType | null = null
15
-
16
- async function loadPlotly(): Promise<typeof PlotlyType> {
17
- const module = await import('plotly.js-dist-min')
18
- return ('default' in module ? module.default : module) as unknown as typeof PlotlyType
19
- }
20
-
21
- async function renderChart(): Promise<void> {
22
- if (!chartElement.value) return
23
- failed.value = false
24
- await nextTick()
25
- try {
26
- const renderer = plotly ?? await loadPlotly()
27
- plotly = renderer
28
- const data = Array.isArray(props.spec.data) ? props.spec.data : []
29
- const layout = props.spec.layout && typeof props.spec.layout === 'object'
30
- ? props.spec.layout
31
- : {}
32
- await renderer.react(
33
- chartElement.value,
34
- data as PlotlyType.Data[],
35
- layout as Partial<PlotlyType.Layout>,
36
- { responsive: true, displaylogo: false },
37
- )
38
- failed.value = false
39
- } catch {
40
- failed.value = true
41
- }
42
- }
43
-
44
- onMounted(renderChart)
45
- watch(() => props.spec, async () => {
46
- await nextTick()
47
- await renderChart()
48
- })
49
- onBeforeUnmount(() => {
50
- if (plotly && chartElement.value) plotly.purge(chartElement.value)
12
+ const data = computed<PlotlyType.Data[]>(() => {
13
+ const traces = toRaw(props.spec).data
14
+ return Array.isArray(traces) ? [...toRaw(traces)] as PlotlyType.Data[] : []
51
15
  })
16
+ const layout = computed<Partial<PlotlyType.Layout>>(() => (
17
+ props.spec.layout && typeof props.spec.layout === 'object'
18
+ ? props.spec.layout as Partial<PlotlyType.Layout>
19
+ : {}
20
+ ))
21
+ const config = computed<Partial<PlotlyType.Config>>(() => (
22
+ props.spec.config && typeof props.spec.config === 'object'
23
+ ? props.spec.config as Partial<PlotlyType.Config>
24
+ : {}
25
+ ))
52
26
  </script>
53
27
 
54
28
  <template>
55
- <ChartContainer title="Interactive chart">
56
- <img
57
- v-if="failed && fallback"
58
- class="mint-generated-result__image"
59
- :src="`data:${fallback.media_type};base64,${fallback.data}`"
60
- alt="Static Plotly chart fallback"
61
- >
62
- <p
63
- v-else-if="failed"
64
- class="mint-generated-result__plotly-error"
65
- role="alert"
66
- >
67
- Unable to display this chart.
68
- </p>
69
- <div
70
- ref="chartElement"
71
- v-show="!failed"
72
- class="mint-generated-result__plotly"
73
- aria-label="Interactive Plotly chart"
74
- />
75
- </ChartContainer>
29
+ <PlotlyChart
30
+ title="Interactive chart"
31
+ :data="data"
32
+ :layout="layout"
33
+ :config="config"
34
+ >
35
+ <template v-if="fallback" #error>
36
+ <img
37
+ class="mint-generated-result__image"
38
+ :src="`data:${fallback.media_type};base64,${fallback.data}`"
39
+ alt="Static Plotly chart fallback"
40
+ >
41
+ </template>
42
+ </PlotlyChart>
76
43
  </template>
77
44
 
78
45
  <style scoped>
79
- .mint-generated-result__plotly-error {
80
- margin: 0;
81
- padding: 1rem;
82
- border: 1px solid var(--mint-error-border);
83
- border-radius: 0.75rem;
84
- background: var(--mint-error-bg);
85
- color: var(--mint-error);
46
+ .mint-generated-result__image {
47
+ display: block;
48
+ max-width: 100%;
49
+ height: auto;
50
+ margin: 0 auto;
86
51
  }
87
52
  </style>
@@ -128,11 +128,6 @@ function humanize(value: string): string {
128
128
  margin: 0 auto;
129
129
  }
130
130
 
131
- .mint-generated-result__plotly {
132
- width: 100%;
133
- min-height: 24rem;
134
- }
135
-
136
131
  .mint-generated-result__artifact {
137
132
  display: flex;
138
133
  align-items: center;
@@ -0,0 +1,61 @@
1
+ <script setup lang="ts">
2
+ import type * as Plotly from 'plotly.js'
3
+
4
+ import PlotlyChart from './PlotlyChart.vue'
5
+
6
+ const data: Plotly.Data[] = [
7
+ {
8
+ type: 'scatter',
9
+ mode: 'lines+markers',
10
+ name: 'Control',
11
+ x: ['0 h', '6 h', '12 h', '24 h'],
12
+ y: [1, 1.3, 1.8, 2.1],
13
+ },
14
+ {
15
+ type: 'scatter',
16
+ mode: 'lines+markers',
17
+ name: 'Treatment',
18
+ x: ['0 h', '6 h', '12 h', '24 h'],
19
+ y: [1, 1.8, 3.1, 4.4],
20
+ },
21
+ ]
22
+
23
+ const layout: Partial<Plotly.Layout> = {
24
+ margin: { t: 24, r: 24, b: 56, l: 56 },
25
+ xaxis: { title: { text: 'Time' } },
26
+ yaxis: { title: { text: 'Relative abundance' } },
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <Story title="Data Display/PlotlyChart">
32
+ <Variant title="Native Plotly">
33
+ <div style="max-width: 760px; margin: 0 auto; padding: 2rem;">
34
+ <PlotlyChart
35
+ title="Metabolite response"
36
+ description="Native Plotly traces with MINT theme and resize handling"
37
+ :data="data"
38
+ :layout="layout"
39
+ :config="{ toImageButtonOptions: { filename: 'metabolite-response' } }"
40
+ />
41
+ </div>
42
+ </Variant>
43
+
44
+ <Variant title="Empty">
45
+ <div style="max-width: 760px; margin: 0 auto; padding: 2rem;">
46
+ <PlotlyChart
47
+ title="Metabolite response"
48
+ :data="[]"
49
+ empty
50
+ empty-message="No measurements are available."
51
+ />
52
+ </div>
53
+ </Variant>
54
+
55
+ <Variant title="Loading">
56
+ <div style="max-width: 760px; margin: 0 auto; padding: 2rem;">
57
+ <PlotlyChart title="Loading measurements" :data="[]" loading />
58
+ </div>
59
+ </Variant>
60
+ </Story>
61
+ </template>