@morscherlab/mint-sdk 1.1.0-beta.1 → 1.1.0-beta.11
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 +18 -4
- package/dist/{EmptyState-D1c4RdC7.js → EmptyState-DgBRoYAy.js} +22 -13
- package/dist/EmptyState-DgBRoYAy.js.map +1 -0
- package/dist/{ExperimentSelectorModal-B7w6k3Y8.js → ExperimentSelectorModal-BBWXC7KW.js} +3 -3
- package/dist/{ExperimentSelectorModal-B7w6k3Y8.js.map → ExperimentSelectorModal-BBWXC7KW.js.map} +1 -1
- package/dist/{SettingsModal-DPDwCbXK.js → SettingsModal-Dpg-X1OM.js} +3 -3
- package/dist/{SettingsModal-DPDwCbXK.js.map → SettingsModal-Dpg-X1OM.js.map} +1 -1
- package/dist/__tests__/components/BaseToggle.test.d.ts +1 -0
- package/dist/__tests__/components/SectionCard.test.d.ts +1 -0
- package/dist/__tests__/composables/analysisArtifactTarget.test.d.ts +1 -0
- package/dist/__tests__/types/GeneratedJsonSchema.test.d.ts +1 -0
- package/dist/analysisArtifactTypes-ejqR8MX3.js.map +1 -1
- package/dist/components/BaseSelect.vue.d.ts +1 -1
- package/dist/components/BaseToggle.vue.d.ts +2 -0
- package/dist/components/FitPanel.vue.d.ts +1 -1
- package/dist/components/PluginWorkspaceView.props.d.ts +1 -1
- package/dist/components/SectionCard.vue.d.ts +47 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +6 -6
- package/dist/{components-UfkPN1bK.js → components-Ci_BTx00.js} +711 -633
- package/dist/components-Ci_BTx00.js.map +1 -0
- package/dist/composables/analysisArtifactTarget.d.ts +5 -0
- package/dist/composables/index.d.ts +3 -1
- package/dist/composables/index.js +8 -8
- package/dist/composables/platformContextHelpers.d.ts +2 -0
- package/dist/composables/useAnalysisArtifacts.d.ts +3 -1
- package/dist/composables/useGeneratedAnalysis.d.ts +1 -1
- package/dist/composables/usePlatformContext.d.ts +3 -0
- package/dist/composables/usePluginClient.d.ts +11 -5
- package/dist/{composables-F4JFK9K1.js → composables-C0VEJ73O.js} +172 -30
- package/dist/composables-C0VEJ73O.js.map +1 -0
- package/dist/index.js +11 -11
- package/dist/install.js +5 -5
- package/dist/styles.css +312 -11
- package/dist/tailwind.preset.d.ts +5 -0
- package/dist/tailwind.preset.js +7 -2
- package/dist/tailwind.preset.js.map +1 -1
- package/dist/templates/index.js +3 -3
- package/dist/{templates-Cttbjbyc.js → templates-Cdj93S9C.js} +2 -2
- package/dist/{templates-Cttbjbyc.js.map → templates-Cdj93S9C.js.map} +1 -1
- package/dist/types/analysisArtifactTypes.d.ts +8 -0
- package/dist/types/components.d.ts +2 -0
- package/dist/types/generated-ui.d.ts +13 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/platform.d.ts +2 -0
- package/dist/{useControlSchema-Dallso-L.js → useControlSchema-B_qgU4rW.js} +48 -19
- package/dist/useControlSchema-B_qgU4rW.js.map +1 -0
- package/dist/{useExperimentSelector-BMjzNcf2.js → useExperimentSelector-BuQZRgMt.js} +2 -2
- package/dist/{useExperimentSelector-BMjzNcf2.js.map → useExperimentSelector-BuQZRgMt.js.map} +1 -1
- package/dist/{useFormBuilder-BXaaDI41.js → useFormBuilder-D1G5J5Z5.js} +2 -2
- package/dist/{useFormBuilder-BXaaDI41.js.map → useFormBuilder-D1G5J5Z5.js.map} +1 -1
- package/dist/{useProtocolTemplates-BP7_4RGQ.js → useProtocolTemplates-DeaOmMaw.js} +4 -4
- package/dist/{useProtocolTemplates-BP7_4RGQ.js.map → useProtocolTemplates-DeaOmMaw.js.map} +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/BaseSelect.test.ts +52 -0
- package/src/__tests__/components/BaseToggle.test.ts +40 -0
- package/src/__tests__/components/GeneratedPluginView.test.ts +10 -0
- package/src/__tests__/components/PluginWorkspaceView.test.ts +68 -14
- package/src/__tests__/components/SectionCard.test.ts +181 -0
- package/src/__tests__/composables/analysisArtifactTarget.test.ts +77 -0
- package/src/__tests__/composables/useAnalysisArtifacts.test.ts +74 -1
- package/src/__tests__/composables/useGeneratedAnalysis.test.ts +2 -0
- package/src/__tests__/composables/usePlatformContext.test.ts +28 -0
- package/src/__tests__/composables/usePluginClient.test.ts +365 -21
- package/src/__tests__/composables/usePluginConfig.test.ts +323 -7
- package/src/__tests__/types/GeneratedJsonSchema.test.ts +62 -0
- package/src/components/BaseSelect.vue +16 -2
- package/src/components/BaseToggle.vue +3 -1
- package/src/components/GeneratedPluginView.vue +1 -1
- package/src/components/PluginWorkspaceView.props.ts +1 -1
- package/src/components/PluginWorkspaceView.vue +1 -5
- package/src/components/SectionCard.story.vue +163 -0
- package/src/components/SectionCard.vue +98 -0
- package/src/components/index.ts +1 -0
- package/src/composables/analysisArtifactTarget.ts +53 -0
- package/src/composables/index.ts +6 -0
- package/src/composables/platformContextHelpers.ts +10 -3
- package/src/composables/useAnalysisArtifacts.ts +48 -0
- package/src/composables/useGeneratedAnalysis.ts +30 -12
- package/src/composables/usePluginClient.ts +130 -20
- package/src/composables/usePluginConfig.ts +156 -20
- package/src/styles/components/section-card.css +161 -0
- package/src/styles/index.css +1 -0
- package/src/styles/variables.css +25 -0
- package/src/tailwind.preset.ts +5 -0
- package/src/types/analysisArtifactTypes.ts +9 -0
- package/src/types/components.ts +12 -0
- package/src/types/generated-ui.ts +13 -0
- package/src/types/index.ts +3 -0
- package/src/types/platform.ts +2 -0
- package/dist/EmptyState-D1c4RdC7.js.map +0 -1
- package/dist/components-UfkPN1bK.js.map +0 -1
- package/dist/composables-F4JFK9K1.js.map +0 -1
- package/dist/useControlSchema-Dallso-L.js.map +0 -1
|
@@ -45,6 +45,7 @@ export type PluginHttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'
|
|
|
45
45
|
|
|
46
46
|
export interface PluginEndpointContract {
|
|
47
47
|
name: string
|
|
48
|
+
operationId?: string
|
|
48
49
|
method: PluginHttpMethod
|
|
49
50
|
path: string
|
|
50
51
|
requiresAuth?: boolean
|
|
@@ -75,8 +76,16 @@ export interface PluginNavItemContract {
|
|
|
75
76
|
requiresFeature?: string
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
export interface PluginContractSettings {
|
|
80
|
+
modelName: string | null
|
|
81
|
+
/** Pydantic JSON Schema used to generate typed settings values. */
|
|
82
|
+
valueSchema: Record<string, unknown> | null
|
|
83
|
+
/** SDK SettingsModal schema used to render the settings form. */
|
|
84
|
+
formSchema: SettingsModalSchema | null
|
|
85
|
+
}
|
|
86
|
+
|
|
78
87
|
export interface PluginContract {
|
|
79
|
-
schemaVersion:
|
|
88
|
+
schemaVersion: 2
|
|
80
89
|
plugin: {
|
|
81
90
|
name?: string
|
|
82
91
|
packageName?: string
|
|
@@ -92,11 +101,9 @@ export interface PluginContract {
|
|
|
92
101
|
analysisResultReaders?: string[]
|
|
93
102
|
capabilities?: Record<string, unknown>
|
|
94
103
|
}
|
|
95
|
-
settings
|
|
96
|
-
modelName?: string | null
|
|
97
|
-
schema?: SettingsModalSchema | null
|
|
98
|
-
}
|
|
104
|
+
settings: PluginContractSettings
|
|
99
105
|
endpoints: PluginEndpointContract[]
|
|
106
|
+
schemas?: Record<string, Record<string, unknown>>
|
|
100
107
|
jobs?: GeneratedJobDefinition[]
|
|
101
108
|
hash: string
|
|
102
109
|
}
|
|
@@ -546,8 +553,14 @@ function normalizePluginSettingsOptions(options: UsePluginSettingsOptions | stri
|
|
|
546
553
|
return typeof options === 'string' ? { pluginName: options } : options
|
|
547
554
|
}
|
|
548
555
|
|
|
556
|
+
const PLATFORM_OWNED_SETTINGS_KEY = 'allowed_experiment_types'
|
|
557
|
+
|
|
549
558
|
function cloneRecord(value: Record<string, unknown>): Record<string, unknown> {
|
|
550
|
-
return
|
|
559
|
+
return JSON.parse(JSON.stringify(value)) as Record<string, unknown>
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function hasOwnSetting(settings: Record<string, unknown>, key: string): boolean {
|
|
563
|
+
return Object.prototype.hasOwnProperty.call(settings, key)
|
|
551
564
|
}
|
|
552
565
|
|
|
553
566
|
function responseSettingsPayload(response: unknown): Record<string, unknown> {
|
|
@@ -565,6 +578,43 @@ function responseSettingsPayload(response: unknown): Record<string, unknown> {
|
|
|
565
578
|
return {}
|
|
566
579
|
}
|
|
567
580
|
|
|
581
|
+
function responseSettingsRevision(response: unknown): string | null {
|
|
582
|
+
if (!response || typeof response !== 'object') return null
|
|
583
|
+
const revision = (response as { revision?: unknown }).revision
|
|
584
|
+
return typeof revision === 'string' && revision ? revision : null
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function topLevelSettingsPatch(
|
|
588
|
+
current: Record<string, unknown>,
|
|
589
|
+
keys: Iterable<string>,
|
|
590
|
+
): Record<string, unknown> {
|
|
591
|
+
return Object.fromEntries(
|
|
592
|
+
[...keys].map(key => [
|
|
593
|
+
key,
|
|
594
|
+
hasOwnSetting(current, key) ? current[key] : null,
|
|
595
|
+
]),
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function changedTopLevelSettingKeys(
|
|
600
|
+
current: Record<string, unknown>,
|
|
601
|
+
saved: Record<string, unknown>,
|
|
602
|
+
): Set<string> {
|
|
603
|
+
return new Set(
|
|
604
|
+
[...new Set([...Object.keys(current), ...Object.keys(saved)])].filter(
|
|
605
|
+
key => JSON.stringify(current[key]) !== JSON.stringify(saved[key]),
|
|
606
|
+
),
|
|
607
|
+
)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function platformOwnedSettingsPatch(settings: Record<string, unknown>): Record<string, unknown> {
|
|
611
|
+
return {
|
|
612
|
+
[PLATFORM_OWNED_SETTINGS_KEY]: hasOwnSetting(settings, PLATFORM_OWNED_SETTINGS_KEY)
|
|
613
|
+
? settings[PLATFORM_OWNED_SETTINGS_KEY]
|
|
614
|
+
: null,
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
568
618
|
/** Load, edit, and persist plugin settings from platform config or the plugin-local settings router. */
|
|
569
619
|
export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
570
620
|
options: UsePluginSettingsOptions | string = {},
|
|
@@ -575,8 +625,9 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
575
625
|
const platformApi = useApi({ baseUrl: resolvedOptions.platformApiBaseUrl ?? injectedContext?.platformApiUrl })
|
|
576
626
|
const pluginApi = useApi({ baseUrl: resolvedOptions.apiBaseUrl ?? injectedContext?.plugin?.api_prefix })
|
|
577
627
|
|
|
578
|
-
const values =
|
|
628
|
+
const values = ref({}) as Ref<PluginSettingsValues<TSettings>>
|
|
579
629
|
const savedValues = shallowRef<Record<string, unknown>>({})
|
|
630
|
+
const revision = ref<string | null>(null)
|
|
580
631
|
const request = useRequestSyncState('Plugin settings request failed.')
|
|
581
632
|
const isLoading = ref(false)
|
|
582
633
|
const isSaving = ref(false)
|
|
@@ -584,8 +635,17 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
584
635
|
const resolvedPluginName = computed(() =>
|
|
585
636
|
resolvedOptions.pluginName ?? plugin.value?.name ?? injectedContext?.plugin?.name ?? '',
|
|
586
637
|
)
|
|
638
|
+
const usesPluginSettingsApi = computed(() => {
|
|
639
|
+
const currentPlugin = injectedContext?.plugin ?? plugin.value
|
|
640
|
+
return currentPlugin?.settings_api === 'plugin'
|
|
641
|
+
&& resolvedPluginName.value === currentPlugin.name
|
|
642
|
+
})
|
|
587
643
|
const usesPlatformConfig = computed(() =>
|
|
588
|
-
Boolean(
|
|
644
|
+
Boolean(
|
|
645
|
+
(isIntegrated.value || injectedContext?.isIntegrated)
|
|
646
|
+
&& resolvedPluginName.value
|
|
647
|
+
&& !usesPluginSettingsApi.value,
|
|
648
|
+
),
|
|
589
649
|
)
|
|
590
650
|
const settings = computed(() => values.value)
|
|
591
651
|
const isDirty = computed(() => JSON.stringify(values.value) !== JSON.stringify(savedValues.value))
|
|
@@ -600,10 +660,11 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
600
660
|
values.value = cloneRecord(nextValues) as PluginSettingsValues<TSettings>
|
|
601
661
|
}
|
|
602
662
|
|
|
603
|
-
function applyLoaded(nextValues: Record<string, unknown
|
|
663
|
+
function applyLoaded(nextValues: Record<string, unknown>, nextRevision: string | null): void {
|
|
604
664
|
const cloned = cloneRecord(nextValues)
|
|
605
665
|
values.value = cloned as PluginSettingsValues<TSettings>
|
|
606
666
|
savedValues.value = cloneRecord(cloned)
|
|
667
|
+
revision.value = nextRevision
|
|
607
668
|
}
|
|
608
669
|
|
|
609
670
|
async function load(): Promise<void> {
|
|
@@ -620,7 +681,10 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
620
681
|
() => pluginApi.get<{ settings: Record<string, unknown>; mode?: string }>('/settings'),
|
|
621
682
|
{ success: 'load', errorMessage: 'Failed to load plugin settings' },
|
|
622
683
|
)
|
|
623
|
-
applyLoaded(
|
|
684
|
+
applyLoaded(
|
|
685
|
+
responseSettingsPayload(response),
|
|
686
|
+
responseSettingsRevision(response),
|
|
687
|
+
)
|
|
624
688
|
} catch {
|
|
625
689
|
// Error state is handled by request.run().
|
|
626
690
|
} finally {
|
|
@@ -636,19 +700,65 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
636
700
|
isSaving.value = true
|
|
637
701
|
try {
|
|
638
702
|
const payload = cloneRecord(values.value)
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
{ success: 'save', errorMessage: 'Failed to save plugin settings' },
|
|
703
|
+
let response: unknown
|
|
704
|
+
if (usesPlatformConfig.value) {
|
|
705
|
+
const dirtyKeys = changedTopLevelSettingKeys(payload, savedValues.value)
|
|
706
|
+
const platformOwnedChanged = dirtyKeys.has(PLATFORM_OWNED_SETTINGS_KEY)
|
|
707
|
+
const typedKeys = [...dirtyKeys].filter(
|
|
708
|
+
key => key !== PLATFORM_OWNED_SETTINGS_KEY,
|
|
646
709
|
)
|
|
647
|
-
|
|
648
|
-
|
|
710
|
+
let nextRevision = revision.value
|
|
711
|
+
|
|
712
|
+
if (typedKeys.length) {
|
|
713
|
+
response = await request.run(
|
|
714
|
+
() => platformApi.patch<{
|
|
715
|
+
plugin_name: string
|
|
716
|
+
config: Record<string, unknown>
|
|
717
|
+
revision?: string
|
|
718
|
+
}>(
|
|
719
|
+
`/plugins/${encodeURIComponent(resolvedPluginName.value)}/config`,
|
|
720
|
+
{ config: topLevelSettingsPatch(payload, typedKeys) },
|
|
721
|
+
),
|
|
722
|
+
{ success: 'save', errorMessage: 'Failed to save plugin settings' },
|
|
723
|
+
)
|
|
724
|
+
nextRevision = responseSettingsRevision(response) ?? nextRevision
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (platformOwnedChanged) {
|
|
728
|
+
response = await request.run(
|
|
729
|
+
() => platformApi.patch<{
|
|
730
|
+
plugin_name: string
|
|
731
|
+
config: Record<string, unknown>
|
|
732
|
+
revision?: string
|
|
733
|
+
}>(
|
|
734
|
+
`/plugins/${encodeURIComponent(resolvedPluginName.value)}/config`,
|
|
735
|
+
{ config: platformOwnedSettingsPatch(payload) },
|
|
736
|
+
),
|
|
737
|
+
{ success: 'save', errorMessage: 'Failed to save plugin settings' },
|
|
738
|
+
)
|
|
739
|
+
nextRevision = responseSettingsRevision(response) ?? nextRevision
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (!response) return true
|
|
743
|
+
applyLoaded(responseSettingsPayload(response), nextRevision)
|
|
744
|
+
return true
|
|
745
|
+
} else {
|
|
746
|
+
const ifMatch = revision.value
|
|
747
|
+
? { headers: { 'If-Match': `"${revision.value}"` } }
|
|
748
|
+
: undefined
|
|
749
|
+
response = await request.run(
|
|
750
|
+
() => pluginApi.put<{
|
|
751
|
+
settings: Record<string, unknown>
|
|
752
|
+
mode?: string
|
|
753
|
+
revision?: string
|
|
754
|
+
}>('/settings', payload, ifMatch),
|
|
649
755
|
{ success: 'save', errorMessage: 'Failed to save plugin settings' },
|
|
650
756
|
)
|
|
651
|
-
|
|
757
|
+
}
|
|
758
|
+
applyLoaded(
|
|
759
|
+
responseSettingsPayload(response),
|
|
760
|
+
responseSettingsRevision(response) ?? revision.value,
|
|
761
|
+
)
|
|
652
762
|
return true
|
|
653
763
|
} catch {
|
|
654
764
|
return false
|
|
@@ -1,8 +1,57 @@
|
|
|
1
1
|
import { ref, computed, onMounted, type Ref, type ComputedRef } from 'vue'
|
|
2
2
|
import { useApi } from './useApi'
|
|
3
3
|
import { usePlatformContext } from './usePlatformContext'
|
|
4
|
+
import { getInjectedPlatformContext } from './platformContextHelpers'
|
|
4
5
|
import { useRequestSyncState } from './useRequestSyncState'
|
|
5
6
|
|
|
7
|
+
const PLATFORM_OWNED_SETTINGS_KEY = 'allowed_experiment_types'
|
|
8
|
+
|
|
9
|
+
function cloneConfig(config: Record<string, unknown>): Record<string, unknown> {
|
|
10
|
+
return JSON.parse(JSON.stringify(config)) as Record<string, unknown>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hasOwnConfig(config: Record<string, unknown>, key: string): boolean {
|
|
14
|
+
return Object.prototype.hasOwnProperty.call(config, key)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function changedTopLevelKeys(
|
|
18
|
+
current: Record<string, unknown>,
|
|
19
|
+
saved: Record<string, unknown>,
|
|
20
|
+
): Set<string> {
|
|
21
|
+
return new Set(
|
|
22
|
+
[...new Set([...Object.keys(current), ...Object.keys(saved)])].filter(
|
|
23
|
+
key => JSON.stringify(current[key]) !== JSON.stringify(saved[key]),
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function topLevelConfigPatch(
|
|
29
|
+
current: Record<string, unknown>,
|
|
30
|
+
keys: Iterable<string>,
|
|
31
|
+
): Record<string, unknown> {
|
|
32
|
+
return Object.fromEntries(
|
|
33
|
+
[...keys].map(key => [
|
|
34
|
+
key,
|
|
35
|
+
hasOwnConfig(current, key) ? current[key] : null,
|
|
36
|
+
]),
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function platformOwnedConfigPatch(config: Record<string, unknown>): Record<string, unknown> {
|
|
41
|
+
return {
|
|
42
|
+
[PLATFORM_OWNED_SETTINGS_KEY]: hasOwnConfig(config, PLATFORM_OWNED_SETTINGS_KEY)
|
|
43
|
+
? config[PLATFORM_OWNED_SETTINGS_KEY]
|
|
44
|
+
: null,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function responseConfigPayload(response: {
|
|
49
|
+
config?: Record<string, unknown>
|
|
50
|
+
settings?: Record<string, unknown>
|
|
51
|
+
}): Record<string, unknown> {
|
|
52
|
+
return response.config ?? response.settings ?? {}
|
|
53
|
+
}
|
|
54
|
+
|
|
6
55
|
export interface UsePluginConfigReturn {
|
|
7
56
|
/** Editable plugin configuration values. */
|
|
8
57
|
config: Ref<Record<string, unknown>>
|
|
@@ -28,13 +77,25 @@ export interface UsePluginConfigReturn {
|
|
|
28
77
|
|
|
29
78
|
/** Loads, saves, and tracks dirty state for a plugin's persistent configuration via the platform API. */
|
|
30
79
|
export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
|
|
31
|
-
const
|
|
80
|
+
const injectedContext = getInjectedPlatformContext()
|
|
32
81
|
const { plugin } = usePlatformContext()
|
|
82
|
+
const platformApi = useApi()
|
|
83
|
+
const pluginApi = useApi({
|
|
84
|
+
baseUrl: injectedContext?.plugin?.api_prefix ?? plugin.value?.api_prefix,
|
|
85
|
+
})
|
|
33
86
|
|
|
34
|
-
const resolvedName = computed(
|
|
87
|
+
const resolvedName = computed(
|
|
88
|
+
() => pluginName ?? plugin.value?.name ?? injectedContext?.plugin?.name ?? '',
|
|
89
|
+
)
|
|
90
|
+
const usesPluginSettingsApi = computed(() => {
|
|
91
|
+
const currentPlugin = injectedContext?.plugin ?? plugin.value
|
|
92
|
+
return currentPlugin?.settings_api === 'plugin'
|
|
93
|
+
&& resolvedName.value === currentPlugin.name
|
|
94
|
+
})
|
|
35
95
|
|
|
36
96
|
const config = ref<Record<string, unknown>>({})
|
|
37
97
|
const savedConfig = ref<Record<string, unknown>>({})
|
|
98
|
+
const revision = ref<string | null>(null)
|
|
38
99
|
const request = useRequestSyncState('Plugin config request failed.')
|
|
39
100
|
const isLoading = ref(false)
|
|
40
101
|
const isSaving = ref(false)
|
|
@@ -52,14 +113,29 @@ export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
|
|
|
52
113
|
|
|
53
114
|
isLoading.value = true
|
|
54
115
|
try {
|
|
55
|
-
const response =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
116
|
+
const response = usesPluginSettingsApi.value
|
|
117
|
+
? await request.run(
|
|
118
|
+
() => pluginApi.get<{
|
|
119
|
+
settings: Record<string, unknown>
|
|
120
|
+
mode?: string
|
|
121
|
+
revision?: string
|
|
122
|
+
}>('/settings'),
|
|
123
|
+
{ success: 'load', errorMessage: 'Failed to load plugin config' },
|
|
124
|
+
)
|
|
125
|
+
: await request.run(
|
|
126
|
+
() => platformApi.get<{
|
|
127
|
+
plugin_name: string
|
|
128
|
+
config: Record<string, unknown>
|
|
129
|
+
revision?: string
|
|
130
|
+
}>(
|
|
131
|
+
`/plugins/${encodeURIComponent(name)}/config`,
|
|
132
|
+
),
|
|
133
|
+
{ success: 'load', errorMessage: 'Failed to load plugin config' },
|
|
134
|
+
)
|
|
135
|
+
const responseConfig = responseConfigPayload(response)
|
|
136
|
+
config.value = cloneConfig(responseConfig)
|
|
137
|
+
savedConfig.value = cloneConfig(responseConfig)
|
|
138
|
+
revision.value = response.revision ?? null
|
|
63
139
|
} catch {
|
|
64
140
|
// Error state is handled by request.run().
|
|
65
141
|
} finally {
|
|
@@ -73,15 +149,75 @@ export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
|
|
|
73
149
|
|
|
74
150
|
isSaving.value = true
|
|
75
151
|
try {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
152
|
+
const payload = cloneConfig(config.value)
|
|
153
|
+
let response: {
|
|
154
|
+
plugin_name?: string
|
|
155
|
+
config?: Record<string, unknown>
|
|
156
|
+
settings?: Record<string, unknown>
|
|
157
|
+
mode?: string
|
|
158
|
+
revision?: string
|
|
159
|
+
} | undefined
|
|
160
|
+
let nextRevision = revision.value
|
|
161
|
+
|
|
162
|
+
if (usesPluginSettingsApi.value) {
|
|
163
|
+
response = await request.run(
|
|
164
|
+
() => pluginApi.put<{
|
|
165
|
+
settings: Record<string, unknown>
|
|
166
|
+
mode?: string
|
|
167
|
+
revision?: string
|
|
168
|
+
}>(
|
|
169
|
+
'/settings',
|
|
170
|
+
payload,
|
|
171
|
+
revision.value
|
|
172
|
+
? { headers: { 'If-Match': `"${revision.value}"` } }
|
|
173
|
+
: undefined,
|
|
174
|
+
),
|
|
175
|
+
{ success: 'save', errorMessage: 'Failed to save plugin config' },
|
|
176
|
+
)
|
|
177
|
+
nextRevision = response.revision ?? nextRevision
|
|
178
|
+
} else {
|
|
179
|
+
const dirtyKeys = changedTopLevelKeys(payload, savedConfig.value)
|
|
180
|
+
const platformOwnedChanged = dirtyKeys.has(PLATFORM_OWNED_SETTINGS_KEY)
|
|
181
|
+
const typedKeys = [...dirtyKeys].filter(
|
|
182
|
+
key => key !== PLATFORM_OWNED_SETTINGS_KEY,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if (typedKeys.length) {
|
|
186
|
+
response = await request.run(
|
|
187
|
+
() => platformApi.patch<{
|
|
188
|
+
plugin_name: string
|
|
189
|
+
config: Record<string, unknown>
|
|
190
|
+
revision?: string
|
|
191
|
+
}>(
|
|
192
|
+
`/plugins/${encodeURIComponent(name)}/config`,
|
|
193
|
+
{ config: topLevelConfigPatch(payload, typedKeys) },
|
|
194
|
+
),
|
|
195
|
+
{ success: 'save', errorMessage: 'Failed to save plugin config' },
|
|
196
|
+
)
|
|
197
|
+
nextRevision = response.revision ?? nextRevision
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (platformOwnedChanged) {
|
|
201
|
+
response = await request.run(
|
|
202
|
+
() => platformApi.patch<{
|
|
203
|
+
plugin_name: string
|
|
204
|
+
config: Record<string, unknown>
|
|
205
|
+
revision?: string
|
|
206
|
+
}>(
|
|
207
|
+
`/plugins/${encodeURIComponent(name)}/config`,
|
|
208
|
+
{ config: platformOwnedConfigPatch(payload) },
|
|
209
|
+
),
|
|
210
|
+
{ success: 'save', errorMessage: 'Failed to save plugin config' },
|
|
211
|
+
)
|
|
212
|
+
nextRevision = response.revision ?? nextRevision
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!response) return true
|
|
217
|
+
const responseConfig = responseConfigPayload(response)
|
|
218
|
+
config.value = cloneConfig(responseConfig)
|
|
219
|
+
savedConfig.value = cloneConfig(responseConfig)
|
|
220
|
+
revision.value = response.revision ?? nextRevision
|
|
85
221
|
return true
|
|
86
222
|
} catch {
|
|
87
223
|
return false
|
|
@@ -91,7 +227,7 @@ export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
|
|
|
91
227
|
}
|
|
92
228
|
|
|
93
229
|
function reset(): void {
|
|
94
|
-
config.value =
|
|
230
|
+
config.value = cloneConfig(savedConfig.value)
|
|
95
231
|
request.clearError()
|
|
96
232
|
}
|
|
97
233
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/* SectionCard — the MINT 1.1 one-card system.
|
|
2
|
+
*
|
|
3
|
+
* One card per page region: 12px radius, one shadow token (--card-shadow),
|
|
4
|
+
* a single header strip, and hairline-separated regions inside — never a
|
|
5
|
+
* card nested in a card.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.mint-section-card {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
border-radius: 12px;
|
|
12
|
+
border: 1px solid var(--border-color);
|
|
13
|
+
background: var(--bg-secondary);
|
|
14
|
+
box-shadow: var(--card-shadow);
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
transition: background-color var(--mint-transition), border-color var(--mint-transition);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Fill the height the parent offers: the body takes the slack and scrolls,
|
|
20
|
+
* so the header and footer stay pinned to the card's edges. */
|
|
21
|
+
.mint-section-card--fill {
|
|
22
|
+
flex: 1;
|
|
23
|
+
min-height: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.mint-section-card--fill .mint-section-card__body {
|
|
27
|
+
flex: 1;
|
|
28
|
+
min-height: 0;
|
|
29
|
+
overflow: auto;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.mint-section-card__header {
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: 0.625rem;
|
|
36
|
+
padding: 0.6875rem 1rem;
|
|
37
|
+
border-bottom: 1px solid var(--border-color);
|
|
38
|
+
flex-shrink: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.mint-section-card__header--divider-light {
|
|
42
|
+
border-bottom-color: var(--border-light);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.mint-section-card__icon-chip {
|
|
46
|
+
display: inline-flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
flex-shrink: 0;
|
|
50
|
+
width: 1.375rem;
|
|
51
|
+
height: 1.375rem;
|
|
52
|
+
border-radius: 6px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.mint-section-card__icon {
|
|
56
|
+
width: 0.8125rem;
|
|
57
|
+
height: 0.8125rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.mint-section-card__icon-chip--primary {
|
|
61
|
+
background: var(--color-primary-soft);
|
|
62
|
+
color: var(--color-primary);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.mint-section-card__icon-chip--experiment {
|
|
66
|
+
background: var(--color-experiment-soft);
|
|
67
|
+
color: var(--color-experiment);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.mint-section-card__icon-chip--project {
|
|
71
|
+
background: var(--color-project-soft);
|
|
72
|
+
color: var(--color-project);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.mint-section-card__icon-chip--success {
|
|
76
|
+
background: var(--mint-success-bg);
|
|
77
|
+
color: var(--mint-success);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.mint-section-card__icon-chip--error {
|
|
81
|
+
background: var(--mint-error-bg);
|
|
82
|
+
color: var(--mint-error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.mint-section-card__icon-chip--warning {
|
|
86
|
+
background: var(--mint-warning-bg);
|
|
87
|
+
color: var(--mint-warning);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.mint-section-card__icon-chip--info {
|
|
91
|
+
background: var(--mint-info-bg);
|
|
92
|
+
color: var(--mint-info);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.mint-section-card__title {
|
|
96
|
+
margin: 0;
|
|
97
|
+
font-size: 0.8125rem;
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
color: var(--text-primary);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.mint-section-card__title--uppercase {
|
|
103
|
+
font-size: 0.75rem;
|
|
104
|
+
letter-spacing: 0.08em;
|
|
105
|
+
text-transform: uppercase;
|
|
106
|
+
color: var(--text-secondary);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.mint-section-card__count {
|
|
110
|
+
font-family: 'Fira Code', monospace;
|
|
111
|
+
font-size: 0.6875rem;
|
|
112
|
+
color: var(--text-muted);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.mint-section-card__spacer {
|
|
116
|
+
flex: 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.mint-section-card__toolbar {
|
|
120
|
+
flex-shrink: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.mint-section-card__footer {
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: space-between;
|
|
127
|
+
padding: 0.5rem 1rem;
|
|
128
|
+
border-top: 1px solid var(--border-color);
|
|
129
|
+
background: var(--bg-primary);
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Hairline row helpers — regions inside a card separate with 1px lines,
|
|
134
|
+
* rendered by a 1px gap over a hairline-colored track. */
|
|
135
|
+
.mint-section-card__rows {
|
|
136
|
+
display: flex;
|
|
137
|
+
flex-direction: column;
|
|
138
|
+
gap: 1px;
|
|
139
|
+
background: var(--border-light);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.mint-section-card__row {
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
gap: 0.625rem;
|
|
146
|
+
padding: 0.5625rem 1rem;
|
|
147
|
+
background: var(--bg-secondary);
|
|
148
|
+
transition: background-color var(--mint-transition);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
a.mint-section-card__row,
|
|
152
|
+
button.mint-section-card__row {
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
color: inherit;
|
|
155
|
+
text-decoration: none;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
a.mint-section-card__row:hover,
|
|
159
|
+
button.mint-section-card__row:hover {
|
|
160
|
+
background: var(--bg-primary);
|
|
161
|
+
}
|
package/src/styles/index.css
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
@import './components/plate-map-editor.css';
|
|
29
29
|
@import './components/radio-group.css';
|
|
30
30
|
@import './components/sample-legend.css';
|
|
31
|
+
@import './components/section-card.css';
|
|
31
32
|
@import './components/select.css';
|
|
32
33
|
@import './components/skeleton.css';
|
|
33
34
|
@import './components/slider.css';
|
package/src/styles/variables.css
CHANGED
|
@@ -36,6 +36,18 @@
|
|
|
36
36
|
--color-cta-hover: #EA580C;
|
|
37
37
|
--color-purple: #8B5CF6;
|
|
38
38
|
|
|
39
|
+
/* Entity accent colors — experiments (cyan) and projects (sky), from the
|
|
40
|
+
MINT 1.1 card system. -soft backs icon chips and pills, -border outlines
|
|
41
|
+
pills that sit on a card surface. */
|
|
42
|
+
--color-experiment: #06B6D4;
|
|
43
|
+
--color-experiment-hover: #0891B2;
|
|
44
|
+
--color-experiment-soft: rgba(6, 182, 212, 0.12);
|
|
45
|
+
--color-experiment-border: rgba(6, 182, 212, 0.3);
|
|
46
|
+
--color-project: #0EA5E9;
|
|
47
|
+
--color-project-hover: #0284C7;
|
|
48
|
+
--color-project-soft: rgba(14, 165, 233, 0.12);
|
|
49
|
+
--color-project-border: rgba(14, 165, 233, 0.32);
|
|
50
|
+
|
|
39
51
|
/* MINT brand mark — used for browser theme-color, marketing chrome, app icon background */
|
|
40
52
|
--mint-brand: #7BD0B5;
|
|
41
53
|
--mint-brand-hover: #5FB89A;
|
|
@@ -51,6 +63,8 @@
|
|
|
51
63
|
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
52
64
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
53
65
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
66
|
+
/* The MINT 1.1 card system uses exactly one shadow for every card. */
|
|
67
|
+
--card-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.04), 0 1px 3px 0 rgb(0 0 0 / 0.06);
|
|
54
68
|
|
|
55
69
|
/* Border radius */
|
|
56
70
|
--radius-sm: 0.25rem;
|
|
@@ -148,6 +162,17 @@ html.dark {
|
|
|
148
162
|
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.2);
|
|
149
163
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.2);
|
|
150
164
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.2);
|
|
165
|
+
--card-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.24), 0 1px 3px 0 rgb(0 0 0 / 0.32);
|
|
166
|
+
|
|
167
|
+
/* Entity accent colors - Dark mode */
|
|
168
|
+
--color-experiment: #22D3EE;
|
|
169
|
+
--color-experiment-hover: #67E8F9;
|
|
170
|
+
--color-experiment-soft: rgba(34, 211, 238, 0.16);
|
|
171
|
+
--color-experiment-border: rgba(34, 211, 238, 0.36);
|
|
172
|
+
--color-project: #38BDF8;
|
|
173
|
+
--color-project-hover: #7DD3FC;
|
|
174
|
+
--color-project-soft: rgba(56, 189, 248, 0.16);
|
|
175
|
+
--color-project-border: rgba(56, 189, 248, 0.38);
|
|
151
176
|
|
|
152
177
|
/* Semantic colors - Dark mode */
|
|
153
178
|
--mint-success-bg: rgba(16, 185, 129, 0.15);
|
package/src/tailwind.preset.ts
CHANGED
|
@@ -18,6 +18,10 @@ const mintPreset = {
|
|
|
18
18
|
'warning-hover': 'var(--mint-warning-hover)',
|
|
19
19
|
brand: 'var(--mint-brand)',
|
|
20
20
|
'brand-hover': 'var(--mint-brand-hover)',
|
|
21
|
+
experiment: 'var(--color-experiment)',
|
|
22
|
+
'experiment-hover': 'var(--color-experiment-hover)',
|
|
23
|
+
project: 'var(--color-project)',
|
|
24
|
+
'project-hover': 'var(--color-project-hover)',
|
|
21
25
|
},
|
|
22
26
|
bg: {
|
|
23
27
|
primary: 'var(--mint-bg-primary)',
|
|
@@ -50,6 +54,7 @@ const mintPreset = {
|
|
|
50
54
|
mint: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
|
|
51
55
|
'mint-md': '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
|
52
56
|
'mint-lg': '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
|
|
57
|
+
'mint-card': 'var(--card-shadow)',
|
|
53
58
|
},
|
|
54
59
|
transitionDuration: {
|
|
55
60
|
mint: '150ms',
|