@morscherlab/mint-sdk 1.2.0-beta.8 → 1.2.0-beta.9
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-CfOdh5Rm.js → ExperimentPopover-oL0fg_A0.js} +5 -3
- package/dist/ExperimentPopover-oL0fg_A0.js.map +1 -0
- package/dist/{ExperimentSelectorModal-DvIagrxY.js → ExperimentSelectorModal-BeEOomw_.js} +4 -4
- package/dist/ExperimentSelectorModal-BeEOomw_.js.map +1 -0
- package/dist/{SettingsModal-W3TYfnP9.js → SettingsModal-Dphh3RrI.js} +4 -4
- package/dist/SettingsModal-Dphh3RrI.js.map +1 -0
- package/dist/{EmptyState-NBqf2_8z.js → Skeleton-DxtwJ5gt.js} +110 -92
- package/dist/Skeleton-DxtwJ5gt.js.map +1 -0
- package/dist/__tests__/components/ExperimentTimeline.test.d.ts +1 -0
- package/dist/__tests__/composables/useListReorder.test.d.ts +1 -0
- package/dist/__tests__/styles/cssTokens.test.d.ts +1 -0
- package/dist/components/ActionMenu.vue.d.ts +1 -1
- package/dist/components/AppPluginSwitcher.vue.d.ts +2 -0
- package/dist/components/AuditTrail.vue.d.ts +13 -0
- package/dist/components/BatchProgressList.vue.d.ts +17 -0
- package/dist/components/DataFrame.vue.d.ts +14 -2
- package/dist/components/DatePicker.vue.d.ts +1 -1
- package/dist/components/DateTimePicker.vue.d.ts +2 -0
- package/dist/components/ExperimentDataViewer.vue.d.ts +26 -1
- package/dist/components/ExperimentPopover.vue.d.ts +1 -0
- package/dist/components/ExperimentTimeline.vue.d.ts +29 -1
- package/dist/components/LcmsSequenceTable.vue.d.ts +29 -1
- package/dist/components/ProgressBar.vue.d.ts +1 -1
- package/dist/components/ProtocolStepEditor.vue.d.ts +1 -0
- package/dist/components/ReagentList.vue.d.ts +29 -1
- package/dist/components/SampleHierarchyTree.vue.d.ts +29 -1
- package/dist/components/SampleSelector.drag.d.ts +13 -0
- package/dist/components/SampleSelector.tree.d.ts +2 -0
- package/dist/components/SampleSelector.vue.d.ts +34 -1
- package/dist/components/ScheduleCalendar.vue.d.ts +20 -0
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -0
- package/dist/components/TimePicker.vue.d.ts +1 -0
- package/dist/components/WellPlate.geometry.d.ts +15 -0
- package/dist/components/WellPlate.interaction.d.ts +13 -0
- package/dist/components/WellPlate.vue.d.ts +29 -0
- package/dist/components/index.js +6 -6
- package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -0
- package/dist/components/internal/DataStateInternal.vue.d.ts +43 -0
- package/dist/{components-B80gV0P6.js → components-CgAUzF8F.js} +1930 -1688
- package/dist/components-CgAUzF8F.js.map +1 -0
- package/dist/composables/controlSchemaTypes.d.ts +34 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.js +9 -9
- package/dist/composables/useDropdownState.d.ts +18 -2
- package/dist/composables/useEventStream.d.ts +9 -0
- package/dist/composables/useListReorder.d.ts +90 -0
- package/dist/composables/useMenuKeyboard.d.ts +2 -2
- package/dist/composables/usePluginConfig.d.ts +3 -1
- package/dist/composables/useRequestSyncState.d.ts +42 -2
- package/dist/composables/useScheduleDrag.d.ts +1 -0
- package/dist/{composables-C8mmD5ta.js → composables-B_pia3hO.js} +13 -69
- package/dist/composables-B_pia3hO.js.map +1 -0
- package/dist/{useApi-CDBq6z23.js → errors-_W0-LbA6.js} +102 -3
- package/dist/errors-_W0-LbA6.js.map +1 -0
- package/dist/{experiment-Y9Iz7KYi.js → experiment-Cwl9AYxw.js} +3 -8
- package/dist/experiment-Cwl9AYxw.js.map +1 -0
- package/dist/experiment-utils-DV3DrvE_.js.map +1 -1
- package/dist/index.js +14 -14
- package/dist/install.js +2 -7
- package/dist/install.js.map +1 -1
- package/dist/permissions.d.ts +42 -1
- package/dist/permissions.js +20 -10
- package/dist/permissions.js.map +1 -1
- package/dist/stores/index.d.ts +1 -1
- package/dist/stores/index.js +3 -3
- package/dist/stores/settings.d.ts +31 -1
- package/dist/styles.css +9572 -27877
- package/dist/templates/index.js +2 -4
- package/dist/{templates-CK72aKO1.js → templates-Qq113pyZ.js} +291 -85
- package/dist/templates-Qq113pyZ.js.map +1 -0
- package/dist/{useControlSchema-_OVhavKq.js → useControlSchema-aMZlb4Sc.js} +197 -1089
- package/dist/useControlSchema-aMZlb4Sc.js.map +1 -0
- package/dist/{useDropdownState-D5G4UvWd.js → useDropdownState-DitCtrTT.js} +21 -5
- package/dist/useDropdownState-DitCtrTT.js.map +1 -0
- package/dist/{useExperimentSelector-DSEckf_g.js → useExperimentSelector-I8HC8hUu.js} +3 -3
- package/dist/{useExperimentSelector-DSEckf_g.js.map → useExperimentSelector-I8HC8hUu.js.map} +1 -1
- package/dist/{useFormBuilder-Bd714C7y.js → useFormBuilder-TCxNnqeb.js} +2 -2
- package/dist/{useFormBuilder-Bd714C7y.js.map → useFormBuilder-TCxNnqeb.js.map} +1 -1
- package/dist/{useProtocolTemplates-Dq5Pzwyd.js → useProtocolTemplates-BmX_iVBR.js} +185 -157
- package/dist/useProtocolTemplates-BmX_iVBR.js.map +1 -0
- package/dist/utils/color.d.ts +13 -0
- package/dist/utils/errors.d.ts +9 -0
- package/package.json +1 -1
- package/src/__tests__/components/ActionMenu.test.ts +94 -0
- package/src/__tests__/components/DataFrame.test.ts +183 -11
- package/src/__tests__/components/DataStates.test.ts +176 -0
- package/src/__tests__/components/DatePicker.test.ts +76 -0
- package/src/__tests__/components/DateTimePicker.test.ts +36 -0
- package/src/__tests__/components/ExperimentTimeline.test.ts +63 -0
- package/src/__tests__/components/GroupAssigner.test.ts +53 -0
- package/src/__tests__/components/LcmsSequenceTable.test.ts +22 -0
- package/src/__tests__/components/RackEditor.test.ts +35 -0
- package/src/__tests__/components/ReagentList.test.ts +64 -2
- package/src/__tests__/components/RetiredStyles.test.ts +31 -0
- package/src/__tests__/components/SampleSelector.drag.test.ts +51 -1
- package/src/__tests__/components/ScheduleCalendar.test.ts +63 -0
- package/src/__tests__/components/SegmentedControl.test.ts +88 -0
- package/src/__tests__/components/TimePicker.test.ts +61 -0
- package/src/__tests__/components/WellPlate.interaction.test.ts +59 -0
- package/src/__tests__/composables/useDropdownState.test.ts +58 -1
- package/src/__tests__/composables/useListReorder.test.ts +145 -0
- package/src/__tests__/composables/useRequestSyncState.test.ts +140 -0
- package/src/__tests__/stores/settings.test.ts +71 -1
- package/src/__tests__/styles/cssTokens.test.ts +131 -0
- package/src/__tests__/utils/permissions.test.ts +68 -0
- package/src/components/ActionMenu.vue +45 -88
- package/src/components/AlertBox.vue +0 -4
- package/src/components/AppAvatarMenu.vue +2 -14
- package/src/components/AppLayout.vue +0 -4
- package/src/components/AppPluginSwitcher.story.vue +87 -0
- package/src/components/AppPluginSwitcher.vue +2 -5
- package/src/components/AppSidebar.vue +0 -4
- package/src/components/AppToastContainer.vue +6 -8
- package/src/components/AppTopBar.vue +0 -4
- package/src/components/ArtifactSaveDialog.vue +0 -4
- package/src/components/ArtifactSelectorModal.vue +0 -4
- package/src/components/AuditTrail.story.vue +18 -0
- package/src/components/AuditTrail.vue +27 -7
- package/src/components/AutoGroupModal.vue +0 -4
- package/src/components/Avatar.vue +0 -4
- package/src/components/BaseButton.vue +0 -4
- package/src/components/BaseCheckbox.vue +0 -4
- package/src/components/BaseInput.vue +0 -4
- package/src/components/BaseModal.vue +0 -4
- package/src/components/BasePill.vue +0 -4
- package/src/components/BaseRadioGroup.vue +0 -4
- package/src/components/BaseSelect.vue +0 -4
- package/src/components/BaseSlider.vue +0 -4
- package/src/components/BaseTabs.vue +0 -4
- package/src/components/BaseTextarea.vue +0 -4
- package/src/components/BaseToggle.vue +0 -4
- package/src/components/BatchProgressList.story.vue +18 -0
- package/src/components/BatchProgressList.vue +32 -4
- package/src/components/Breadcrumb.vue +0 -4
- package/src/components/Calendar.vue +0 -4
- package/src/components/ChartContainer.story.vue +1 -1
- package/src/components/ChartContainer.vue +0 -4
- package/src/components/ChemicalFormula.vue +0 -4
- package/src/components/CollapsibleCard.vue +0 -4
- package/src/components/ColorSlider.vue +0 -4
- package/src/components/ConcentrationInput.vue +0 -4
- package/src/components/ConfirmDialog.vue +0 -4
- package/src/components/DataFrame.story.vue +14 -0
- package/src/components/DataFrame.vue +97 -23
- package/src/components/DatePicker.vue +73 -27
- package/src/components/DateTimePicker.vue +45 -17
- package/src/components/Divider.vue +0 -4
- package/src/components/DoseCalculator.vue +0 -4
- package/src/components/DropdownButton.vue +2 -14
- package/src/components/EmptyState.vue +0 -4
- package/src/components/ExperimentCodeBadge.vue +0 -4
- package/src/components/ExperimentDataViewer.story.vue +3 -0
- package/src/components/ExperimentDataViewer.vue +33 -13
- package/src/components/ExperimentPopover.vue +2 -5
- package/src/components/ExperimentSelectorModal.vue +0 -4
- package/src/components/ExperimentTimeline.story.vue +18 -0
- package/src/components/ExperimentTimeline.vue +81 -75
- package/src/components/FileUploader.vue +0 -4
- package/src/components/FitPanel.vue +0 -4
- package/src/components/FormActions.story.vue +96 -0
- package/src/components/FormActions.vue +0 -4
- package/src/components/FormBuilder.vue +0 -4
- package/src/components/FormField.vue +0 -4
- package/src/components/FormulaInput.vue +0 -4
- package/src/components/GeneratedPathInput.story.vue +68 -0
- package/src/components/GeneratedPlotlyResult.story.vue +96 -0
- package/src/components/GeneratedPlotlyResult.vue +3 -3
- package/src/components/GeneratedPluginView.story.vue +145 -0
- package/src/components/GeneratedPluginView.vue +10 -10
- package/src/components/GeneratedResultRenderer.story.vue +117 -0
- package/src/components/GeneratedResultRenderer.vue +4 -4
- package/src/components/GroupAssigner.vue +67 -5
- package/src/components/IconButton.vue +0 -4
- package/src/components/InstrumentAlertLog.vue +0 -4
- package/src/components/InstrumentStateBadge.vue +0 -4
- package/src/components/InstrumentStatusCard.vue +0 -4
- package/src/components/JobsStatusTray.vue +0 -4
- package/src/components/LcmsSequenceTable.vue +81 -50
- package/src/components/LoadingSpinner.vue +0 -4
- package/src/components/MobileSupportGate.vue +0 -4
- package/src/components/MoleculeInput.vue +0 -4
- package/src/components/MultiSelect.vue +0 -4
- package/src/components/NumberInput.vue +0 -4
- package/src/components/PlateMapEditor.vue +3 -14
- package/src/components/PluginIcon.vue +0 -4
- package/src/components/ProgressBar.vue +0 -4
- package/src/components/ProtocolStepEditor.vue +2 -4
- package/src/components/RackEditor.vue +51 -70
- package/src/components/ReagentEditor.vue +0 -4
- package/src/components/ReagentList.story.vue +18 -0
- package/src/components/ReagentList.vue +127 -82
- package/src/components/ResourceCard.vue +0 -4
- package/src/components/RuntimeUpdateNotice.story.vue +49 -0
- package/src/components/RuntimeUpdateNotice.vue +4 -4
- package/src/components/SampleHierarchyTree.story.vue +18 -0
- package/src/components/SampleHierarchyTree.vue +32 -9
- package/src/components/SampleLegend.vue +2 -8
- package/src/components/SampleSelector.drag.ts +51 -0
- package/src/components/SampleSelector.story.vue +6 -0
- package/src/components/SampleSelector.tree.ts +2 -0
- package/src/components/SampleSelector.vue +49 -10
- package/src/components/SampleSelectorGroupNode.vue +19 -0
- package/src/components/ScheduleCalendar.story.vue +6 -0
- package/src/components/ScheduleCalendar.vue +82 -7
- package/src/components/ScientificNumber.vue +0 -4
- package/src/components/SectionCard.vue +0 -4
- package/src/components/SegmentedControl.vue +48 -5
- package/src/components/SequenceInput.vue +0 -4
- package/src/components/SequenceProgressBar.story.vue +110 -0
- package/src/components/SequenceProgressBar.vue +0 -4
- package/src/components/SettingsModal.vue +0 -4
- package/src/components/Skeleton.vue +0 -4
- package/src/components/SmartGroupFieldRecipe.vue +8 -6
- package/src/components/SmartGroupManual.vue +0 -4
- package/src/components/StatusIndicator.vue +0 -4
- package/src/components/StepWizard.vue +0 -4
- package/src/components/TagsInput.vue +4 -6
- package/src/components/ThemeToggle.vue +0 -4
- package/src/components/TimePicker.vue +36 -7
- package/src/components/TimeRangeInput.vue +0 -4
- package/src/components/Tooltip.story.vue +3 -3
- package/src/components/Tooltip.vue +0 -4
- package/src/components/UnitInput.vue +0 -4
- package/src/components/WellPlate.geometry.ts +60 -1
- package/src/components/WellPlate.interaction.ts +44 -0
- package/src/components/WellPlate.story.vue +8 -0
- package/src/components/WellPlate.vue +53 -7
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +2 -5
- package/src/components/internal/AppTopBarPillNavInternal.vue +0 -4
- package/src/components/internal/DataStateInternal.vue +74 -0
- package/src/components/internal/FormSectionRenderer.vue +0 -4
- package/src/components/internal/WellEditPopupInternal.vue +0 -4
- package/src/composables/controlSchemaTypes.ts +34 -0
- package/src/composables/index.ts +10 -0
- package/src/composables/useDropdownState.ts +35 -5
- package/src/composables/useEventStream.ts +14 -2
- package/src/composables/useFileImport.ts +2 -7
- package/src/composables/useGeneratedAnalysis.ts +8 -9
- package/src/composables/useListReorder.story.vue +143 -0
- package/src/composables/useListReorder.ts +242 -0
- package/src/composables/useMenuKeyboard.ts +2 -2
- package/src/composables/useOptimisticMutation.ts +1 -15
- package/src/composables/usePluginConfig.ts +3 -1
- package/src/composables/useRackEditor.ts +2 -11
- package/src/composables/useRequestSyncState.ts +98 -28
- package/src/composables/useResourceCrud.ts +1 -15
- package/src/composables/useScheduleDrag.ts +37 -0
- package/src/composables/useWellPainting.ts +5 -29
- package/src/composables/useWellPlateEditor.ts +1 -11
- package/src/permissions.ts +48 -9
- package/src/stores/experiment.ts +2 -7
- package/src/stores/index.ts +9 -1
- package/src/stores/settings.ts +93 -1
- package/src/styles/components/audit-trail.css +5 -5
- package/src/styles/components/batch-progress-list.css +7 -0
- package/src/styles/components/color-slider.css +2 -2
- package/src/styles/components/data-state.css +30 -0
- package/src/styles/components/dataframe.css +29 -35
- package/src/styles/components/experiment-data-viewer.css +4 -4
- package/src/styles/components/experiment-timeline.css +3 -30
- package/src/styles/components/form-builder.css +5 -5
- package/src/styles/components/lcms-sequence-table.css +16 -1
- package/src/styles/components/multi-select.css +1 -1
- package/src/styles/components/reagent-list.css +38 -31
- package/src/styles/components/sample-hierarchy-tree.css +5 -19
- package/src/styles/components/sample-selector.css +9 -2
- package/src/styles/components/schedule-calendar.css +8 -0
- package/src/styles/components/scientific-number.css +1 -1
- package/src/styles/components/well-plate.css +8 -0
- package/src/styles/index.css +8 -0
- package/src/styles/variables.css +29 -0
- package/src/templates/builderPresetControls.ts +1 -1
- package/src/templates/componentPlateHelpers.ts +3 -1
- package/src/templates/doseResponseAdapters.ts +2 -13
- package/src/templates/elisaAssayCollectionBuilder.ts +2 -2
- package/src/templates/plateMapBuilder.ts +2 -2
- package/src/templates/qpcrPlateBuilder.ts +1 -1
- package/src/templates/templateValidators.ts +1 -1
- package/src/templates/timeCourseBuilder.ts +2 -2
- package/src/templates/wellPlateScreenCollectionBuilder.ts +2 -2
- package/src/utils/color.ts +51 -0
- package/src/utils/errors.ts +29 -0
- package/dist/EmptyState-NBqf2_8z.js.map +0 -1
- package/dist/ExperimentPopover-CfOdh5Rm.js.map +0 -1
- package/dist/ExperimentSelectorModal-DvIagrxY.js.map +0 -1
- package/dist/SettingsModal-W3TYfnP9.js.map +0 -1
- package/dist/components-B80gV0P6.js.map +0 -1
- package/dist/composables/autoGroupConstants.d.ts +0 -1
- package/dist/composables/autoGroupGrouping.d.ts +0 -3
- package/dist/composables/useAutoGroupInputSources.d.ts +0 -32
- package/dist/composables-C8mmD5ta.js.map +0 -1
- package/dist/experiment-Y9Iz7KYi.js.map +0 -1
- package/dist/templates/builderDefaults.d.ts +0 -1
- package/dist/templates/plateGeometry.d.ts +0 -4
- package/dist/templates-CK72aKO1.js.map +0 -1
- package/dist/useApi-CDBq6z23.js.map +0 -1
- package/dist/useControlSchema-_OVhavKq.js.map +0 -1
- package/dist/useDropdownState-D5G4UvWd.js.map +0 -1
- package/dist/useProtocolTemplates-Dq5Pzwyd.js.map +0 -1
- package/src/__tests__/composables/useAutoGroupInputSources.test.ts +0 -127
- package/src/composables/autoGroupConstants.ts +0 -4
- package/src/composables/autoGroupGrouping.ts +0 -148
- package/src/composables/useAutoGroupInputSources.ts +0 -147
- package/src/templates/builderDefaults.ts +0 -11
- package/src/templates/plateGeometry.ts +0 -62
- /package/dist/__tests__/{composables/useAutoGroupInputSources.test.d.ts → components/DataStates.test.d.ts} +0 -0
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import { ref, type Ref } from 'vue'
|
|
1
|
+
import { nextTick, ref, type Ref } from 'vue'
|
|
2
2
|
import { useEventListener } from './useEventListener'
|
|
3
3
|
|
|
4
4
|
export interface UseDropdownStateOptions {
|
|
5
5
|
closeOnOutsideClick?: boolean
|
|
6
6
|
closeOnEscape?: boolean
|
|
7
|
+
/** Close the panel when the page scrolls or the viewport resizes. Default `false`. */
|
|
8
|
+
closeOnScroll?: boolean
|
|
7
9
|
insideRefs?: Array<Ref<HTMLElement | null>>
|
|
10
|
+
/**
|
|
11
|
+
* Element focus returns to when the panel closes with `close(true)` (Escape).
|
|
12
|
+
* Pass one when the trigger lives outside `rootRef`; otherwise bind the
|
|
13
|
+
* returned `triggerRef` to the trigger element.
|
|
14
|
+
*/
|
|
15
|
+
triggerRef?: Ref<HTMLElement | null>
|
|
8
16
|
onOpen?: () => void
|
|
9
17
|
onClose?: () => void
|
|
10
18
|
}
|
|
@@ -12,19 +20,29 @@ export interface UseDropdownStateOptions {
|
|
|
12
20
|
export interface UseDropdownStateReturn {
|
|
13
21
|
isOpen: Ref<boolean>
|
|
14
22
|
rootRef: Ref<HTMLElement | null>
|
|
23
|
+
/** Bind to the trigger element so Escape can return focus to it. */
|
|
24
|
+
triggerRef: Ref<HTMLElement | null>
|
|
15
25
|
open: () => void
|
|
16
|
-
|
|
26
|
+
/** Closes the panel; `returnFocus` moves focus back to `triggerRef`. */
|
|
27
|
+
close: (returnFocus?: boolean) => void
|
|
17
28
|
toggle: () => void
|
|
18
29
|
handleClickOutside: (event: MouseEvent) => void
|
|
19
30
|
handleKeydown: (event: KeyboardEvent) => void
|
|
20
31
|
}
|
|
21
32
|
|
|
22
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Shared disclosure state for dropdown-style SDK components: open/close state,
|
|
35
|
+
* outside-click and Escape closing, optional scroll/resize closing, and focus
|
|
36
|
+
* return to the trigger. `close(returnFocus)` mirrors `useMenuKeyboard`'s
|
|
37
|
+
* contract, so the two compose directly.
|
|
38
|
+
*/
|
|
23
39
|
export function useDropdownState(options: UseDropdownStateOptions = {}): UseDropdownStateReturn {
|
|
24
40
|
const isOpen = ref(false)
|
|
25
41
|
const rootRef = ref<HTMLElement | null>(null)
|
|
42
|
+
const triggerRef = options.triggerRef ?? ref<HTMLElement | null>(null)
|
|
26
43
|
const closeOnOutsideClick = options.closeOnOutsideClick ?? true
|
|
27
44
|
const closeOnEscape = options.closeOnEscape ?? true
|
|
45
|
+
const closeOnScroll = options.closeOnScroll ?? false
|
|
28
46
|
|
|
29
47
|
function open(): void {
|
|
30
48
|
if (isOpen.value) return
|
|
@@ -32,10 +50,13 @@ export function useDropdownState(options: UseDropdownStateOptions = {}): UseDrop
|
|
|
32
50
|
options.onOpen?.()
|
|
33
51
|
}
|
|
34
52
|
|
|
35
|
-
function close(): void {
|
|
53
|
+
function close(returnFocus = false): void {
|
|
36
54
|
if (!isOpen.value) return
|
|
37
55
|
isOpen.value = false
|
|
38
56
|
options.onClose?.()
|
|
57
|
+
if (returnFocus) {
|
|
58
|
+
void nextTick(() => triggerRef.value?.focus())
|
|
59
|
+
}
|
|
39
60
|
}
|
|
40
61
|
|
|
41
62
|
function toggle(): void {
|
|
@@ -60,20 +81,29 @@ export function useDropdownState(options: UseDropdownStateOptions = {}): UseDrop
|
|
|
60
81
|
|
|
61
82
|
function handleKeydown(event: KeyboardEvent): void {
|
|
62
83
|
if (closeOnEscape && event.key === 'Escape') {
|
|
63
|
-
close()
|
|
84
|
+
close(true)
|
|
64
85
|
}
|
|
65
86
|
}
|
|
66
87
|
|
|
88
|
+
function handleViewportChange(): void {
|
|
89
|
+
if (isOpen.value) close()
|
|
90
|
+
}
|
|
91
|
+
|
|
67
92
|
if (closeOnOutsideClick) {
|
|
68
93
|
useEventListener(() => document, 'click', handleClickOutside)
|
|
69
94
|
}
|
|
70
95
|
if (closeOnEscape) {
|
|
71
96
|
useEventListener(() => document, 'keydown', handleKeydown)
|
|
72
97
|
}
|
|
98
|
+
if (closeOnScroll) {
|
|
99
|
+
useEventListener(() => window, 'scroll', handleViewportChange, true)
|
|
100
|
+
useEventListener(() => window, 'resize', handleViewportChange)
|
|
101
|
+
}
|
|
73
102
|
|
|
74
103
|
return {
|
|
75
104
|
isOpen,
|
|
76
105
|
rootRef,
|
|
106
|
+
triggerRef,
|
|
77
107
|
open,
|
|
78
108
|
close,
|
|
79
109
|
toggle,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
shallowRef,
|
|
8
8
|
type Ref,
|
|
9
9
|
} from 'vue'
|
|
10
|
+
import { tryUseSettingsStore } from '../stores/settings'
|
|
10
11
|
|
|
11
12
|
export type EventStreamTransport = (
|
|
12
13
|
input: RequestInfo | URL,
|
|
@@ -26,7 +27,16 @@ export interface EventStreamOptions<TData = string> {
|
|
|
26
27
|
headers?: HeadersInit
|
|
27
28
|
credentials?: RequestCredentials
|
|
28
29
|
immediate?: boolean
|
|
30
|
+
/**
|
|
31
|
+
* Reconnect after a dropped stream. Left unset, it follows the platform's
|
|
32
|
+
* `wsAutoReconnect` setting, defaulting to `true` without a settings store.
|
|
33
|
+
*/
|
|
29
34
|
reconnect?: boolean
|
|
35
|
+
/**
|
|
36
|
+
* Delay before the first reconnect attempt. Left unset, it follows the
|
|
37
|
+
* platform's `wsReconnectInterval` setting, defaulting to 2000 ms without a
|
|
38
|
+
* settings store. A server-sent `retry:` field still overrides it.
|
|
39
|
+
*/
|
|
30
40
|
reconnectDelayMs?: number
|
|
31
41
|
parseJson?: boolean
|
|
32
42
|
shouldReconnect?: (cause?: unknown) => boolean
|
|
@@ -114,7 +124,9 @@ export function useEventStream<TData = string>(
|
|
|
114
124
|
let controller: AbortController | null = null
|
|
115
125
|
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null
|
|
116
126
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
117
|
-
|
|
127
|
+
const settings = tryUseSettingsStore()
|
|
128
|
+
const reconnectEnabled = options.reconnect ?? settings?.wsAutoReconnect ?? true
|
|
129
|
+
let reconnectDelayMs = options.reconnectDelayMs ?? settings?.wsReconnectInterval ?? 2_000
|
|
118
130
|
let stopped = true
|
|
119
131
|
let generation = 0
|
|
120
132
|
|
|
@@ -135,7 +147,7 @@ export function useEventStream<TData = string>(
|
|
|
135
147
|
|
|
136
148
|
function scheduleReconnect(connectionGeneration: number, cause?: unknown): void {
|
|
137
149
|
if (stopped || generation !== connectionGeneration) return
|
|
138
|
-
if (
|
|
150
|
+
if (!reconnectEnabled || options.shouldReconnect?.(cause) === false) {
|
|
139
151
|
stopped = true
|
|
140
152
|
return
|
|
141
153
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ref, shallowRef, type Ref, type ShallowRef } from 'vue'
|
|
2
|
+
import { readErrorMessage } from '../utils/errors'
|
|
2
3
|
|
|
3
4
|
export interface DelimitedTable {
|
|
4
5
|
columns: string[]
|
|
@@ -63,7 +64,7 @@ export function useFileImport<TResult = string>(
|
|
|
63
64
|
result.value = parsed
|
|
64
65
|
return parsed
|
|
65
66
|
} catch (err) {
|
|
66
|
-
const message = readErrorMessage(err)
|
|
67
|
+
const message = readErrorMessage(err, 'Failed to import file.')
|
|
67
68
|
error.value = message
|
|
68
69
|
throw err
|
|
69
70
|
} finally {
|
|
@@ -223,9 +224,3 @@ function formatBytes(bytes: number): string {
|
|
|
223
224
|
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`
|
|
224
225
|
return `${Math.round(bytes / (1024 * 1024))} MB`
|
|
225
226
|
}
|
|
226
|
-
|
|
227
|
-
function readErrorMessage(value: unknown): string {
|
|
228
|
-
if (value instanceof Error && value.message) return value.message
|
|
229
|
-
if (typeof value === 'string' && value.trim()) return value
|
|
230
|
-
return 'Failed to import file.'
|
|
231
|
-
}
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
PluginJobCenterSource,
|
|
14
14
|
} from './useJobsStatusTray'
|
|
15
15
|
import { useEventStream, type EventStreamMessage } from './useEventStream'
|
|
16
|
-
import { normalizeJobState } from '../jobs'
|
|
16
|
+
import { isActiveJobStatus, normalizeJobState } from '../jobs'
|
|
17
17
|
import type {
|
|
18
18
|
GeneratedAnalysisJob,
|
|
19
19
|
GeneratedAnalysisJobInput,
|
|
@@ -111,7 +111,6 @@ export interface UsePluginJobsReturn extends PluginJobCenterSource {
|
|
|
111
111
|
|
|
112
112
|
export type UseGeneratedAnalysisReturn = UsePluginJobsReturn
|
|
113
113
|
|
|
114
|
-
const ACTIVE_STATUSES = new Set(['awaiting_input', 'queued', 'running'])
|
|
115
114
|
const AUTH_TOKEN_KEY = 'mint-auth-token'
|
|
116
115
|
const SESSION_NOT_FOUND_DETAIL = 'Session not found'
|
|
117
116
|
const MAX_RESULT_CACHE_ENTRIES = 32
|
|
@@ -473,12 +472,12 @@ function createPluginJobs(
|
|
|
473
472
|
.then((payload) => {
|
|
474
473
|
if (disposed) return jobs.value
|
|
475
474
|
if (revision !== jobsRevision) {
|
|
476
|
-
if (jobs.value.some(job =>
|
|
475
|
+
if (jobs.value.some(job => isActiveJobStatus(job.status))) schedulePoll()
|
|
477
476
|
return jobs.value
|
|
478
477
|
}
|
|
479
478
|
replaceJobs(payload.jobs)
|
|
480
479
|
error.value = null
|
|
481
|
-
if (!paused && !jobStream.isConnected.value && jobs.value.some(job =>
|
|
480
|
+
if (!paused && !jobStream.isConnected.value && jobs.value.some(job => isActiveJobStatus(job.status))) {
|
|
482
481
|
schedulePoll()
|
|
483
482
|
}
|
|
484
483
|
return jobs.value
|
|
@@ -530,7 +529,7 @@ function createPluginJobs(
|
|
|
530
529
|
const current = typeof job === 'string'
|
|
531
530
|
? jobs.value.find(candidate => candidate.job_id === jobId)
|
|
532
531
|
: jobs.value.find(candidate => candidate.job_id === jobId) ?? job
|
|
533
|
-
if (current && !
|
|
532
|
+
if (current && !isActiveJobStatus(current.status)) {
|
|
534
533
|
return terminalWaitResult(current, options.rejectOnFailure === true)
|
|
535
534
|
}
|
|
536
535
|
if (paused) return Promise.reject(new Error('Plugin jobs runtime is paused'))
|
|
@@ -567,7 +566,7 @@ function createPluginJobs(
|
|
|
567
566
|
}
|
|
568
567
|
|
|
569
568
|
function settleJobWaiters(job: GeneratedAnalysisJob): void {
|
|
570
|
-
if (
|
|
569
|
+
if (isActiveJobStatus(job.status)) return
|
|
571
570
|
const waiting = jobWaiters.get(job.job_id)
|
|
572
571
|
if (!waiting) return
|
|
573
572
|
jobWaiters.delete(job.job_id)
|
|
@@ -915,7 +914,7 @@ function createPluginJobs(
|
|
|
915
914
|
await refreshJobs()
|
|
916
915
|
} catch (cause) {
|
|
917
916
|
error.value = errorMessage(cause, 'Failed to refresh analysis jobs')
|
|
918
|
-
if (jobs.value.some(job =>
|
|
917
|
+
if (jobs.value.some(job => isActiveJobStatus(job.status))) schedulePoll()
|
|
919
918
|
}
|
|
920
919
|
}, pollInterval)
|
|
921
920
|
}
|
|
@@ -927,7 +926,7 @@ function createPluginJobs(
|
|
|
927
926
|
}
|
|
928
927
|
|
|
929
928
|
function schedulePollForActiveJobs(): void {
|
|
930
|
-
if (jobs.value.some(job =>
|
|
929
|
+
if (jobs.value.some(job => isActiveJobStatus(job.status))) schedulePoll()
|
|
931
930
|
}
|
|
932
931
|
|
|
933
932
|
function applyJobStreamMessage(message: EventStreamMessage<PluginJobStreamData>): void {
|
|
@@ -1005,7 +1004,7 @@ function createPluginJobs(
|
|
|
1005
1004
|
scheduleManagedDownloadRefresh(jobId, managedDownloadRefreshDelay(result))
|
|
1006
1005
|
}
|
|
1007
1006
|
if (streamEnabled) jobStream.start()
|
|
1008
|
-
else if (jobs.value.some(job =>
|
|
1007
|
+
else if (jobs.value.some(job => isActiveJobStatus(job.status))) schedulePoll()
|
|
1009
1008
|
}
|
|
1010
1009
|
|
|
1011
1010
|
function dispose(): void {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** Demonstrates `useListReorder`: drag a row, or focus a handle and press Alt with the arrow keys. */
|
|
3
|
+
import { ref } from 'vue'
|
|
4
|
+
import { useListReorder } from './useListReorder'
|
|
5
|
+
|
|
6
|
+
interface Step { id: string; name: string }
|
|
7
|
+
|
|
8
|
+
const steps = ref<Step[]>([
|
|
9
|
+
{ id: 's1', name: 'Thaw samples' },
|
|
10
|
+
{ id: 's2', name: 'Quench metabolism' },
|
|
11
|
+
{ id: 's3', name: 'Extract metabolites' },
|
|
12
|
+
{ id: 's4', name: 'Dry down' },
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
const handles = new Map<string, HTMLElement>()
|
|
16
|
+
|
|
17
|
+
function setHandle(id: string, el: Element | null) {
|
|
18
|
+
if (el instanceof HTMLElement) handles.set(id, el)
|
|
19
|
+
else handles.delete(id)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
draggingId,
|
|
24
|
+
dragOverId,
|
|
25
|
+
announcement,
|
|
26
|
+
announcerStyle,
|
|
27
|
+
onDragStart,
|
|
28
|
+
onDragOver,
|
|
29
|
+
onDragLeave,
|
|
30
|
+
onDrop,
|
|
31
|
+
onDragEnd,
|
|
32
|
+
onKeydown,
|
|
33
|
+
} = useListReorder<Step>({
|
|
34
|
+
items: () => steps.value,
|
|
35
|
+
getId: (step) => step.id,
|
|
36
|
+
getLabel: (step) => step.name,
|
|
37
|
+
getElement: (id) => handles.get(id),
|
|
38
|
+
itemNoun: 'Step',
|
|
39
|
+
onReorder: ({ items }) => {
|
|
40
|
+
steps.value = items
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
function initState() {
|
|
45
|
+
return { showAnnouncement: true }
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<Story title="Composables/useListReorder">
|
|
51
|
+
<Variant title="Playground" :init-state="initState">
|
|
52
|
+
<template #default="{ state }">
|
|
53
|
+
<div style="padding: 2rem; max-width: 600px;">
|
|
54
|
+
<p class="mint-story-hint">
|
|
55
|
+
Drag a row, or focus its handle and press Alt with the up/down arrow keys.
|
|
56
|
+
</p>
|
|
57
|
+
<ul class="mint-story-list">
|
|
58
|
+
<li
|
|
59
|
+
v-for="(step, index) in steps"
|
|
60
|
+
:key="step.id"
|
|
61
|
+
class="mint-story-row"
|
|
62
|
+
:class="{
|
|
63
|
+
'mint-story-row--dragging': draggingId === step.id,
|
|
64
|
+
'mint-story-row--over': dragOverId === step.id,
|
|
65
|
+
}"
|
|
66
|
+
draggable="true"
|
|
67
|
+
@dragstart="onDragStart($event, step, index)"
|
|
68
|
+
@dragover="onDragOver($event, step, index)"
|
|
69
|
+
@dragleave="onDragLeave"
|
|
70
|
+
@drop="onDrop($event, step, index)"
|
|
71
|
+
@dragend="onDragEnd"
|
|
72
|
+
>
|
|
73
|
+
<button
|
|
74
|
+
:ref="(el) => setHandle(step.id, el as Element | null)"
|
|
75
|
+
type="button"
|
|
76
|
+
class="mint-story-handle"
|
|
77
|
+
:aria-label="`Reorder ${step.name}. Press Alt with the arrow keys to move it.`"
|
|
78
|
+
@keydown="onKeydown($event, step, index)"
|
|
79
|
+
>
|
|
80
|
+
⠿
|
|
81
|
+
</button>
|
|
82
|
+
<span>{{ index + 1 }}. {{ step.name }}</span>
|
|
83
|
+
</li>
|
|
84
|
+
</ul>
|
|
85
|
+
|
|
86
|
+
<p v-if="state.showAnnouncement" class="mint-story-hint">
|
|
87
|
+
Live region: <em>{{ announcement || '—' }}</em>
|
|
88
|
+
</p>
|
|
89
|
+
<div role="status" aria-live="polite" :style="announcerStyle">{{ announcement }}</div>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
92
|
+
|
|
93
|
+
<template #controls="{ state }">
|
|
94
|
+
<HstCheckbox v-model="state.showAnnouncement" title="Show live-region text" />
|
|
95
|
+
</template>
|
|
96
|
+
</Variant>
|
|
97
|
+
</Story>
|
|
98
|
+
</template>
|
|
99
|
+
|
|
100
|
+
<style scoped>
|
|
101
|
+
.mint-story-list {
|
|
102
|
+
list-style: none;
|
|
103
|
+
margin: 0;
|
|
104
|
+
padding: 0;
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
gap: 0.25rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.mint-story-row {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 0.5rem;
|
|
114
|
+
padding: 0.5rem 0.75rem;
|
|
115
|
+
border: 1px solid var(--border-color);
|
|
116
|
+
border-radius: 0.5rem;
|
|
117
|
+
background: var(--bg-primary);
|
|
118
|
+
color: var(--text-primary);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.mint-story-row--dragging {
|
|
122
|
+
opacity: 0.5;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.mint-story-row--over {
|
|
126
|
+
border-color: var(--color-primary);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.mint-story-handle {
|
|
130
|
+
padding: 0;
|
|
131
|
+
border: 0;
|
|
132
|
+
background: none;
|
|
133
|
+
color: var(--text-muted);
|
|
134
|
+
cursor: grab;
|
|
135
|
+
font-size: 1rem;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.mint-story-hint {
|
|
139
|
+
margin: 0 0 0.75rem;
|
|
140
|
+
color: var(--text-secondary);
|
|
141
|
+
font-size: 0.8125rem;
|
|
142
|
+
}
|
|
143
|
+
</style>
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { nextTick, ref, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
/** Inline style for a visually hidden `aria-live` region (no global utility class in the SDK). */
|
|
4
|
+
export const REORDER_ANNOUNCER_STYLE = Object.freeze({
|
|
5
|
+
position: 'absolute',
|
|
6
|
+
width: '1px',
|
|
7
|
+
height: '1px',
|
|
8
|
+
padding: '0',
|
|
9
|
+
margin: '-1px',
|
|
10
|
+
overflow: 'hidden',
|
|
11
|
+
clip: 'rect(0, 0, 0, 0)',
|
|
12
|
+
whiteSpace: 'nowrap',
|
|
13
|
+
border: '0',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export interface UseReorderAnnouncerReturn {
|
|
17
|
+
/** Bind to a polite live region's text content. */
|
|
18
|
+
announcement: Ref<string>
|
|
19
|
+
/** Publishes a message to the live region. */
|
|
20
|
+
announce: (message: string) => void
|
|
21
|
+
/** Bind to the live region's `style` so it stays visually hidden. */
|
|
22
|
+
announcerStyle: typeof REORDER_ANNOUNCER_STYLE
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Screen-reader announcements for drag/keyboard move interactions. Bind
|
|
27
|
+
* `announcement` to a `role="status" aria-live="polite"` element styled with
|
|
28
|
+
* `announcerStyle`.
|
|
29
|
+
*/
|
|
30
|
+
export function useReorderAnnouncer(): UseReorderAnnouncerReturn {
|
|
31
|
+
const announcement = ref('')
|
|
32
|
+
|
|
33
|
+
function announce(message: string) {
|
|
34
|
+
// Re-announce identical consecutive messages by clearing first.
|
|
35
|
+
if (announcement.value === message) announcement.value = ''
|
|
36
|
+
void nextTick(() => {
|
|
37
|
+
announcement.value = message
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { announcement, announce, announcerStyle: REORDER_ANNOUNCER_STYLE }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ListReorderMove<T> {
|
|
45
|
+
/** Index the item moved from, within `items()`. */
|
|
46
|
+
fromIndex: number
|
|
47
|
+
/** Index the item moved to, within `items()`. */
|
|
48
|
+
toIndex: number
|
|
49
|
+
/** The reordered array (a copy — `items()` is never mutated). */
|
|
50
|
+
items: T[]
|
|
51
|
+
/** Whether the move came from the keyboard or a pointer drag. */
|
|
52
|
+
source: 'keyboard' | 'drag'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface UseListReorderOptions<T> {
|
|
56
|
+
/** The list being reordered, in its canonical (not merely rendered) order. */
|
|
57
|
+
items: () => T[]
|
|
58
|
+
/** Stable id for an item; falls back to the render index when ids repeat. */
|
|
59
|
+
getId: (item: T, index: number) => string
|
|
60
|
+
/** Applies the move. The composable never mutates the source array. */
|
|
61
|
+
onReorder: (move: ListReorderMove<T>) => void
|
|
62
|
+
/** Human-readable name used in announcements. Defaults to the item's id. */
|
|
63
|
+
getLabel?: (item: T, index: number) => string
|
|
64
|
+
/** Blocks every reorder while it returns `true` (readonly / non-sortable lists). */
|
|
65
|
+
disabled?: () => boolean
|
|
66
|
+
/** Resolves the focusable element for an id so keyboard moves keep focus on the item. */
|
|
67
|
+
getElement?: (id: string) => HTMLElement | null | undefined
|
|
68
|
+
/** Noun used in announcements, e.g. `'Reagent'`. Default `'Item'`. */
|
|
69
|
+
itemNoun?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface UseListReorderReturn<T> {
|
|
73
|
+
/** Id of the item currently being dragged, or `null`. */
|
|
74
|
+
draggingId: Ref<string | null>
|
|
75
|
+
/** Id of the item the pointer is hovering as a drop target, or `null`. */
|
|
76
|
+
dragOverId: Ref<string | null>
|
|
77
|
+
isDragging: (id: string) => boolean
|
|
78
|
+
isDragOver: (id: string) => boolean
|
|
79
|
+
announcement: Ref<string>
|
|
80
|
+
announcerStyle: typeof REORDER_ANNOUNCER_STYLE
|
|
81
|
+
announce: (message: string) => void
|
|
82
|
+
/** Bind to the draggable element's `@dragstart`. */
|
|
83
|
+
onDragStart: (event: DragEvent, item: T, index: number) => void
|
|
84
|
+
/** Bind to `@dragover` — calls `preventDefault()` so the drop is accepted. */
|
|
85
|
+
onDragOver: (event: DragEvent, item: T, index: number) => void
|
|
86
|
+
/** Bind to `@dragleave`. */
|
|
87
|
+
onDragLeave: () => void
|
|
88
|
+
/** Bind to `@drop`. */
|
|
89
|
+
onDrop: (event: DragEvent, item: T, index: number) => void
|
|
90
|
+
/** Bind to `@dragend`. */
|
|
91
|
+
onDragEnd: () => void
|
|
92
|
+
/**
|
|
93
|
+
* Bind to the item's (or its drag handle's) `@keydown`: Alt+ArrowUp / Alt+ArrowLeft
|
|
94
|
+
* moves the item one position earlier, Alt+ArrowDown / Alt+ArrowRight one later.
|
|
95
|
+
*/
|
|
96
|
+
onKeydown: (event: KeyboardEvent, item: T, index: number) => void
|
|
97
|
+
/** Moves an item between two indices of `items()` programmatically. */
|
|
98
|
+
move: (fromIndex: number, toIndex: number, source?: 'keyboard' | 'drag') => void
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Pure helper: a copy of `items` with the entry at `from` moved to `to`. */
|
|
102
|
+
export function reorderList<T>(items: T[], from: number, to: number): T[] {
|
|
103
|
+
const next = [...items]
|
|
104
|
+
const [moved] = next.splice(from, 1)
|
|
105
|
+
next.splice(to, 0, moved)
|
|
106
|
+
return next
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Keyboard-accessible list reordering shared by the SDK's drag-to-reorder lists.
|
|
111
|
+
* It owns the HTML5 drag state, the drop-index maths, and an Alt+Arrow keyboard
|
|
112
|
+
* alternative that announces every move through a polite live region — so a
|
|
113
|
+
* reorder never depends on a pointer.
|
|
114
|
+
*/
|
|
115
|
+
export function useListReorder<T>(options: UseListReorderOptions<T>): UseListReorderReturn<T> {
|
|
116
|
+
const draggingId = ref<string | null>(null)
|
|
117
|
+
const dragOverId = ref<string | null>(null)
|
|
118
|
+
const { announcement, announce, announcerStyle } = useReorderAnnouncer()
|
|
119
|
+
const noun = options.itemNoun ?? 'Item'
|
|
120
|
+
|
|
121
|
+
function isDisabled(): boolean {
|
|
122
|
+
return options.disabled?.() === true
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function idFor(item: T, index: number): string {
|
|
126
|
+
return options.getId(item, index)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Index within `items()`, resolved by id so filtered/sorted renders still map back. */
|
|
130
|
+
function indexOf(item: T, renderIndex: number): number {
|
|
131
|
+
const id = idFor(item, renderIndex)
|
|
132
|
+
const found = options.items().findIndex((candidate, index) => idFor(candidate, index) === id)
|
|
133
|
+
return found === -1 ? renderIndex : found
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function labelFor(index: number): string {
|
|
137
|
+
const item = options.items()[index]
|
|
138
|
+
if (item === undefined) return noun
|
|
139
|
+
return options.getLabel?.(item, index) ?? idFor(item, index)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function clearDragState() {
|
|
143
|
+
draggingId.value = null
|
|
144
|
+
dragOverId.value = null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function move(fromIndex: number, toIndex: number, source: 'keyboard' | 'drag' = 'keyboard') {
|
|
148
|
+
if (isDisabled()) return
|
|
149
|
+
const items = options.items()
|
|
150
|
+
if (
|
|
151
|
+
fromIndex === toIndex ||
|
|
152
|
+
fromIndex < 0 ||
|
|
153
|
+
toIndex < 0 ||
|
|
154
|
+
fromIndex >= items.length ||
|
|
155
|
+
toIndex >= items.length
|
|
156
|
+
) return
|
|
157
|
+
|
|
158
|
+
const movedId = idFor(items[fromIndex], fromIndex)
|
|
159
|
+
const label = labelFor(fromIndex)
|
|
160
|
+
options.onReorder({
|
|
161
|
+
fromIndex,
|
|
162
|
+
toIndex,
|
|
163
|
+
items: reorderList(items, fromIndex, toIndex),
|
|
164
|
+
source,
|
|
165
|
+
})
|
|
166
|
+
announce(`${label} moved to position ${toIndex + 1} of ${items.length}.`)
|
|
167
|
+
void nextTick(() => options.getElement?.(movedId)?.focus())
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function onDragStart(event: DragEvent, item: T, index: number) {
|
|
171
|
+
if (isDisabled()) return
|
|
172
|
+
const id = idFor(item, index)
|
|
173
|
+
draggingId.value = id
|
|
174
|
+
if (event.dataTransfer) {
|
|
175
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
176
|
+
event.dataTransfer.setData('text/plain', id)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function onDragOver(event: DragEvent, item: T, index: number) {
|
|
181
|
+
if (isDisabled() || draggingId.value === null) return
|
|
182
|
+
event.preventDefault()
|
|
183
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'
|
|
184
|
+
const id = idFor(item, index)
|
|
185
|
+
dragOverId.value = id === draggingId.value ? null : id
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function onDragLeave() {
|
|
189
|
+
dragOverId.value = null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function onDrop(event: DragEvent, item: T, index: number) {
|
|
193
|
+
if (isDisabled() || draggingId.value === null) {
|
|
194
|
+
clearDragState()
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
event.preventDefault()
|
|
198
|
+
|
|
199
|
+
const items = options.items()
|
|
200
|
+
const fromIndex = items.findIndex((candidate, i) => idFor(candidate, i) === draggingId.value)
|
|
201
|
+
const toIndex = indexOf(item, index)
|
|
202
|
+
clearDragState()
|
|
203
|
+
if (fromIndex === -1) return
|
|
204
|
+
move(fromIndex, toIndex, 'drag')
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function onDragEnd() {
|
|
208
|
+
clearDragState()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function onKeydown(event: KeyboardEvent, item: T, index: number) {
|
|
212
|
+
if (isDisabled() || !event.altKey) return
|
|
213
|
+
const direction =
|
|
214
|
+
event.key === 'ArrowUp' || event.key === 'ArrowLeft'
|
|
215
|
+
? -1
|
|
216
|
+
: event.key === 'ArrowDown' || event.key === 'ArrowRight'
|
|
217
|
+
? 1
|
|
218
|
+
: 0
|
|
219
|
+
if (direction === 0) return
|
|
220
|
+
|
|
221
|
+
event.preventDefault()
|
|
222
|
+
const fromIndex = indexOf(item, index)
|
|
223
|
+
move(fromIndex, fromIndex + direction, 'keyboard')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
draggingId,
|
|
228
|
+
dragOverId,
|
|
229
|
+
isDragging: (id: string) => draggingId.value === id,
|
|
230
|
+
isDragOver: (id: string) => dragOverId.value === id,
|
|
231
|
+
announcement,
|
|
232
|
+
announcerStyle,
|
|
233
|
+
announce,
|
|
234
|
+
onDragStart,
|
|
235
|
+
onDragOver,
|
|
236
|
+
onDragLeave,
|
|
237
|
+
onDrop,
|
|
238
|
+
onDragEnd,
|
|
239
|
+
onKeydown,
|
|
240
|
+
move,
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -27,8 +27,8 @@ export interface UseMenuKeyboardReturn {
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Shared keyboard contract for menu- and listbox-style disclosure panels (the
|
|
30
|
-
* WAI-ARIA menu button pattern). Used by AppAvatarMenu and DropdownButton
|
|
31
|
-
*
|
|
30
|
+
* WAI-ARIA menu button pattern). Used by ActionMenu, AppAvatarMenu and DropdownButton,
|
|
31
|
+
* each pairing it with `useDropdownState` for the disclosure state itself.
|
|
32
32
|
*/
|
|
33
33
|
export function useMenuKeyboard(options: UseMenuKeyboardOptions): UseMenuKeyboardReturn {
|
|
34
34
|
function focusItem(position: 'first' | 'last') {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computed, ref, shallowRef, type ComputedRef, type Ref, type ShallowRef } from 'vue'
|
|
2
|
+
import { readErrorMessage } from '../utils/errors'
|
|
2
3
|
|
|
3
4
|
export interface OptimisticMutationContext<TVariables, TSnapshot> {
|
|
4
5
|
variables: TVariables
|
|
@@ -94,18 +95,3 @@ export function useOptimisticMutation<TVariables, TData, TSnapshot = unknown>(
|
|
|
94
95
|
reset,
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
|
-
|
|
98
|
-
function readErrorMessage(value: unknown): string {
|
|
99
|
-
if (value instanceof Error && value.message) return value.message
|
|
100
|
-
if (typeof value === 'string' && value.trim()) return value
|
|
101
|
-
if (
|
|
102
|
-
typeof value === 'object'
|
|
103
|
-
&& value !== null
|
|
104
|
-
&& 'message' in value
|
|
105
|
-
&& typeof (value as { message?: unknown }).message === 'string'
|
|
106
|
-
&& (value as { message: string }).message.trim()
|
|
107
|
-
) {
|
|
108
|
-
return (value as { message: string }).message
|
|
109
|
-
}
|
|
110
|
-
return 'Request failed.'
|
|
111
|
-
}
|
|
@@ -75,7 +75,9 @@ export interface UsePluginConfigReturn {
|
|
|
75
75
|
reset: () => void
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* @deprecated Unused by the MINT platform and scheduled for removal in MINT 1.3; use `usePluginSettings()` for typed plugin settings.
|
|
80
|
+
*/
|
|
79
81
|
export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
|
|
80
82
|
const injectedContext = getInjectedPlatformContext()
|
|
81
83
|
const { plugin } = usePlatformContext()
|