@morscherlab/mint-sdk 1.2.0-beta.4 → 1.2.0-beta.7
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/dist/{EmptyState-p90quq8Z.js → EmptyState-NBqf2_8z.js} +2 -2
- package/dist/{EmptyState-p90quq8Z.js.map → EmptyState-NBqf2_8z.js.map} +1 -1
- package/dist/{ExperimentPopover-tLwJ7XF4.js → ExperimentPopover-CfOdh5Rm.js} +3 -3
- package/dist/{ExperimentPopover-tLwJ7XF4.js.map → ExperimentPopover-CfOdh5Rm.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-B6P2msT4.js → ExperimentSelectorModal-DvIagrxY.js} +4 -4
- package/dist/{ExperimentSelectorModal-B6P2msT4.js.map → ExperimentSelectorModal-DvIagrxY.js.map} +1 -1
- package/dist/{SettingsModal-CkNM8qAI.js → SettingsModal-W3TYfnP9.js} +4 -7
- package/dist/SettingsModal-W3TYfnP9.js.map +1 -0
- package/dist/__tests__/components/PluginWorkspaceView.internal.test.d.ts +1 -0
- package/dist/__tests__/components/SecretInput.test.d.ts +1 -0
- package/dist/__tests__/components/WellEditPopupInternal.test.d.ts +1 -0
- package/dist/components/AppAvatarMenu.vue.d.ts +4 -0
- package/dist/components/AppSidebar.vue.d.ts +1 -1
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
- package/dist/components/DropdownButton.vue.d.ts +2 -0
- package/dist/components/FitPanel.vue.d.ts +1 -1
- package/dist/components/PluginWorkspaceView.props.d.ts +29 -1
- package/dist/components/PluginWorkspaceView.vue.d.ts +5 -5
- package/dist/components/SecretInput.vue.d.ts +28 -0
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -1
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.js +8 -8
- package/dist/components/internal/WellEditPopupInternal.vue.d.ts +3 -1
- package/dist/components/internal/WorkspaceControlsInternal.vue.d.ts +23 -0
- package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +70 -0
- package/dist/components/internal/WorkspaceSidebarInternal.vue.d.ts +43 -0
- package/dist/{components-MGNL6tRn.js → components-B80gV0P6.js} +575 -561
- package/dist/components-B80gV0P6.js.map +1 -0
- package/dist/composables/flattenWorkspaceBindings.d.ts +61 -0
- package/dist/composables/index.d.ts +5 -0
- package/dist/composables/index.js +9 -9
- package/dist/composables/pluginWorkspaceModel.d.ts +164 -0
- package/dist/composables/useAsync.d.ts +5 -1
- package/dist/composables/useBioTemplateWorkspace.d.ts +4 -20
- package/dist/composables/useCommandHistory.d.ts +5 -1
- package/dist/composables/useFocusTrap.d.ts +21 -0
- package/dist/composables/useMenuKeyboard.d.ts +29 -0
- package/dist/composables/useOptimisticMutation.d.ts +5 -1
- package/dist/composables/usePluginWorkspace.d.ts +51 -0
- package/dist/composables/useResourceCrud.d.ts +5 -1
- package/dist/composables/useRovingFocus.d.ts +24 -0
- package/dist/composables/useWellPainting.d.ts +5 -1
- package/dist/composables/useWellPlateAdapter.d.ts +5 -1
- package/dist/composables/useWellPlateValidation.d.ts +5 -1
- package/dist/composables/workspaceRecipes.d.ts +188 -0
- package/dist/{composables-CKxOcvgh.js → composables-C8mmD5ta.js} +65 -17
- package/dist/composables-C8mmD5ta.js.map +1 -0
- package/dist/{experiment-utils-CLnzPPq5.js → experiment-utils-DV3DrvE_.js} +70 -51
- package/dist/experiment-utils-DV3DrvE_.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +13 -13
- package/dist/install.js +7 -7
- package/dist/styles.css +100 -49
- package/dist/templates/builderReadUtils.d.ts +1 -1
- package/dist/templates/index.js +4 -4
- package/dist/{templates-BTJ9C9Kb.js → templates-CK72aKO1.js} +14 -23
- package/dist/templates-CK72aKO1.js.map +1 -0
- package/dist/types/form-builder.d.ts +1 -1
- package/dist/{useControlSchema-CXhxTul7.js → useControlSchema-_OVhavKq.js} +280 -63
- package/dist/useControlSchema-_OVhavKq.js.map +1 -0
- package/dist/{useDropdownState-DSb3BC3N.js → useDropdownState-D5G4UvWd.js} +2 -2
- package/dist/{useDropdownState-DSb3BC3N.js.map → useDropdownState-D5G4UvWd.js.map} +1 -1
- package/dist/{useExperimentSelector-D0pjj9WH.js → useExperimentSelector-DSEckf_g.js} +3 -3
- package/dist/{useExperimentSelector-D0pjj9WH.js.map → useExperimentSelector-DSEckf_g.js.map} +1 -1
- package/dist/{useFormBuilder-CuOFB3aP.js → useFormBuilder-Bd714C7y.js} +2 -2
- package/dist/{useFormBuilder-CuOFB3aP.js.map → useFormBuilder-Bd714C7y.js.map} +1 -1
- package/dist/{useProtocolTemplates-BWhQIWV5.js → useProtocolTemplates-Dq5Pzwyd.js} +481 -46
- package/dist/useProtocolTemplates-Dq5Pzwyd.js.map +1 -0
- package/dist/utils/pluginSecrets.d.ts +53 -0
- package/dist/utils/records.d.ts +4 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppAvatarMenu.test.ts +123 -0
- package/src/__tests__/components/BaseTabs.test.ts +67 -0
- package/src/__tests__/components/DropdownButton.test.ts +134 -0
- package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +230 -0
- package/src/__tests__/components/SecretInput.test.ts +87 -0
- package/src/__tests__/components/WellEditPopupInternal.test.ts +108 -0
- package/src/__tests__/composables/usePluginClient.test.ts +140 -0
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +22 -1
- package/src/components/AppAvatarMenu.vue +34 -3
- package/src/components/BaseModal.vue +8 -56
- package/src/components/BaseTabs.vue +26 -1
- package/src/components/BioTemplateExperimentWorkspaceView.vue +10 -21
- package/src/components/BioTemplatePackWorkspaceView.vue +11 -15
- package/src/components/BioTemplatePresetWorkspaceView.vue +8 -13
- package/src/components/ComponentBindingRenderer.vue +1 -3
- package/src/components/ControlWorkspaceView.vue +11 -22
- package/src/components/DoseDesignWorkspaceView.vue +6 -25
- package/src/components/DropdownButton.vue +32 -2
- package/src/components/PluginWorkspaceView.props.ts +38 -0
- package/src/components/PluginWorkspaceView.vue +53 -224
- package/src/components/SecretInput.story.vue +79 -0
- package/src/components/SecretInput.vue +150 -0
- package/src/components/index.ts +9 -0
- package/src/components/internal/WellEditPopupInternal.vue +20 -1
- package/src/components/internal/WorkspaceControlsInternal.vue +20 -0
- package/src/components/internal/WorkspaceShellInternal.vue +125 -0
- package/src/components/internal/WorkspaceSidebarInternal.vue +82 -0
- package/src/composables/experimentDesignData.ts +1 -3
- package/src/composables/flattenWorkspaceBindings.ts +104 -0
- package/src/composables/formBuilderRegistry.ts +5 -0
- package/src/composables/index.ts +26 -0
- package/src/composables/pluginWorkspaceModel.ts +182 -0
- package/src/composables/useAsync.ts +5 -1
- package/src/composables/useBioTemplatePackWorkspace.ts +15 -9
- package/src/composables/useBioTemplatePresetWorkspace.ts +14 -15
- package/src/composables/useBioTemplateWorkspace.ts +18 -43
- package/src/composables/useCommandHistory.ts +5 -1
- package/src/composables/useFocusTrap.ts +91 -0
- package/src/composables/useMenuKeyboard.ts +90 -0
- package/src/composables/useOptimisticMutation.ts +5 -1
- package/src/composables/usePluginClient.ts +42 -4
- package/src/composables/usePluginWorkspace.ts +208 -0
- package/src/composables/useResourceCrud.ts +5 -1
- package/src/composables/useRovingFocus.ts +66 -0
- package/src/composables/useWellPainting.ts +5 -1
- package/src/composables/useWellPlateAdapter.ts +5 -1
- package/src/composables/useWellPlateValidation.ts +5 -1
- package/src/composables/workspaceRecipes.ts +267 -0
- package/src/index.ts +16 -0
- package/src/styles/components/secret-input.css +60 -0
- package/src/styles/index.css +1 -0
- package/src/templates/builderReadUtils.ts +1 -3
- package/src/templates/componentTargetResolvers.ts +1 -3
- package/src/templates/controlSchemaTargets.ts +1 -3
- package/src/types/form-builder.ts +1 -0
- package/src/utils/formModelSync.ts +1 -3
- package/src/utils/pluginIcon.ts +1 -3
- package/src/utils/pluginSecrets.ts +102 -0
- package/src/utils/records.ts +9 -0
- package/dist/SettingsModal-CkNM8qAI.js.map +0 -1
- package/dist/components-MGNL6tRn.js.map +0 -1
- package/dist/composables-CKxOcvgh.js.map +0 -1
- package/dist/experiment-utils-CLnzPPq5.js.map +0 -1
- package/dist/templates-BTJ9C9Kb.js.map +0 -1
- package/dist/useControlSchema-CXhxTul7.js.map +0 -1
- package/dist/useProtocolTemplates-BWhQIWV5.js.map +0 -1
|
@@ -34,6 +34,9 @@ import {
|
|
|
34
34
|
type BioTemplateRendererBinding,
|
|
35
35
|
type UseBioTemplateWorkspaceReturn,
|
|
36
36
|
} from './useBioTemplateWorkspace'
|
|
37
|
+
import {
|
|
38
|
+
liftWorkspaceComponentBindings,
|
|
39
|
+
} from './flattenWorkspaceBindings'
|
|
37
40
|
import type {
|
|
38
41
|
BioTemplateComponentBinding,
|
|
39
42
|
BioTemplateComponentBindingsById,
|
|
@@ -138,14 +141,17 @@ export function useBioTemplatePresetWorkspace(
|
|
|
138
141
|
const collection = templateCollection.templateCollection
|
|
139
142
|
const workspace = computed(() => useBioTemplateWorkspace(collection.value, resolvedControlOptions))
|
|
140
143
|
const renderer = computed<BioTemplateRendererBinding>(() => ({ target: collection.value }))
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
const {
|
|
145
|
+
componentBindings,
|
|
146
|
+
componentBindingsById,
|
|
147
|
+
componentImports,
|
|
148
|
+
componentProps,
|
|
149
|
+
componentPropsById,
|
|
150
|
+
componentPropsByComponent,
|
|
151
|
+
componentSnippets,
|
|
152
|
+
componentUsage,
|
|
153
|
+
getComponentProps,
|
|
154
|
+
} = liftWorkspaceComponentBindings(workspace)
|
|
149
155
|
const form = reactive({
|
|
150
156
|
...controls.form,
|
|
151
157
|
modelValue: controlValues.value,
|
|
@@ -179,13 +185,6 @@ export function useBioTemplatePresetWorkspace(
|
|
|
179
185
|
...topBarSettings,
|
|
180
186
|
}))
|
|
181
187
|
|
|
182
|
-
function getComponentProps(
|
|
183
|
-
component: string,
|
|
184
|
-
options?: Parameters<UseBioTemplateWorkspaceReturn['getComponentProps']>[1],
|
|
185
|
-
) {
|
|
186
|
-
return workspace.value.getComponentProps(component, options)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
188
|
const bindings: BioTemplatePresetWorkspaceBindings = {
|
|
190
189
|
renderer,
|
|
191
190
|
form,
|
|
@@ -21,16 +21,12 @@ import {
|
|
|
21
21
|
} from './useBioTemplateControls'
|
|
22
22
|
import {
|
|
23
23
|
useBioTemplateComponents,
|
|
24
|
-
type BioTemplateComponentBinding,
|
|
25
|
-
type BioTemplateComponentBindingsById,
|
|
26
|
-
type BioTemplateComponentImport,
|
|
27
|
-
type BioTemplateComponentPropsByComponent,
|
|
28
|
-
type BioTemplateComponentPropsBinding,
|
|
29
|
-
type BioTemplateComponentPropsById,
|
|
30
|
-
type BioTemplateComponentSnippet,
|
|
31
|
-
type BioTemplateComponentUsage,
|
|
32
24
|
type UseBioTemplateComponentsReturn,
|
|
33
25
|
} from './useBioTemplateComponents'
|
|
26
|
+
import {
|
|
27
|
+
flattenWorkspaceComponentBindings,
|
|
28
|
+
type BioTemplateWorkspaceComponentBindings,
|
|
29
|
+
} from './flattenWorkspaceBindings'
|
|
34
30
|
|
|
35
31
|
export type BioTemplateWorkspaceTarget = BioTemplateControlTarget
|
|
36
32
|
|
|
@@ -38,24 +34,16 @@ export interface BioTemplateRendererBinding {
|
|
|
38
34
|
target: BioTemplateEnvelope<unknown> | TemplateCollectionEnvelope
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
export interface BioTemplateWorkspaceBindings {
|
|
37
|
+
export interface BioTemplateWorkspaceBindings extends BioTemplateWorkspaceComponentBindings {
|
|
42
38
|
renderer: BioTemplateRendererBinding | null
|
|
43
39
|
form: ControlWorkspaceFormBinding
|
|
44
40
|
sidebar: ControlWorkspaceSidebarBinding
|
|
45
41
|
topBar: UseControlWorkspaceReturn<BioTemplateControlSchema>['bindings']['topBar']
|
|
46
42
|
topBarSettings: ControlWorkspaceTopBarSettingsBinding
|
|
47
43
|
pillNav: ControlWorkspacePillNavBinding
|
|
48
|
-
componentBindings: BioTemplateComponentBinding[]
|
|
49
|
-
componentBindingsById: BioTemplateComponentBindingsById
|
|
50
|
-
componentProps: BioTemplateComponentPropsBinding[]
|
|
51
|
-
componentPropsById: BioTemplateComponentPropsById
|
|
52
|
-
componentPropsByComponent: BioTemplateComponentPropsByComponent
|
|
53
|
-
componentSnippets: BioTemplateComponentSnippet[]
|
|
54
|
-
componentUsage: BioTemplateComponentUsage | null
|
|
55
|
-
getComponentProps: UseBioTemplateComponentsReturn['getComponentProps']
|
|
56
44
|
}
|
|
57
45
|
|
|
58
|
-
export interface UseBioTemplateWorkspaceReturn {
|
|
46
|
+
export interface UseBioTemplateWorkspaceReturn extends BioTemplateWorkspaceComponentBindings {
|
|
59
47
|
target: BioTemplateWorkspaceTarget
|
|
60
48
|
controls: UseControlSchemaReturn<BioTemplateControlSchema>
|
|
61
49
|
workspace: UseControlWorkspaceReturn<BioTemplateControlSchema>
|
|
@@ -70,15 +58,6 @@ export interface UseBioTemplateWorkspaceReturn {
|
|
|
70
58
|
topBarSettings: ControlWorkspaceTopBarSettingsBinding
|
|
71
59
|
bindings: BioTemplateWorkspaceBindings
|
|
72
60
|
renderer: BioTemplateRendererBinding | null
|
|
73
|
-
componentBindings: BioTemplateComponentBinding[]
|
|
74
|
-
componentBindingsById: BioTemplateComponentBindingsById
|
|
75
|
-
componentImports: BioTemplateComponentImport[]
|
|
76
|
-
componentProps: BioTemplateComponentPropsBinding[]
|
|
77
|
-
componentPropsById: BioTemplateComponentPropsById
|
|
78
|
-
componentPropsByComponent: BioTemplateComponentPropsByComponent
|
|
79
|
-
componentSnippets: BioTemplateComponentSnippet[]
|
|
80
|
-
componentUsage: BioTemplateComponentUsage | null
|
|
81
|
-
getComponentProps: UseBioTemplateComponentsReturn['getComponentProps']
|
|
82
61
|
}
|
|
83
62
|
|
|
84
63
|
/** One-stop wiring for BioTemplateRenderer, FormBuilder, AppSidebar, and template-aware SDK components. */
|
|
@@ -90,21 +69,25 @@ export function useBioTemplateWorkspace(
|
|
|
90
69
|
const workspace = useControlWorkspace(controls.controls, options)
|
|
91
70
|
const components = useBioTemplateComponents(target)
|
|
92
71
|
const renderer = isTemplateObjectTarget(target) ? { target } : null
|
|
93
|
-
const
|
|
94
|
-
renderer,
|
|
95
|
-
form: workspace.bindings.form,
|
|
96
|
-
sidebar: workspace.bindings.sidebar,
|
|
97
|
-
topBar: workspace.bindings.topBar,
|
|
98
|
-
topBarSettings: workspace.bindings.topBarSettings,
|
|
99
|
-
pillNav: workspace.bindings.pillNav,
|
|
72
|
+
const componentBindings = flattenWorkspaceComponentBindings({
|
|
100
73
|
componentBindings: components.bindings,
|
|
101
74
|
componentBindingsById: components.componentBindingsById,
|
|
75
|
+
componentImports: components.imports,
|
|
102
76
|
componentProps: components.componentProps,
|
|
103
77
|
componentPropsById: components.componentPropsById,
|
|
104
78
|
componentPropsByComponent: components.componentPropsByComponent,
|
|
105
79
|
componentSnippets: components.snippets,
|
|
106
80
|
componentUsage: components.usage,
|
|
107
81
|
getComponentProps: components.getComponentProps,
|
|
82
|
+
})
|
|
83
|
+
const bindings: BioTemplateWorkspaceBindings = {
|
|
84
|
+
renderer,
|
|
85
|
+
form: workspace.bindings.form,
|
|
86
|
+
sidebar: workspace.bindings.sidebar,
|
|
87
|
+
topBar: workspace.bindings.topBar,
|
|
88
|
+
topBarSettings: workspace.bindings.topBarSettings,
|
|
89
|
+
pillNav: workspace.bindings.pillNav,
|
|
90
|
+
...componentBindings,
|
|
108
91
|
}
|
|
109
92
|
|
|
110
93
|
return {
|
|
@@ -122,15 +105,7 @@ export function useBioTemplateWorkspace(
|
|
|
122
105
|
topBarSettings: workspace.topBarSettings,
|
|
123
106
|
bindings,
|
|
124
107
|
renderer,
|
|
125
|
-
componentBindings
|
|
126
|
-
componentBindingsById: components.componentBindingsById,
|
|
127
|
-
componentImports: components.imports,
|
|
128
|
-
componentProps: components.componentProps,
|
|
129
|
-
componentPropsById: components.componentPropsById,
|
|
130
|
-
componentPropsByComponent: components.componentPropsByComponent,
|
|
131
|
-
componentSnippets: components.snippets,
|
|
132
|
-
componentUsage: components.usage,
|
|
133
|
-
getComponentProps: components.getComponentProps,
|
|
108
|
+
...componentBindings,
|
|
134
109
|
}
|
|
135
110
|
}
|
|
136
111
|
|
|
@@ -31,7 +31,11 @@ export interface UseCommandHistoryReturn<TCommand extends CommandHistoryCommand
|
|
|
31
31
|
clear: () => void
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Generic command-stack state for plugin editors with undo/redo controls.
|
|
36
|
+
*
|
|
37
|
+
* @deprecated Unused by the MINT platform and scheduled for removal in MINT 1.3; keep command stacks plugin-local.
|
|
38
|
+
*/
|
|
35
39
|
export function useCommandHistory<TCommand extends CommandHistoryCommand = CommandHistoryCommand>(
|
|
36
40
|
options: UseCommandHistoryOptions = {},
|
|
37
41
|
): UseCommandHistoryReturn<TCommand> {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { nextTick, watch, type Ref } from 'vue'
|
|
2
|
+
import { useEventListener } from './useEventListener'
|
|
3
|
+
|
|
4
|
+
export interface UseFocusTrapOptions {
|
|
5
|
+
/** Ref to the trap's container element. Focusable elements are queried within it. */
|
|
6
|
+
containerRef: Ref<HTMLElement | null>
|
|
7
|
+
/** Reactive flag — the trap activates when this becomes `true` and releases when it becomes `false`. */
|
|
8
|
+
active: Ref<boolean>
|
|
9
|
+
/** Restore focus to the element that was focused before activation, when the trap releases. Default `true`. */
|
|
10
|
+
restoreFocus?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UseFocusTrapReturn {
|
|
14
|
+
/** Focusable descendants of the container, in DOM order, excluding roving-tabindex (`tabindex="-1"`) elements. */
|
|
15
|
+
getFocusableElements: () => HTMLElement[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const FOCUSABLE_SELECTOR = [
|
|
19
|
+
'a[href]',
|
|
20
|
+
'button:not([disabled])',
|
|
21
|
+
'input:not([disabled])',
|
|
22
|
+
'select:not([disabled])',
|
|
23
|
+
'textarea:not([disabled])',
|
|
24
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
25
|
+
].join(', ')
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Traps Tab focus within a container while `active` is true: Tab wraps from the last
|
|
29
|
+
* focusable element back to the first (and Shift+Tab from the first to the last), the
|
|
30
|
+
* first focusable element (or the container itself, if none) receives focus when the
|
|
31
|
+
* trap activates, and focus returns to whatever was focused beforehand when it releases.
|
|
32
|
+
* Used by BaseModal and WellEditPopupInternal.
|
|
33
|
+
*/
|
|
34
|
+
export function useFocusTrap(options: UseFocusTrapOptions): UseFocusTrapReturn {
|
|
35
|
+
const { containerRef, active } = options
|
|
36
|
+
const restoreFocus = options.restoreFocus ?? true
|
|
37
|
+
let previouslyFocused: HTMLElement | null = null
|
|
38
|
+
|
|
39
|
+
function getFocusableElements(): HTMLElement[] {
|
|
40
|
+
if (!containerRef.value) return []
|
|
41
|
+
// Roving tabindex (inactive tabs/rail items) stays out of the Tab cycle. Checking
|
|
42
|
+
// the attribute directly (rather than the `.tabIndex` IDL property) avoids relying
|
|
43
|
+
// on a browser/engine's default-tabindex computation for freshly-patched elements.
|
|
44
|
+
return Array.from(containerRef.value.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))
|
|
45
|
+
.filter((el) => el.getAttribute('tabindex') !== '-1')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
49
|
+
if (event.key !== 'Tab' || !active.value || !containerRef.value) return
|
|
50
|
+
|
|
51
|
+
const focusable = getFocusableElements()
|
|
52
|
+
if (focusable.length === 0) {
|
|
53
|
+
event.preventDefault()
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const first = focusable[0]
|
|
58
|
+
const last = focusable[focusable.length - 1]
|
|
59
|
+
|
|
60
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
61
|
+
event.preventDefault()
|
|
62
|
+
last.focus()
|
|
63
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
64
|
+
event.preventDefault()
|
|
65
|
+
first.focus()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// `flush: 'sync'` so releasing the trap from `onBeforeUnmount` restores focus
|
|
70
|
+
// before the component's reactive scope is torn down — a batched (default
|
|
71
|
+
// 'pre') watcher would be scheduled but never flushed once that scope stops.
|
|
72
|
+
watch(active, async (isActive) => {
|
|
73
|
+
if (isActive) {
|
|
74
|
+
previouslyFocused = document.activeElement as HTMLElement | null
|
|
75
|
+
await nextTick()
|
|
76
|
+
const focusable = getFocusableElements()
|
|
77
|
+
if (focusable.length > 0) {
|
|
78
|
+
focusable[0].focus()
|
|
79
|
+
} else {
|
|
80
|
+
containerRef.value?.focus()
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
if (restoreFocus) previouslyFocused?.focus()
|
|
84
|
+
previouslyFocused = null
|
|
85
|
+
}
|
|
86
|
+
}, { flush: 'sync' })
|
|
87
|
+
|
|
88
|
+
useEventListener(() => document, 'keydown', handleKeydown)
|
|
89
|
+
|
|
90
|
+
return { getFocusableElements }
|
|
91
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { nextTick } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface UseMenuKeyboardOptions {
|
|
4
|
+
/** Returns the currently navigable items inside the open panel, in DOM order. */
|
|
5
|
+
items: () => HTMLElement[]
|
|
6
|
+
/** True while the panel is open. */
|
|
7
|
+
isOpen: () => boolean
|
|
8
|
+
/** Opens the panel. */
|
|
9
|
+
open: () => void
|
|
10
|
+
/** Closes the panel. When `returnFocus` is true, focus should return to the trigger. */
|
|
11
|
+
close: (returnFocus?: boolean) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UseMenuKeyboardReturn {
|
|
15
|
+
/**
|
|
16
|
+
* Bind to the trigger's `@keydown`. ArrowDown/Enter/Space open the panel (when
|
|
17
|
+
* closed) and focus its first item; ArrowUp opens it focused on the last item.
|
|
18
|
+
*/
|
|
19
|
+
handleTriggerKeydown: (event: KeyboardEvent) => void
|
|
20
|
+
/**
|
|
21
|
+
* Bind to the open panel's `@keydown`. ArrowUp/ArrowDown cycle focus with
|
|
22
|
+
* wraparound, Home/End jump to the first/last item, Escape closes and returns
|
|
23
|
+
* focus to the trigger, and Tab closes without moving focus.
|
|
24
|
+
*/
|
|
25
|
+
handlePanelKeydown: (event: KeyboardEvent) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Shared keyboard contract for menu- and listbox-style disclosure panels (the
|
|
30
|
+
* WAI-ARIA menu button pattern). Used by AppAvatarMenu and DropdownButton; ActionMenu
|
|
31
|
+
* implements the same contract inline.
|
|
32
|
+
*/
|
|
33
|
+
export function useMenuKeyboard(options: UseMenuKeyboardOptions): UseMenuKeyboardReturn {
|
|
34
|
+
function focusItem(position: 'first' | 'last') {
|
|
35
|
+
void nextTick(() => {
|
|
36
|
+
const items = options.items()
|
|
37
|
+
const target = position === 'first' ? items[0] : items[items.length - 1]
|
|
38
|
+
target?.focus()
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleTriggerKeydown(event: KeyboardEvent) {
|
|
43
|
+
if (options.isOpen()) return
|
|
44
|
+
|
|
45
|
+
if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
|
|
46
|
+
event.preventDefault()
|
|
47
|
+
options.open()
|
|
48
|
+
focusItem('first')
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
if (event.key === 'ArrowUp') {
|
|
52
|
+
event.preventDefault()
|
|
53
|
+
options.open()
|
|
54
|
+
focusItem('last')
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function handlePanelKeydown(event: KeyboardEvent) {
|
|
59
|
+
const items = options.items()
|
|
60
|
+
const currentIndex = items.indexOf(document.activeElement as HTMLElement)
|
|
61
|
+
|
|
62
|
+
if (event.key === 'Escape') {
|
|
63
|
+
event.preventDefault()
|
|
64
|
+
options.close(true)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
if (event.key === 'Tab') {
|
|
68
|
+
options.close()
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
if (event.key === 'Home') {
|
|
72
|
+
event.preventDefault()
|
|
73
|
+
items[0]?.focus()
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
if (event.key === 'End') {
|
|
77
|
+
event.preventDefault()
|
|
78
|
+
items[items.length - 1]?.focus()
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return
|
|
82
|
+
|
|
83
|
+
event.preventDefault()
|
|
84
|
+
const direction = event.key === 'ArrowDown' ? 1 : -1
|
|
85
|
+
const nextIndex = (Math.max(currentIndex, 0) + direction + items.length) % items.length
|
|
86
|
+
items[nextIndex]?.focus()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { handleTriggerKeydown, handlePanelKeydown }
|
|
90
|
+
}
|
|
@@ -32,7 +32,11 @@ export interface UseOptimisticMutationReturn<TVariables, TData, TSnapshot = unkn
|
|
|
32
32
|
reset: () => void
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Run async mutations with optional optimistic local updates and rollback.
|
|
37
|
+
*
|
|
38
|
+
* @deprecated Unused by the MINT platform and scheduled for removal in MINT 1.3; use `useRequestSyncState()` instead.
|
|
39
|
+
*/
|
|
36
40
|
export function useOptimisticMutation<TVariables, TData, TSnapshot = unknown>(
|
|
37
41
|
options: UseOptimisticMutationOptions<TVariables, TData, TSnapshot>,
|
|
38
42
|
): UseOptimisticMutationReturn<TVariables, TData, TSnapshot> {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { usePlatformContext } from './usePlatformContext'
|
|
16
16
|
import { useRequestSyncState } from './useRequestSyncState'
|
|
17
17
|
import { useAuthStore } from '../stores/auth'
|
|
18
|
+
import { PLUGIN_SECRET_FORMAT_KEY } from '../utils/pluginSecrets'
|
|
18
19
|
import {
|
|
19
20
|
currentExperimentFromContext,
|
|
20
21
|
getInjectedPlatformContext,
|
|
@@ -545,6 +546,26 @@ export function usePluginClient<TClient>(client: TClient): TClient {
|
|
|
545
546
|
|
|
546
547
|
const PLATFORM_OWNED_SETTINGS_KEY = 'allowed_experiment_types'
|
|
547
548
|
|
|
549
|
+
/**
|
|
550
|
+
* Secret settings travel as `{ "$secret": "<id>" }` references, so an untouched
|
|
551
|
+
* secret round-trips without ever exposing its value. Declaring the format on
|
|
552
|
+
* every write keeps a `null` unambiguous: it clears the stored secret instead of
|
|
553
|
+
* meaning "unchanged", which is what the deprecated placeholder form meant.
|
|
554
|
+
*/
|
|
555
|
+
function withSecretFormat(
|
|
556
|
+
payload: Record<string, unknown>,
|
|
557
|
+
secretFormat: string | null,
|
|
558
|
+
): Record<string, unknown> {
|
|
559
|
+
if (!secretFormat) return payload
|
|
560
|
+
return { ...payload, [PLUGIN_SECRET_FORMAT_KEY]: secretFormat }
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function responseSecretFormat(response: unknown): string | null {
|
|
564
|
+
if (!response || typeof response !== 'object') return null
|
|
565
|
+
const format = (response as Record<string, unknown>)[PLUGIN_SECRET_FORMAT_KEY]
|
|
566
|
+
return typeof format === 'string' && format ? format : null
|
|
567
|
+
}
|
|
568
|
+
|
|
548
569
|
function cloneRecord(value: Record<string, unknown>): Record<string, unknown> {
|
|
549
570
|
return JSON.parse(JSON.stringify(value)) as Record<string, unknown>
|
|
550
571
|
}
|
|
@@ -618,6 +639,7 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
618
639
|
const values = ref({}) as Ref<PluginSettingsValues<TSettings>>
|
|
619
640
|
const savedValues = shallowRef<Record<string, unknown>>({})
|
|
620
641
|
const revision = ref<string | null>(null)
|
|
642
|
+
const secretFormat = ref<string | null>(null)
|
|
621
643
|
const request = useRequestSyncState('Plugin settings request failed.')
|
|
622
644
|
const isLoading = ref(false)
|
|
623
645
|
const isSaving = ref(false)
|
|
@@ -650,11 +672,16 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
650
672
|
values.value = cloneRecord(nextValues) as PluginSettingsValues<TSettings>
|
|
651
673
|
}
|
|
652
674
|
|
|
653
|
-
function applyLoaded(
|
|
675
|
+
function applyLoaded(
|
|
676
|
+
nextValues: Record<string, unknown>,
|
|
677
|
+
nextRevision: string | null,
|
|
678
|
+
nextSecretFormat: string | null = secretFormat.value,
|
|
679
|
+
): void {
|
|
654
680
|
const cloned = cloneRecord(nextValues)
|
|
655
681
|
values.value = cloned as PluginSettingsValues<TSettings>
|
|
656
682
|
savedValues.value = cloneRecord(cloned)
|
|
657
683
|
revision.value = nextRevision
|
|
684
|
+
secretFormat.value = nextSecretFormat
|
|
658
685
|
}
|
|
659
686
|
|
|
660
687
|
async function load(): Promise<void> {
|
|
@@ -674,6 +701,7 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
674
701
|
applyLoaded(
|
|
675
702
|
responseSettingsPayload(response),
|
|
676
703
|
responseSettingsRevision(response),
|
|
704
|
+
responseSecretFormat(response),
|
|
677
705
|
)
|
|
678
706
|
} catch {
|
|
679
707
|
// Error state is handled by request.run().
|
|
@@ -707,7 +735,12 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
707
735
|
revision?: string
|
|
708
736
|
}>(
|
|
709
737
|
`/plugins/${encodeURIComponent(resolvedPluginName.value)}/config`,
|
|
710
|
-
{
|
|
738
|
+
{
|
|
739
|
+
config: withSecretFormat(
|
|
740
|
+
topLevelSettingsPatch(payload, typedKeys),
|
|
741
|
+
secretFormat.value,
|
|
742
|
+
),
|
|
743
|
+
},
|
|
711
744
|
),
|
|
712
745
|
{ success: 'save', errorMessage: 'Failed to save plugin settings' },
|
|
713
746
|
)
|
|
@@ -730,7 +763,11 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
730
763
|
}
|
|
731
764
|
|
|
732
765
|
if (!response) return true
|
|
733
|
-
applyLoaded(
|
|
766
|
+
applyLoaded(
|
|
767
|
+
responseSettingsPayload(response),
|
|
768
|
+
nextRevision,
|
|
769
|
+
responseSecretFormat(response) ?? secretFormat.value,
|
|
770
|
+
)
|
|
734
771
|
return true
|
|
735
772
|
} else {
|
|
736
773
|
const ifMatch = revision.value
|
|
@@ -741,13 +778,14 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
741
778
|
settings: Record<string, unknown>
|
|
742
779
|
mode?: string
|
|
743
780
|
revision?: string
|
|
744
|
-
}>('/settings', payload, ifMatch),
|
|
781
|
+
}>('/settings', withSecretFormat(payload, secretFormat.value), ifMatch),
|
|
745
782
|
{ success: 'save', errorMessage: 'Failed to save plugin settings' },
|
|
746
783
|
)
|
|
747
784
|
}
|
|
748
785
|
applyLoaded(
|
|
749
786
|
responseSettingsPayload(response),
|
|
750
787
|
responseSettingsRevision(response) ?? revision.value,
|
|
788
|
+
responseSecretFormat(response) ?? secretFormat.value,
|
|
751
789
|
)
|
|
752
790
|
return true
|
|
753
791
|
} catch {
|