@morscherlab/mint-sdk 1.4.0-beta.7 → 1.4.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/dist/{ActionMenu-CScINahU.js → AlertBox-B2Bgr3ei.js} +40 -209
- package/dist/AlertBox-B2Bgr3ei.js.map +1 -0
- package/dist/{AppSidebar-BmiiPfz9.js → AppSidebar-Dq9W1jE2.js} +160 -109
- package/dist/AppSidebar-Dq9W1jE2.js.map +1 -0
- package/dist/{ConfirmDialog-CJ7hqYUP.js → ConfirmDialog-nbJj51is.js} +2 -2
- package/dist/{ConfirmDialog-CJ7hqYUP.js.map → ConfirmDialog-nbJj51is.js.map} +1 -1
- package/dist/ExperimentPopover-5T1fAq2L.js +337 -0
- package/dist/ExperimentPopover-5T1fAq2L.js.map +1 -0
- package/dist/{ExperimentSelectorModal-D-9z7cNx.js → ExperimentSelectorModal-65peG4Up.js} +198 -181
- package/dist/ExperimentSelectorModal-65peG4Up.js.map +1 -0
- package/dist/{SegmentedControl-DERBN-Xd.js → SegmentedControl-BtnnjWzs.js} +2 -2
- package/dist/{SegmentedControl-DERBN-Xd.js.map → SegmentedControl-BtnnjWzs.js.map} +1 -1
- package/dist/{SettingsModal-CSLE31yg.js → SettingsModal-483nyI6b.js} +9 -9
- package/dist/SettingsModal-483nyI6b.js.map +1 -0
- package/dist/components/AppSidebar.vue.d.ts +1 -1
- package/dist/components/ArtifactSaveDialog.vue.d.ts +9 -1
- package/dist/components/AuditTrail.vue.d.ts +1 -1
- package/dist/components/BaseSelect.vue.d.ts +2 -2
- package/dist/components/ChartContainer.vue.d.ts +2 -0
- package/dist/components/DatePicker.vue.d.ts +1 -1
- package/dist/components/ExperimentPopover.vue.d.ts +18 -0
- package/dist/components/ExperimentSelectorModal.vue.d.ts +0 -4
- package/dist/components/FilePicker.vue.d.ts +1 -1
- package/dist/components/FileUploader.vue.d.ts +2 -2
- package/dist/components/PluginIcon.vue.d.ts +2 -2
- package/dist/components/SampleSelector.vue.d.ts +6 -6
- package/dist/components/SequenceProgressBar.vue.d.ts +3 -0
- package/dist/components/StepWizard.vue.d.ts +3 -0
- package/dist/components/internal/AutoGroupModalInternal.vue.d.ts +1 -1
- package/dist/components/internal/DatePickerTimeInternal.vue.d.ts +1 -1
- package/dist/components/internal/ResourcePickerListInternal.vue.d.ts +2 -7
- package/dist/components/internal/SelectMultipleInternal.vue.d.ts +4 -0
- package/dist/composables/createClient.d.ts +2 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/pluginIdentity.d.ts +24 -0
- package/dist/composables/useAppExperiment.d.ts +8 -0
- package/dist/composables/useCalendarGrid.d.ts +1 -1
- package/dist/composables/useExperimentSelector.d.ts +6 -2
- package/dist/composables/usePlatform.d.ts +1 -0
- package/dist/{controlSchemaModel-CP5-_nAj.js → controlSchemaModel-B8CTXagP.js} +2 -2
- package/dist/{controlSchemaModel-CP5-_nAj.js.map → controlSchemaModel-B8CTXagP.js.map} +1 -1
- package/dist/{data-C2PwI_dn.js → data-CgILxDLW.js} +178 -124
- package/dist/data-CgILxDLW.js.map +1 -0
- package/dist/data.js +8 -8
- package/dist/{experimentStatus-BZmU4czV.js → experimentStatus-Cqp6qNrR.js} +3 -3
- package/dist/{experimentStatus-BZmU4czV.js.map → experimentStatus-Cqp6qNrR.js.map} +1 -1
- package/dist/index.js +19 -19
- package/dist/{install-DAID6c2-.js → install-BtLWTk6H.js} +15 -14
- package/dist/{install-DAID6c2-.js.map → install-BtLWTk6H.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-DLwRl_Ql.js → lab-MHzp-_J8.js} +128 -147
- package/dist/lab-MHzp-_J8.js.map +1 -0
- package/dist/lab.js +4 -3
- package/dist/{records-BzTcxekG.js → records-B6gpM_3e.js} +472 -274
- package/dist/records-B6gpM_3e.js.map +1 -0
- package/dist/{runtime-D6gVat6r.js → runtime-7vfl_bI3.js} +27 -31
- package/dist/runtime-7vfl_bI3.js.map +1 -0
- package/dist/runtime.js +6 -6
- package/dist/{shell-5ju97nOS.js → shell-Cs9OJMnm.js} +5 -5
- package/dist/{shell-5ju97nOS.js.map → shell-Cs9OJMnm.js.map} +1 -1
- package/dist/shell.js +5 -5
- package/dist/styles.css +1606 -1666
- package/dist/types/componentWorkflowTypes.d.ts +2 -0
- package/dist/types/generated/platformContext.d.ts +1 -0
- package/dist/types/generated-ui.d.ts +4 -0
- package/dist/{BaseModal-DfRIuVoj.js → useDropdownState-8yWMu-7_.js} +60 -2
- package/dist/useDropdownState-8yWMu-7_.js.map +1 -0
- package/dist/{useExperimentSamples-CxcnxZ8h.js → useExperimentSamples-DHnxKFAF.js} +3 -3
- package/dist/{useExperimentSamples-CxcnxZ8h.js.map → useExperimentSamples-DHnxKFAF.js.map} +1 -1
- package/dist/{useListReorder-CcbEYxX-.js → useListReorder-BitdUhxn.js} +109 -277
- package/dist/useListReorder-BitdUhxn.js.map +1 -0
- package/dist/{useRackEditor-BD5NIo5s.js → useRackEditor-DiwCt77M.js} +71 -57
- package/dist/useRackEditor-DiwCt77M.js.map +1 -0
- package/dist/{useRealtime-PalKue84.js → useRealtime-C3iy9QkU.js} +7 -4
- package/dist/useRealtime-C3iy9QkU.js.map +1 -0
- package/dist/useRequest-D506eHi8.js +244 -0
- package/dist/useRequest-D506eHi8.js.map +1 -0
- package/dist/utils/time.d.ts +7 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppTopBar.test.ts +51 -2
- package/src/__tests__/components/AppTopBarPillNav.test.ts +35 -0
- package/src/__tests__/components/ArtifactSaveDialog.test.ts +78 -0
- package/src/__tests__/components/BaseSelect.multiple.test.ts +87 -32
- package/src/__tests__/components/BaseSelect.test.ts +2 -1
- package/src/__tests__/components/BatchProgressList.test.ts +16 -0
- package/src/__tests__/components/CalendarGridPanel.test.ts +2 -2
- package/src/__tests__/components/ChartContainer.test.ts +11 -0
- package/src/__tests__/components/ChemicalFormula.test.ts +6 -3
- package/src/__tests__/components/DatePicker.datetime.test.ts +6 -0
- package/src/__tests__/components/DatePicker.test.ts +21 -0
- package/src/__tests__/components/DatePicker.time.test.ts +2 -2
- package/src/__tests__/components/EmptyState.test.ts +1 -1
- package/src/__tests__/components/ExperimentPopover.test.ts +125 -16
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +119 -13
- package/src/__tests__/components/GeneratedPluginView.test.ts +22 -1
- package/src/__tests__/components/InstrumentStateBadge.test.ts +4 -4
- package/src/__tests__/components/ResourcePickerListInternal.test.ts +0 -20
- package/src/__tests__/components/SampleSelector.test.ts +13 -8
- package/src/__tests__/components/SequenceProgressBar.test.ts +12 -0
- package/src/__tests__/components/StepWizard.test.ts +11 -0
- package/src/__tests__/components/WellPlate.decoration.test.ts +11 -4
- package/src/__tests__/components/deprecationTags.test.ts +1 -1
- package/src/__tests__/composables/useAppExperiment.test.ts +27 -1
- package/src/__tests__/composables/useCalendarGrid.test.ts +4 -2
- package/src/__tests__/composables/useExperimentSelector.test.ts +111 -0
- package/src/__tests__/styles/baseControlsRules.test.ts +6 -2
- package/src/__tests__/styles/controlRefreshRules.test.ts +0 -6
- package/src/__tests__/styles/cssTokens.test.ts +2 -2
- package/src/__tests__/styles/menusShellRefresh.test.ts +14 -10
- package/src/__tests__/utils/plotlyTemplate.test.ts +4 -4
- package/src/__tests__/utils/time.test.ts +18 -1
- package/src/components/AdductText.story.vue +7 -0
- package/src/components/AdductText.vue +1 -1
- package/src/components/AlertBox.story.vue +2 -2
- package/src/components/AlertBox.vue +18 -16
- package/src/components/AppSidebar.story.vue +0 -28
- package/src/components/AppTopBar.story.vue +47 -12
- package/src/components/AppTopBar.vue +118 -114
- package/src/components/ArtifactSaveDialog.story.vue +23 -0
- package/src/components/ArtifactSaveDialog.vue +102 -12
- package/src/components/AuditTrail.story.vue +2 -1
- package/src/components/AuditTrail.vue +2 -21
- package/src/components/Avatar.vue +10 -14
- package/src/components/BaseSelect.multiple.story.vue +13 -2
- package/src/components/BaseSelect.vue +4 -3
- package/src/components/BatchProgressList.vue +35 -25
- package/src/components/ChartContainer.story.vue +2 -1
- package/src/components/ChartContainer.vue +8 -6
- package/src/components/ConfirmDialog.story.vue +1 -1
- package/src/components/EmptyState.vue +1 -1
- package/src/components/ExperimentPopover.story.vue +165 -53
- package/src/components/ExperimentPopover.vue +169 -187
- package/src/components/ExperimentSelectorModal.story.vue +40 -36
- package/src/components/ExperimentSelectorModal.vue +124 -110
- package/src/components/FormField.story.vue +8 -48
- package/src/components/GeneratedPluginView.vue +7 -1
- package/src/components/InstrumentStateBadge.vue +1 -1
- package/src/components/JobsStatusTray.vue +1 -1
- package/src/components/LoadingSpinner.vue +1 -1
- package/src/components/NumberInput.vue +3 -6
- package/src/components/PluginIcon.story.vue +2 -10
- package/src/components/PluginIcon.vue +2 -2
- package/src/components/PluginWorkspaceView.story.vue +1 -1
- package/src/components/RuntimeUpdateNotice.vue +1 -1
- package/src/components/SampleSelector.story.vue +1 -1
- package/src/components/SampleSelector.vue +27 -37
- package/src/components/SampleSelectorGroupNode.vue +1 -1
- package/src/components/SectionCard.story.vue +2 -2
- package/src/components/SequenceProgressBar.story.vue +9 -0
- package/src/components/SequenceProgressBar.vue +6 -1
- package/src/components/SettingsModal.schema.ts +1 -1
- package/src/components/SettingsModal.vue +2 -2
- package/src/components/SmartGroupFieldRecipe.story.vue +1 -1
- package/src/components/SmartGroupManual.story.vue +1 -1
- package/src/components/StepWizard.story.vue +33 -0
- package/src/components/StepWizard.vue +8 -1
- package/src/components/WellPlate.story.vue +3 -1
- package/src/components/WellPlate.vue +7 -0
- package/src/components/internal/AppTopBarPillNavInternal.vue +33 -2
- package/src/components/internal/DatePickerDateInternal.vue +16 -40
- package/src/components/internal/DatePickerDateTimeInternal.vue +3 -13
- package/src/components/internal/DatePickerTimeInternal.vue +5 -8
- package/src/components/internal/ResourcePickerListInternal.vue +45 -85
- package/src/components/internal/SelectMultipleInternal.vue +182 -65
- package/src/components/internal/SelectSearchableInternal.vue +2 -3
- package/src/composables/createClient.ts +2 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/pluginIdentity.ts +31 -0
- package/src/composables/useAppExperiment.ts +20 -0
- package/src/composables/useCalendarGrid.ts +4 -3
- package/src/composables/useExperimentSelector.ts +77 -31
- package/src/styles/components/adduct-text.css +13 -5
- package/src/styles/components/alert-box.css +39 -58
- package/src/styles/components/app-layout.css +6 -5
- package/src/styles/components/app-page-selector.css +3 -1
- package/src/styles/components/app-pill-nav.css +11 -12
- package/src/styles/components/app-sidebar.css +9 -18
- package/src/styles/components/app-top-bar.css +57 -55
- package/src/styles/components/artifact-save-dialog.css +153 -2
- package/src/styles/components/audit-trail.css +16 -19
- package/src/styles/components/avatar.css +6 -4
- package/src/styles/components/batch-progress-list.css +38 -36
- package/src/styles/components/button.css +28 -59
- package/src/styles/components/calendar.css +12 -12
- package/src/styles/components/chart-container.css +34 -23
- package/src/styles/components/checkbox.css +2 -12
- package/src/styles/components/collapsible-card.css +27 -29
- package/src/styles/components/confirm-dialog.css +13 -14
- package/src/styles/components/dataframe.css +12 -4
- package/src/styles/components/date-picker.css +80 -92
- package/src/styles/components/datetime-picker.css +31 -43
- package/src/styles/components/divider.css +20 -20
- package/src/styles/components/dose-calculator.css +3 -3
- package/src/styles/components/empty-state.css +20 -17
- package/src/styles/components/experiment-code-badge.css +2 -0
- package/src/styles/components/experiment-popover.css +174 -329
- package/src/styles/components/experiment-selector-modal.css +75 -56
- package/src/styles/components/file-picker.css +3 -3
- package/src/styles/components/file-uploader.css +38 -49
- package/src/styles/components/form-builder.css +3 -3
- package/src/styles/components/formula-input.css +1 -1
- package/src/styles/components/icon-button.css +18 -24
- package/src/styles/components/input.css +5 -4
- package/src/styles/components/instrument-monitor.css +13 -15
- package/src/styles/components/jobs-status-tray.css +31 -25
- package/src/styles/components/modal.css +2 -4
- package/src/styles/components/molecule-input.css +2 -2
- package/src/styles/components/multi-select.css +65 -117
- package/src/styles/components/number-input.css +26 -19
- package/src/styles/components/pill.css +3 -2
- package/src/styles/components/plate-editor.css +0 -1
- package/src/styles/components/plugin-icon.css +26 -23
- package/src/styles/components/popover.css +2 -2
- package/src/styles/components/presence-avatars.css +1 -1
- package/src/styles/components/protocol-step-editor.css +1 -3
- package/src/styles/components/resource-card.css +2 -2
- package/src/styles/components/resource-picker-list.css +6 -57
- package/src/styles/components/sample-hierarchy-tree.css +4 -4
- package/src/styles/components/sample-selector.css +95 -79
- package/src/styles/components/schedule-calendar.css +8 -8
- package/src/styles/components/searchable-select.css +12 -12
- package/src/styles/components/section-card.css +28 -47
- package/src/styles/components/segmented-control.css +18 -15
- package/src/styles/components/select.css +3 -3
- package/src/styles/components/sequence-input.css +2 -2
- package/src/styles/components/sequence-progress-bar.css +17 -8
- package/src/styles/components/smart-group.css +18 -18
- package/src/styles/components/step-wizard.css +52 -16
- package/src/styles/components/tabs.css +2 -2
- package/src/styles/components/textarea.css +3 -3
- package/src/styles/components/time-picker.css +36 -21
- package/src/styles/components/toast.css +12 -10
- package/src/styles/components/toggle.css +1 -11
- package/src/styles/components/tooltip.css +10 -9
- package/src/styles/components/unit-input.css +8 -7
- package/src/styles/components/well-plate.css +9 -2
- package/src/styles/variables.css +106 -75
- package/src/types/componentWorkflowTypes.ts +2 -0
- package/src/types/generated/platformContext.ts +1 -0
- package/src/types/generated-ui.ts +4 -0
- package/src/utils/time.ts +21 -0
- package/dist/ActionMenu-CScINahU.js.map +0 -1
- package/dist/AppSidebar-BmiiPfz9.js.map +0 -1
- package/dist/BaseModal-DfRIuVoj.js.map +0 -1
- package/dist/ExperimentPopover-C4sGw-is.js +0 -359
- package/dist/ExperimentPopover-C4sGw-is.js.map +0 -1
- package/dist/ExperimentSelectorModal-D-9z7cNx.js.map +0 -1
- package/dist/SettingsModal-CSLE31yg.js.map +0 -1
- package/dist/data-C2PwI_dn.js.map +0 -1
- package/dist/lab-DLwRl_Ql.js.map +0 -1
- package/dist/records-BzTcxekG.js.map +0 -1
- package/dist/runtime-D6gVat6r.js.map +0 -1
- package/dist/useListReorder-CcbEYxX-.js.map +0 -1
- package/dist/useRackEditor-BD5NIo5s.js.map +0 -1
- package/dist/useRealtime-PalKue84.js.map +0 -1
- package/dist/useRequest-D5JDfsgZ.js +0 -76
- package/dist/useRequest-D5JDfsgZ.js.map +0 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts" generic="T">
|
|
2
2
|
/**
|
|
3
|
-
* Searchable
|
|
3
|
+
* Searchable single-select list shared by the experiment and
|
|
4
4
|
* artifact selector modals: two-line rows (title, then code + meta) with status and date
|
|
5
5
|
* in a right-hand column, a filled selected row, skeleton,
|
|
6
6
|
* error and empty states, and the combobox keyboard model (↑ ↓ Enter).
|
|
7
7
|
*/
|
|
8
|
-
import { computed, nextTick,
|
|
8
|
+
import { computed, nextTick, ref, useId, watch } from 'vue'
|
|
9
9
|
import BaseInput from '../BaseInput.vue'
|
|
10
10
|
import Skeleton from '../Skeleton.vue'
|
|
11
11
|
import EmptyState from '../EmptyState.vue'
|
|
@@ -26,8 +26,6 @@ export interface ResourcePickerRow {
|
|
|
26
26
|
const props = withDefaults(
|
|
27
27
|
defineProps<{
|
|
28
28
|
items: T[]
|
|
29
|
-
/** When set, rows render under collapsible group headers in this order. */
|
|
30
|
-
groups?: { label: string; items: T[] }[] | null
|
|
31
29
|
itemKey: (item: T) => string | number
|
|
32
30
|
describe: (item: T) => ResourcePickerRow
|
|
33
31
|
selectedKey?: string | number | null
|
|
@@ -39,9 +37,10 @@ const props = withDefaults(
|
|
|
39
37
|
error?: string | null
|
|
40
38
|
emptyTitle?: string
|
|
41
39
|
emptyDescription?: string
|
|
40
|
+
/** Focus the search field when the list mounts (the modal's focus trap honours `autofocus`). */
|
|
41
|
+
autofocusSearch?: boolean
|
|
42
42
|
}>(),
|
|
43
43
|
{
|
|
44
|
-
groups: null,
|
|
45
44
|
selectedKey: null,
|
|
46
45
|
placeholder: 'Search…',
|
|
47
46
|
loading: false,
|
|
@@ -57,50 +56,33 @@ const emit = defineEmits<{
|
|
|
57
56
|
defineSlots<{
|
|
58
57
|
/** Extra filters on the search row. */
|
|
59
58
|
filters?: () => unknown
|
|
60
|
-
/** A second filter row under the search. */
|
|
61
|
-
advanced?: () => unknown
|
|
62
59
|
/** Row controls before the check; keys pressed inside them never select the row. */
|
|
63
60
|
actions?: (props: { item: T }) => unknown
|
|
64
61
|
}>()
|
|
65
62
|
|
|
66
63
|
const listId = `mint-resource-picker-${useId().replace(/:/g, '')}`
|
|
67
|
-
const collapsed = reactive(new Set<string>())
|
|
68
64
|
const activeIndex = ref(-1)
|
|
69
65
|
const listRef = ref<HTMLElement | null>(null)
|
|
70
66
|
|
|
71
|
-
const
|
|
72
|
-
props.groups?.map((group) => ({
|
|
73
|
-
...group,
|
|
74
|
-
rows: collapsed.has(group.label) ? [] : group.items,
|
|
75
|
-
})),
|
|
76
|
-
)
|
|
77
|
-
/** Keyboard order: the rows on screen, top to bottom. */
|
|
78
|
-
const flat = computed(() =>
|
|
79
|
-
visibleGroups.value ? visibleGroups.value.flatMap((group) => group.rows) : props.items,
|
|
80
|
-
)
|
|
81
|
-
const indexByKey = computed(() => new Map(flat.value.map((item, i) => [props.itemKey(item), i])))
|
|
67
|
+
const indexByKey = computed(() => new Map(props.items.map((item, i) => [props.itemKey(item), i])))
|
|
82
68
|
const showList = computed(() => !props.loading && !props.error && props.items.length > 0)
|
|
83
69
|
|
|
84
70
|
function optionId(item: T): string {
|
|
85
71
|
return `${listId}-${props.itemKey(item)}`
|
|
86
72
|
}
|
|
87
73
|
const activeDescendant = computed(() => {
|
|
88
|
-
const item =
|
|
74
|
+
const item = props.items[activeIndex.value]
|
|
89
75
|
return item === undefined ? undefined : optionId(item)
|
|
90
76
|
})
|
|
91
77
|
|
|
92
78
|
function indexOf(item: T): number {
|
|
93
79
|
return indexByKey.value.get(props.itemKey(item)) ?? -1
|
|
94
80
|
}
|
|
95
|
-
function toggleGroup(label: string) {
|
|
96
|
-
if (collapsed.has(label)) collapsed.delete(label)
|
|
97
|
-
else collapsed.add(label)
|
|
98
|
-
}
|
|
99
81
|
function keydown(event: KeyboardEvent) {
|
|
100
|
-
// Keys belong to the search combobox; a focused
|
|
82
|
+
// Keys belong to the search combobox; a focused filter or row action keeps its own Enter.
|
|
101
83
|
const target = event.target as HTMLElement | null
|
|
102
84
|
if (target !== event.currentTarget && !target?.closest('.mint-resource-picker__search')) return
|
|
103
|
-
const rows =
|
|
85
|
+
const rows = props.items
|
|
104
86
|
if (!rows.length) return
|
|
105
87
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
106
88
|
event.preventDefault()
|
|
@@ -120,7 +102,7 @@ function keydown(event: KeyboardEvent) {
|
|
|
120
102
|
}
|
|
121
103
|
|
|
122
104
|
watch(
|
|
123
|
-
() =>
|
|
105
|
+
() => props.items,
|
|
124
106
|
() => {
|
|
125
107
|
activeIndex.value = -1
|
|
126
108
|
},
|
|
@@ -136,6 +118,7 @@ watch(
|
|
|
136
118
|
:placeholder="placeholder"
|
|
137
119
|
size="sm"
|
|
138
120
|
type="search"
|
|
121
|
+
:autofocus="autofocusSearch"
|
|
139
122
|
role="combobox"
|
|
140
123
|
:aria-label="`Search ${label.toLowerCase()}`"
|
|
141
124
|
:aria-expanded="showList"
|
|
@@ -151,7 +134,6 @@ watch(
|
|
|
151
134
|
</BaseInput>
|
|
152
135
|
<slot name="filters" />
|
|
153
136
|
</div>
|
|
154
|
-
<slot name="advanced" />
|
|
155
137
|
|
|
156
138
|
<div v-if="loading" class="mint-resource-picker__frame" aria-busy="true">
|
|
157
139
|
<div v-for="n in 4" :key="n" class="mint-resource-picker__skeleton-row">
|
|
@@ -174,64 +156,42 @@ watch(
|
|
|
174
156
|
role="listbox"
|
|
175
157
|
:aria-label="label"
|
|
176
158
|
>
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
159
|
+
<div
|
|
160
|
+
v-for="item in items"
|
|
161
|
+
:id="optionId(item)"
|
|
162
|
+
:key="itemKey(item)"
|
|
163
|
+
role="option"
|
|
164
|
+
:aria-selected="itemKey(item) === selectedKey"
|
|
165
|
+
class="mint-resource-picker__row"
|
|
166
|
+
:class="{
|
|
167
|
+
'mint-resource-picker__row--selected': itemKey(item) === selectedKey,
|
|
168
|
+
'mint-resource-picker__row--active': indexOf(item) === activeIndex,
|
|
169
|
+
}"
|
|
170
|
+
@click="emit('select', item)"
|
|
171
|
+
@mouseenter="activeIndex = indexOf(item)"
|
|
172
|
+
>
|
|
173
|
+
<span class="mint-resource-picker__text">
|
|
174
|
+
<span class="mint-resource-picker__title">{{ describe(item).title }}</span>
|
|
175
|
+
<span v-if="describe(item).code || describe(item).meta?.length" class="mint-resource-picker__meta"
|
|
176
|
+
><span v-if="describe(item).code" class="mint-resource-picker__code mint-exp-code mint-exp-code--sm">{{
|
|
177
|
+
describe(item).code
|
|
178
|
+
}}</span
|
|
179
|
+
><span v-if="describe(item).meta?.length">{{ describe(item).meta!.join(' · ') }}</span></span
|
|
192
180
|
>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
<span
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
'mint-resource-picker__row--active': indexOf(item) === activeIndex,
|
|
208
|
-
}"
|
|
209
|
-
@click="emit('select', item)"
|
|
210
|
-
@mouseenter="activeIndex = indexOf(item)"
|
|
211
|
-
>
|
|
212
|
-
<span class="mint-resource-picker__text">
|
|
213
|
-
<span class="mint-resource-picker__title">{{ describe(item).title }}</span>
|
|
214
|
-
<span v-if="describe(item).code || describe(item).meta?.length" class="mint-resource-picker__meta"
|
|
215
|
-
><span v-if="describe(item).code" class="mint-resource-picker__code mint-exp-code mint-exp-code--sm">{{
|
|
216
|
-
describe(item).code
|
|
217
|
-
}}</span
|
|
218
|
-
><span v-if="describe(item).meta?.length">{{ describe(item).meta!.join(' · ') }}</span></span
|
|
219
|
-
>
|
|
220
|
-
</span>
|
|
221
|
-
<span v-if="describe(item).status || describe(item).date" class="mint-resource-picker__aside">
|
|
222
|
-
<span
|
|
223
|
-
v-if="describe(item).status"
|
|
224
|
-
class="mint-resource-picker__status"
|
|
225
|
-
:class="`mint-resource-picker__status--${describe(item).status!.tone}`"
|
|
226
|
-
>{{ describe(item).status!.label }}</span
|
|
227
|
-
>
|
|
228
|
-
<span v-if="describe(item).date" class="mint-resource-picker__date">{{ describe(item).date }}</span>
|
|
229
|
-
</span>
|
|
230
|
-
<span v-if="$slots.actions" class="mint-resource-picker__actions" @click.stop>
|
|
231
|
-
<slot name="actions" :item="item" />
|
|
232
|
-
</span>
|
|
233
|
-
</div>
|
|
234
|
-
</template>
|
|
181
|
+
</span>
|
|
182
|
+
<span v-if="describe(item).status || describe(item).date" class="mint-resource-picker__aside">
|
|
183
|
+
<span
|
|
184
|
+
v-if="describe(item).status"
|
|
185
|
+
class="mint-resource-picker__status"
|
|
186
|
+
:class="`mint-resource-picker__status--${describe(item).status!.tone}`"
|
|
187
|
+
>{{ describe(item).status!.label }}</span
|
|
188
|
+
>
|
|
189
|
+
<span v-if="describe(item).date" class="mint-resource-picker__date">{{ describe(item).date }}</span>
|
|
190
|
+
</span>
|
|
191
|
+
<span v-if="$slots.actions" class="mint-resource-picker__actions" @click.stop>
|
|
192
|
+
<slot name="actions" :item="item" />
|
|
193
|
+
</span>
|
|
194
|
+
</div>
|
|
235
195
|
</div>
|
|
236
196
|
</div>
|
|
237
197
|
</template>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
/** BaseSelect with `multiple`: a
|
|
3
|
-
import { computed } from 'vue'
|
|
2
|
+
/** BaseSelect with `multiple`: a field of removable chips that opens a filterable checkbox listbox, capped by `max`. */
|
|
3
|
+
import { computed, nextTick, ref, useAttrs, useId, watch } from 'vue'
|
|
4
|
+
import { Check, ChevronDown, X } from 'lucide-vue-next'
|
|
4
5
|
import type { OptionPrimitive, SelectOption, SelectOptionInput } from '../../types'
|
|
6
|
+
import { useEventListener } from '../../composables/useEventListener'
|
|
5
7
|
import { useListSelection } from '../../composables/useListSelection'
|
|
6
8
|
import { normalizeOptionInput } from '../../utils/options'
|
|
7
9
|
|
|
@@ -15,6 +17,8 @@ interface Props {
|
|
|
15
17
|
invalid?: boolean
|
|
16
18
|
max?: number
|
|
17
19
|
size?: 'sm' | 'md' | 'lg'
|
|
20
|
+
ariaLabelledby?: string
|
|
21
|
+
ariaDescribedby?: string
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -23,103 +27,216 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
23
27
|
invalid: false,
|
|
24
28
|
max: undefined,
|
|
25
29
|
size: 'md',
|
|
30
|
+
ariaLabelledby: undefined,
|
|
31
|
+
ariaDescribedby: undefined,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
defineOptions({ inheritAttrs: false })
|
|
35
|
+
|
|
36
|
+
// The wrapper takes class and style; every other attribute (aria-label, data-*) names the focusable field.
|
|
37
|
+
const attrs = useAttrs()
|
|
38
|
+
const fieldAttrs = computed(() => {
|
|
39
|
+
const { class: _class, style: _style, ...rest } = attrs
|
|
40
|
+
return rest
|
|
26
41
|
})
|
|
27
42
|
|
|
28
43
|
const emit = defineEmits<{
|
|
29
44
|
'update:modelValue': [value: OptionPrimitive[]]
|
|
30
45
|
}>()
|
|
31
46
|
|
|
47
|
+
const id = useId().replace(/:/g, '')
|
|
48
|
+
const listboxId = `mint-multi-select-${id}`
|
|
49
|
+
const root = ref<HTMLElement | null>(null)
|
|
50
|
+
const field = ref<HTMLElement | null>(null)
|
|
51
|
+
const search = ref<HTMLInputElement | null>(null)
|
|
52
|
+
const open = ref(false)
|
|
53
|
+
const query = ref('')
|
|
54
|
+
const activeIndex = ref(-1)
|
|
55
|
+
|
|
32
56
|
const normalizedOptions = computed<MultiOption[]>(() => props.options.map(normalizeOptionInput<OptionPrimitive, MultiOption>))
|
|
33
|
-
const
|
|
57
|
+
const selection = useListSelection({
|
|
34
58
|
selected: () => props.modelValue,
|
|
35
59
|
items: normalizedOptions,
|
|
36
60
|
getValue: option => option.value,
|
|
37
61
|
disabled: () => props.disabled,
|
|
38
62
|
max: () => props.max,
|
|
39
63
|
})
|
|
40
|
-
const canSelectMore = optionSelection.canAddMore
|
|
41
64
|
|
|
42
|
-
|
|
43
|
-
|
|
65
|
+
const chips = computed(() => props.modelValue.map(value => ({
|
|
66
|
+
value,
|
|
67
|
+
label: normalizedOptions.value.find(option => option.value === value)?.label ?? String(value),
|
|
68
|
+
})))
|
|
69
|
+
const visibleOptions = computed(() => {
|
|
70
|
+
const needle = query.value.trim().toLocaleLowerCase()
|
|
71
|
+
if (!needle) return normalizedOptions.value
|
|
72
|
+
return normalizedOptions.value.filter(option => option.label.toLocaleLowerCase().includes(needle))
|
|
73
|
+
})
|
|
74
|
+
const activeDescendant = computed(() =>
|
|
75
|
+
activeIndex.value >= 0 ? `${listboxId}-option-${activeIndex.value}` : undefined,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
function isUnavailable(option: MultiOption): boolean {
|
|
79
|
+
return !!option.disabled || (!selection.isSelected(option.value) && !selection.canAddMore.value)
|
|
44
80
|
}
|
|
45
81
|
|
|
46
|
-
function
|
|
47
|
-
if (props.disabled ||
|
|
82
|
+
async function openPanel(): Promise<void> {
|
|
83
|
+
if (props.disabled || open.value) return
|
|
84
|
+
query.value = ''
|
|
85
|
+
open.value = true
|
|
86
|
+
activeIndex.value = visibleOptions.value.length > 0 ? 0 : -1
|
|
87
|
+
await nextTick()
|
|
88
|
+
search.value?.focus()
|
|
89
|
+
}
|
|
48
90
|
|
|
49
|
-
|
|
50
|
-
|
|
91
|
+
async function closePanel(restoreFocus = false): Promise<void> {
|
|
92
|
+
open.value = false
|
|
93
|
+
query.value = ''
|
|
94
|
+
activeIndex.value = -1
|
|
95
|
+
if (restoreFocus) {
|
|
96
|
+
await nextTick()
|
|
97
|
+
field.value?.focus()
|
|
51
98
|
}
|
|
52
99
|
}
|
|
53
100
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
101
|
+
function moveActive(direction: 1 | -1): void {
|
|
102
|
+
const count = visibleOptions.value.length
|
|
103
|
+
if (count === 0) return
|
|
104
|
+
const start = activeIndex.value < 0 ? (direction === 1 ? -1 : count) : activeIndex.value
|
|
105
|
+
activeIndex.value = (start + direction + count) % count
|
|
106
|
+
void nextTick(() => document.getElementById(activeDescendant.value ?? '')?.scrollIntoView({ block: 'nearest' }))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function toggle(option: MultiOption): void {
|
|
110
|
+
if (props.disabled || isUnavailable(option)) return
|
|
111
|
+
emit('update:modelValue', selection.toggleValue(option.value))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function remove(value: OptionPrimitive): void {
|
|
115
|
+
if (props.disabled) return
|
|
116
|
+
emit('update:modelValue', props.modelValue.filter(selected => selected !== value))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function handleFieldKeydown(event: KeyboardEvent): void {
|
|
120
|
+
if (event.key !== 'Enter' && event.key !== ' ' && event.key !== 'ArrowDown') return
|
|
121
|
+
event.preventDefault()
|
|
122
|
+
void openPanel()
|
|
59
123
|
}
|
|
60
124
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
125
|
+
function handleSearchKeydown(event: KeyboardEvent): void {
|
|
126
|
+
if (event.key === 'ArrowDown') moveActive(1)
|
|
127
|
+
else if (event.key === 'ArrowUp') moveActive(-1)
|
|
128
|
+
else if (event.key === 'Enter') {
|
|
129
|
+
const option = visibleOptions.value[activeIndex.value]
|
|
130
|
+
if (option) toggle(option)
|
|
131
|
+
} else if (event.key === 'Escape') {
|
|
132
|
+
void closePanel(true)
|
|
133
|
+
} else if (event.key === 'Backspace' && query.value === '' && props.modelValue.length > 0) {
|
|
134
|
+
remove(props.modelValue[props.modelValue.length - 1]!)
|
|
135
|
+
} else return
|
|
136
|
+
event.preventDefault()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
watch(visibleOptions, options => { activeIndex.value = options.length > 0 ? 0 : -1 })
|
|
140
|
+
watch(() => props.disabled, disabled => { if (disabled) void closePanel() })
|
|
141
|
+
useEventListener(() => document, 'pointerdown', event => {
|
|
142
|
+
if (open.value && !root.value?.contains(event.target as Node)) void closePanel()
|
|
65
143
|
})
|
|
66
144
|
</script>
|
|
67
145
|
|
|
68
146
|
<template>
|
|
69
147
|
<div
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
invalid ? 'mint-multi-select--error' : '',
|
|
75
|
-
]"
|
|
148
|
+
ref="root"
|
|
149
|
+
:class="['mint-multi-select', `mint-multi-select--${size}`, $attrs.class]"
|
|
150
|
+
:style="$attrs.style"
|
|
151
|
+
@focusout="!root?.contains($event.relatedTarget as Node | null) && closePanel()"
|
|
76
152
|
>
|
|
77
153
|
<div
|
|
78
|
-
v-
|
|
79
|
-
|
|
154
|
+
v-bind="fieldAttrs"
|
|
155
|
+
ref="field"
|
|
156
|
+
role="combobox"
|
|
157
|
+
:tabindex="disabled ? undefined : 0"
|
|
158
|
+
:class="[
|
|
159
|
+
'mint-multi-select__field',
|
|
160
|
+
{
|
|
161
|
+
'mint-multi-select__field--open': open,
|
|
162
|
+
'mint-multi-select__field--error': invalid,
|
|
163
|
+
'mint-multi-select__field--disabled': disabled,
|
|
164
|
+
},
|
|
165
|
+
]"
|
|
166
|
+
aria-haspopup="listbox"
|
|
167
|
+
:aria-expanded="open"
|
|
168
|
+
:aria-controls="listboxId"
|
|
169
|
+
:aria-disabled="disabled || undefined"
|
|
170
|
+
:aria-invalid="invalid || undefined"
|
|
171
|
+
:aria-labelledby="ariaLabelledby || undefined"
|
|
172
|
+
:aria-describedby="ariaDescribedby || undefined"
|
|
173
|
+
@click="open ? closePanel() : openPanel()"
|
|
174
|
+
@keydown.self="handleFieldKeydown"
|
|
80
175
|
>
|
|
81
|
-
|
|
176
|
+
<div class="mint-multi-select__chips">
|
|
177
|
+
<span v-for="chip in chips" :key="String(chip.value)" class="mint-multi-select__chip">
|
|
178
|
+
{{ chip.label }}
|
|
179
|
+
<button
|
|
180
|
+
v-if="!disabled"
|
|
181
|
+
type="button"
|
|
182
|
+
tabindex="-1"
|
|
183
|
+
class="mint-multi-select__chip-remove"
|
|
184
|
+
:aria-label="`Remove ${chip.label}`"
|
|
185
|
+
@click.stop="remove(chip.value)"
|
|
186
|
+
>
|
|
187
|
+
<X :size="11" :stroke-width="2.25" aria-hidden="true" />
|
|
188
|
+
</button>
|
|
189
|
+
</span>
|
|
190
|
+
<span v-if="chips.length === 0" class="mint-multi-select__placeholder">{{ placeholder }}</span>
|
|
191
|
+
</div>
|
|
192
|
+
<ChevronDown class="mint-multi-select__icon" :size="14" aria-hidden="true" />
|
|
82
193
|
</div>
|
|
83
194
|
|
|
84
|
-
<div
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
type="button"
|
|
89
|
-
role="checkbox"
|
|
90
|
-
:aria-checked="isSelected(option.value)"
|
|
91
|
-
:disabled="disabled || option.disabled || (!isSelected(option.value) && !canSelectMore)"
|
|
92
|
-
:class="[
|
|
93
|
-
'mint-multi-select__chip',
|
|
94
|
-
`mint-multi-select__chip--${size}`,
|
|
95
|
-
isSelected(option.value) ? 'mint-multi-select__chip--active' : '',
|
|
96
|
-
option.disabled ? 'mint-multi-select__chip--disabled' : '',
|
|
97
|
-
!isSelected(option.value) && !canSelectMore ? 'mint-multi-select__chip--disabled' : '',
|
|
98
|
-
]"
|
|
99
|
-
@click="toggleOption(option)"
|
|
100
|
-
@keydown="handleKeydown($event, option)"
|
|
101
|
-
>
|
|
102
|
-
<svg
|
|
103
|
-
class="mint-multi-select__chip-check"
|
|
104
|
-
viewBox="0 0 24 24"
|
|
105
|
-
fill="none"
|
|
106
|
-
stroke="currentColor"
|
|
107
|
-
stroke-width="2.5"
|
|
108
|
-
stroke-linecap="round"
|
|
109
|
-
stroke-linejoin="round"
|
|
110
|
-
aria-hidden="true"
|
|
111
|
-
>
|
|
112
|
-
<path d="M20 6 9 17l-5-5" />
|
|
195
|
+
<div v-if="open" class="mint-popover mint-searchable-select__panel">
|
|
196
|
+
<div class="mint-searchable-select__search-wrap">
|
|
197
|
+
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" aria-hidden="true">
|
|
198
|
+
<circle cx="8.5" cy="8.5" r="5.5" /><path d="m13 13 4 4" />
|
|
113
199
|
</svg>
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
200
|
+
<input
|
|
201
|
+
ref="search"
|
|
202
|
+
v-model="query"
|
|
203
|
+
role="combobox"
|
|
204
|
+
type="search"
|
|
205
|
+
class="mint-searchable-select__search"
|
|
206
|
+
placeholder="Filter…"
|
|
207
|
+
aria-label="Filter options"
|
|
208
|
+
:aria-expanded="open"
|
|
209
|
+
:aria-controls="listboxId"
|
|
210
|
+
:aria-activedescendant="activeDescendant"
|
|
211
|
+
aria-autocomplete="list"
|
|
212
|
+
@keydown="handleSearchKeydown"
|
|
213
|
+
>
|
|
214
|
+
</div>
|
|
117
215
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
216
|
+
<ul :id="listboxId" class="mint-searchable-select__list" role="listbox" aria-multiselectable="true">
|
|
217
|
+
<li
|
|
218
|
+
v-for="(option, index) in visibleOptions"
|
|
219
|
+
:id="`${listboxId}-option-${index}`"
|
|
220
|
+
:key="String(option.value)"
|
|
221
|
+
role="option"
|
|
222
|
+
class="mint-menu-item mint-multi-select__option"
|
|
223
|
+
:class="{ 'mint-menu-item--active': activeIndex === index }"
|
|
224
|
+
:aria-selected="selection.isSelected(option.value)"
|
|
225
|
+
:aria-disabled="isUnavailable(option) || undefined"
|
|
226
|
+
@mousedown.prevent
|
|
227
|
+
@mouseenter="!isUnavailable(option) && (activeIndex = index)"
|
|
228
|
+
@click="toggle(option)"
|
|
229
|
+
>
|
|
230
|
+
<span
|
|
231
|
+
:class="['mint-multi-select__check', { 'mint-multi-select__check--on': selection.isSelected(option.value) }]"
|
|
232
|
+
aria-hidden="true"
|
|
233
|
+
>
|
|
234
|
+
<Check v-if="selection.isSelected(option.value)" :size="10" :stroke-width="3.5" />
|
|
235
|
+
</span>
|
|
236
|
+
<span class="mint-menu-item__label">{{ option.label }}</span>
|
|
237
|
+
</li>
|
|
238
|
+
<li v-if="visibleOptions.length === 0" class="mint-searchable-select__empty">No matching options</li>
|
|
239
|
+
</ul>
|
|
123
240
|
</div>
|
|
124
241
|
</div>
|
|
125
242
|
</template>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** BaseSelect with `searchable`: a single-select listbox with a search field, descriptions, disabled reasons, and custom slots. */
|
|
3
3
|
import { computed, nextTick, ref, useAttrs, useId, watch } from 'vue'
|
|
4
|
+
import { ChevronsUpDown } from 'lucide-vue-next'
|
|
4
5
|
import type { SelectOption, SelectOptionInput } from '../../types'
|
|
5
6
|
import { useEventListener } from '../../composables/useEventListener'
|
|
6
7
|
import { normalizeOptionInput } from '../../utils/options'
|
|
@@ -176,9 +177,7 @@ useEventListener(() => document, 'pointerdown', event => {
|
|
|
176
177
|
<span :class="['mint-searchable-select__value', { 'mint-searchable-select__value--placeholder': !selectedOption }]">
|
|
177
178
|
{{ selectedOption?.label ?? placeholder }}
|
|
178
179
|
</span>
|
|
179
|
-
<
|
|
180
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.75" d="m6 8 4 4 4-4" />
|
|
181
|
-
</svg>
|
|
180
|
+
<ChevronsUpDown :size="14" aria-hidden="true" />
|
|
182
181
|
</button>
|
|
183
182
|
|
|
184
183
|
<div v-if="open" class="mint-popover mint-searchable-select__panel">
|
package/src/composables/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { useToast } from './useToast'
|
|
|
6
6
|
export { usePlatform } from './usePlatform'
|
|
7
7
|
export { useExperiment, type ExperimentLoadStatus, type UseExperimentReturn } from './useExperiment'
|
|
8
8
|
export { resolvePluginFrontendBase } from './pluginFrontendBase'
|
|
9
|
+
export { providePluginIdentity, type PluginIdentityContract } from './pluginIdentity'
|
|
9
10
|
export {
|
|
10
11
|
useRequest,
|
|
11
12
|
type RequestKind,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { reactive, readonly } from 'vue'
|
|
2
|
+
import type { PluginContract } from './createClient'
|
|
3
|
+
|
|
4
|
+
/** The `plugin` block of the generated contract (`./generated/mint-plugin`) that `providePluginIdentity` reads. */
|
|
5
|
+
export interface PluginIdentityContract {
|
|
6
|
+
plugin: Pick<PluginContract['plugin'], 'name' | 'displayName' | 'subtitle' | 'description' | 'icon'>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const identity = reactive<{ name?: string; subtitle?: string; icon?: string }>({})
|
|
10
|
+
|
|
11
|
+
/** The seeded identity AppTopBar shows in standalone mode. Internal: not exported from the package. */
|
|
12
|
+
export const pluginIdentity = readonly(identity)
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Seed the standalone AppTopBar with the plugin name, subtitle and icon. In integrated
|
|
16
|
+
* mode the platform supplies them. Explicit `title`/`subtitle` props and the `#icon` slot still win.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { providePluginIdentity } from '@morscherlab/mint-sdk'
|
|
21
|
+
* import { contract } from './generated/mint-plugin'
|
|
22
|
+
*
|
|
23
|
+
* providePluginIdentity(contract)
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function providePluginIdentity(contract: PluginIdentityContract): void {
|
|
27
|
+
const { plugin } = contract
|
|
28
|
+
identity.name = plugin.displayName || plugin.name || undefined
|
|
29
|
+
identity.subtitle = plugin.subtitle || plugin.description || undefined
|
|
30
|
+
identity.icon = plugin.icon || undefined
|
|
31
|
+
}
|
|
@@ -23,12 +23,19 @@ export interface UseAppExperimentOptions {
|
|
|
23
23
|
onDetach?: () => void
|
|
24
24
|
saveDisabled?: Ref<boolean> | ComputedRef<boolean>
|
|
25
25
|
saveDisabledMessage?: string | Ref<string | undefined> | ComputedRef<string | undefined>
|
|
26
|
+
/** Unsaved changes; marks the Save button with a dot. */
|
|
27
|
+
dirty?: Ref<boolean> | ComputedRef<boolean>
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export interface AppExperimentPopoverBinding {
|
|
29
31
|
experimentName?: string
|
|
30
32
|
experimentCode?: string
|
|
31
33
|
experimentStatus?: ExperimentStatus
|
|
34
|
+
experimentId?: number | null
|
|
35
|
+
experimentProject?: string
|
|
36
|
+
experimentOwner?: string
|
|
37
|
+
/** ISO timestamp of the experiment's last update. */
|
|
38
|
+
experimentUpdatedAt?: string
|
|
32
39
|
/** An experiment is bound but its record has not arrived yet. */
|
|
33
40
|
isResolving: boolean
|
|
34
41
|
/** Resolution failed; the binding is kept so the user can retry or detach. */
|
|
@@ -39,6 +46,7 @@ export interface AppExperimentPopoverBinding {
|
|
|
39
46
|
saveDisabledMessage?: string
|
|
40
47
|
saveLoading: boolean
|
|
41
48
|
saveSuccessMessage?: string
|
|
49
|
+
dirty?: boolean
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
export interface AppExperimentSelectorModalBinding {
|
|
@@ -125,10 +133,19 @@ export function useAppExperiment(options: UseAppExperimentOptions = {}): UseAppE
|
|
|
125
133
|
const showDetach = computed(() => experimentId.value !== null)
|
|
126
134
|
const saveDisabled = computed(() => toValue(options.saveDisabled) ?? false)
|
|
127
135
|
const saveDisabledMessage = computed(() => toValue(options.saveDisabledMessage))
|
|
136
|
+
const dirty = computed(() => toValue(options.dirty) ?? false)
|
|
137
|
+
const experimentOwner = computed(() => {
|
|
138
|
+
const owner = store.current?.created_by_user
|
|
139
|
+
return owner ? [owner.first_name, owner.last_name].filter(Boolean).join(' ') || owner.username : undefined
|
|
140
|
+
})
|
|
128
141
|
const popover = computed<AppExperimentPopoverBinding>(() => ({
|
|
129
142
|
experimentName: experimentName.value,
|
|
130
143
|
experimentCode: experimentCode.value,
|
|
131
144
|
experimentStatus: experimentStatus.value,
|
|
145
|
+
experimentId: experimentId.value,
|
|
146
|
+
experimentProject: store.current?.project_name || store.current?.project || undefined,
|
|
147
|
+
experimentOwner: experimentOwner.value,
|
|
148
|
+
experimentUpdatedAt: store.current?.updated_at,
|
|
132
149
|
isResolving: isResolving.value,
|
|
133
150
|
error: error.value ?? undefined,
|
|
134
151
|
showSave: showSave.value,
|
|
@@ -137,6 +154,7 @@ export function useAppExperiment(options: UseAppExperimentOptions = {}): UseAppE
|
|
|
137
154
|
saveDisabledMessage: saveDisabledMessage.value,
|
|
138
155
|
saveLoading: saveLoading.value,
|
|
139
156
|
saveSuccessMessage: saveSuccessMessage.value,
|
|
157
|
+
dirty: dirty.value,
|
|
140
158
|
}))
|
|
141
159
|
const selectorModal = computed<AppExperimentSelectorModalBinding>(() => ({
|
|
142
160
|
open: showModal.value,
|
|
@@ -166,6 +184,8 @@ export function useAppExperiment(options: UseAppExperimentOptions = {}): UseAppE
|
|
|
166
184
|
saveLoading.value = true
|
|
167
185
|
try {
|
|
168
186
|
const message = await options.onSave()
|
|
187
|
+
// The save changed the record on the platform; read it back so "Updated" is current.
|
|
188
|
+
await store.refresh()
|
|
169
189
|
if (message) {
|
|
170
190
|
saveSuccessMessage.value = message
|
|
171
191
|
if (successTimer) clearTimeout(successTimer)
|