@cat-factory/app 0.167.0 → 0.168.0
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 +45 -2
- package/app/components/auth/UserMenu.vue +9 -2
- package/app/components/board/AddTaskModal.vue +28 -9
- package/app/components/layout/BoardSwitcher.vue +21 -4
- package/app/components/layout/CommandBar.vue +3 -1
- package/app/components/layout/LanguageSwitcher.vue +9 -2
- package/app/components/layout/SideBar.vue +68 -15
- package/app/components/layout/UiModeSwitcher.vue +90 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +35 -8
- package/app/components/settings/ConnectionWarnings.vue +38 -0
- package/app/components/settings/KubernetesEnvironmentForm.vue +5 -1
- package/app/components/settings/ProviderConnectionTab.vue +5 -1
- package/app/components/settings/ProviderManifestEditor.vue +5 -1
- package/app/components/tasks/BugHuntModal.vue +3 -1
- package/app/components/tasks/ContextIssuePicker.vue +22 -5
- package/app/components/tasks/TaskImportModal.vue +1 -1
- package/app/composables/api/errors.ts +16 -0
- package/app/composables/api/tasks.ts +5 -2
- package/app/composables/useNavContributions.ts +3 -0
- package/app/docs/consumer-extensions.md +6 -1
- package/app/modular/nav-contributions.spec.ts +59 -1
- package/app/modular/nav-contributions.ts +62 -4
- package/app/modular/nav-gates.ts +7 -1
- package/app/modular/registry.spec.ts +1 -0
- package/app/stores/bugHunt.ts +2 -10
- package/app/stores/tasks.ts +7 -4
- package/app/stores/uiMode.spec.ts +151 -0
- package/app/stores/uiMode.ts +88 -0
- package/app/utils/connectionWarnings.ts +17 -0
- package/app/utils/uiMode.spec.ts +67 -0
- package/app/utils/uiMode.ts +71 -0
- package/i18n/locales/de.json +23 -5
- package/i18n/locales/en.json +26 -5
- package/i18n/locales/es.json +23 -5
- package/i18n/locales/fr.json +23 -5
- package/i18n/locales/he.json +23 -5
- package/i18n/locales/it.json +23 -5
- package/i18n/locales/ja.json +23 -5
- package/i18n/locales/pl.json +23 -5
- package/i18n/locales/tr.json +23 -5
- package/i18n/locales/uk.json +23 -5
- package/nuxt.config.ts +5 -0
- package/package.json +2 -2
package/app/stores/tasks.ts
CHANGED
|
@@ -146,14 +146,17 @@ export const useTasksStore = defineStore('tasks', () => {
|
|
|
146
146
|
|
|
147
147
|
/**
|
|
148
148
|
* Search a connected tracker's issues by free text (title/content). `blockId`
|
|
149
|
-
* (a service frame or a task/module under one) scopes a GitHub
|
|
150
|
-
* service's linked repo
|
|
151
|
-
* number resolves to the exact issue.
|
|
149
|
+
* (a service frame or a task/module under one) is REQUIRED: it scopes a GitHub
|
|
150
|
+
* search to that service's linked repo, so hits stay in-repo and a bare issue
|
|
151
|
+
* number resolves to the exact issue. There is no unscoped mode — an unscoped
|
|
152
|
+
* GitHub search reaches every repository the backend's credential can see. An
|
|
153
|
+
* issue in another repo is linked by pasting its URL (the picker's by-reference
|
|
154
|
+
* row imports it directly instead of searching).
|
|
152
155
|
*/
|
|
153
156
|
async function search(
|
|
154
157
|
source: TaskSourceKind,
|
|
155
158
|
query: string,
|
|
156
|
-
blockId
|
|
159
|
+
blockId: string,
|
|
157
160
|
): Promise<TaskSearchResult[]> {
|
|
158
161
|
const { results } = await api.searchTaskSource(workspace.requireId(), source, query, blockId)
|
|
159
162
|
return results
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { createPinia, setActivePinia } from 'pinia'
|
|
3
|
+
import { nextTick } from 'vue'
|
|
4
|
+
import { useUiModeStore } from '~/stores/uiMode'
|
|
5
|
+
import type { UiMode } from '~/utils/uiMode'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Point `runtimeConfig.public.uiMode` at a value and hand back a store built against it.
|
|
9
|
+
* The env value is read ONCE at store setup (it is build-time-baked in a `ssr: false` SPA),
|
|
10
|
+
* so every case needs a fresh Pinia — which is also what proves the read isn't reactive.
|
|
11
|
+
*/
|
|
12
|
+
function storeWithEnv(uiMode: string | undefined) {
|
|
13
|
+
vi.stubGlobal('useRuntimeConfig', () => ({ public: { apiBase: '', uiMode } }))
|
|
14
|
+
setActivePinia(createPinia())
|
|
15
|
+
return useUiModeStore()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('useUiModeStore mode resolution', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
// The default runtime-config stub carries no `uiMode` at all — the shape a deployment
|
|
21
|
+
// that never set NUXT_PUBLIC_UI_MODE ends up with once Nuxt applies the '' default.
|
|
22
|
+
vi.stubGlobal('useRuntimeConfig', () => ({ public: { apiBase: '', uiMode: '' } }))
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('boots in basic mode with no env pin and nothing stored', () => {
|
|
26
|
+
const ui = useUiModeStore()
|
|
27
|
+
expect(ui.mode).toBe('basic')
|
|
28
|
+
expect(ui.isAdvanced).toBe(false)
|
|
29
|
+
expect(ui.envPinned).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('honours the browser-stored choice when no env pin is present', () => {
|
|
33
|
+
const ui = useUiModeStore()
|
|
34
|
+
ui.setMode('advanced')
|
|
35
|
+
expect(ui.mode).toBe('advanced')
|
|
36
|
+
expect(ui.isAdvanced).toBe(true)
|
|
37
|
+
// Persisted, so a reload restores it (the persist plugin picks `storedMode`).
|
|
38
|
+
expect(ui.storedMode).toBe('advanced')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('lets the env pin override a conflicting stored choice, in both directions', () => {
|
|
42
|
+
const pinnedBasic = storeWithEnv('advanced')
|
|
43
|
+
pinnedBasic.storedMode = 'basic'
|
|
44
|
+
expect(pinnedBasic.mode).toBe('advanced')
|
|
45
|
+
|
|
46
|
+
const pinnedAdvanced = storeWithEnv('basic')
|
|
47
|
+
pinnedAdvanced.storedMode = 'advanced'
|
|
48
|
+
expect(pinnedAdvanced.mode).toBe('basic')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('refuses to record a preference under an env pin', () => {
|
|
52
|
+
const ui = storeWithEnv('basic')
|
|
53
|
+
expect(ui.envPinned).toBe(true)
|
|
54
|
+
ui.setMode('advanced')
|
|
55
|
+
ui.toggleMode()
|
|
56
|
+
// Nothing written: the resolver would ignore it, so persisting it would be a lie.
|
|
57
|
+
expect(ui.storedMode).toBeNull()
|
|
58
|
+
expect(ui.mode).toBe('basic')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('ignores an unrecognised env value rather than failing the boot', () => {
|
|
62
|
+
const ui = storeWithEnv('expert')
|
|
63
|
+
expect(ui.envPinned).toBe(false)
|
|
64
|
+
expect(ui.mode).toBe('basic')
|
|
65
|
+
ui.setMode('advanced')
|
|
66
|
+
expect(ui.mode).toBe('advanced')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('toggleMode flips between the two tiers', () => {
|
|
70
|
+
const ui = useUiModeStore()
|
|
71
|
+
ui.toggleMode()
|
|
72
|
+
expect(ui.mode).toBe('advanced')
|
|
73
|
+
ui.toggleMode()
|
|
74
|
+
expect(ui.mode).toBe('basic')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('useUiModeStore navbar collapse', () => {
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
vi.stubGlobal('useRuntimeConfig', () => ({ public: { apiBase: '', uiMode: '' } }))
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('starts collapsed in basic mode and expanded in advanced mode', () => {
|
|
84
|
+
expect(useUiModeStore().navCollapsed).toBe(true)
|
|
85
|
+
|
|
86
|
+
const advanced = storeWithEnv('advanced')
|
|
87
|
+
expect(advanced.navCollapsed).toBe(false)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('remembers the rail choice in EITHER tier, across a reload', () => {
|
|
91
|
+
// The persisted shape is per-tier, so an expand in basic is remembered just as an advanced
|
|
92
|
+
// one is — the default is what differs between the tiers, not whether a choice sticks.
|
|
93
|
+
const ui = useUiModeStore()
|
|
94
|
+
ui.toggleNav()
|
|
95
|
+
expect(ui.navCollapsed).toBe(false)
|
|
96
|
+
expect(ui.railCollapsed.basic).toBe(false)
|
|
97
|
+
|
|
98
|
+
const advanced = storeWithEnv('advanced')
|
|
99
|
+
advanced.toggleNav()
|
|
100
|
+
expect(advanced.navCollapsed).toBe(true)
|
|
101
|
+
expect(advanced.railCollapsed.advanced).toBe(true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('keeps each tier’s rail state independent across a switch', async () => {
|
|
105
|
+
const ui = useUiModeStore()
|
|
106
|
+
// Expand in basic, then collapse in advanced: neither choice may leak into the other tier.
|
|
107
|
+
ui.toggleNav()
|
|
108
|
+
expect(ui.navCollapsed).toBe(false)
|
|
109
|
+
|
|
110
|
+
ui.setMode('advanced')
|
|
111
|
+
await nextTick()
|
|
112
|
+
expect(ui.navCollapsed).toBe(false) // advanced's own default
|
|
113
|
+
ui.toggleNav()
|
|
114
|
+
expect(ui.navCollapsed).toBe(true)
|
|
115
|
+
|
|
116
|
+
ui.setMode('basic')
|
|
117
|
+
await nextTick()
|
|
118
|
+
expect(ui.navCollapsed).toBe(false) // the basic expand survived the round trip
|
|
119
|
+
|
|
120
|
+
ui.setMode('advanced')
|
|
121
|
+
await nextTick()
|
|
122
|
+
expect(ui.navCollapsed).toBe(true)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('falls back to the tier default when the restored preference is missing a key', () => {
|
|
126
|
+
// A persisted blob from an older build (or a hand-edited cookie) can omit a tier; a rail
|
|
127
|
+
// stuck on `undefined` would render expanded, which is wrong for basic.
|
|
128
|
+
const ui = useUiModeStore()
|
|
129
|
+
ui.railCollapsed = {} as Record<UiMode, boolean>
|
|
130
|
+
expect(ui.navCollapsed).toBe(true)
|
|
131
|
+
ui.setMode('advanced')
|
|
132
|
+
expect(ui.navCollapsed).toBe(false)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('setNavCollapsed is idempotent', () => {
|
|
136
|
+
const ui = storeWithEnv('advanced')
|
|
137
|
+
ui.setNavCollapsed(true)
|
|
138
|
+
ui.setNavCollapsed(true)
|
|
139
|
+
expect(ui.navCollapsed).toBe(true)
|
|
140
|
+
ui.setNavCollapsed(false)
|
|
141
|
+
expect(ui.navCollapsed).toBe(false)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('useUiModeStore typing', () => {
|
|
146
|
+
it('exposes the mode as the closed union', () => {
|
|
147
|
+
const ui = useUiModeStore()
|
|
148
|
+
const mode: UiMode = ui.mode
|
|
149
|
+
expect(['basic', 'advanced']).toContain(mode)
|
|
150
|
+
})
|
|
151
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import { DEFAULT_RAIL_COLLAPSED, parseUiMode, resolveUiMode, type UiMode } from '~/utils/uiMode'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The interface tier (`basic` / `advanced`) and the side-navbar collapse state.
|
|
7
|
+
*
|
|
8
|
+
* Mode resolution is `env → browser-stored → basic` (see `utils/uiMode.ts`). The env value
|
|
9
|
+
* is `runtimeConfig.public.uiMode`, i.e. `NUXT_PUBLIC_UI_MODE`: the SPA is `ssr: false`, so
|
|
10
|
+
* — exactly like `apiBase` — it is baked in at build time and cannot change while the app
|
|
11
|
+
* is loaded. It is therefore read ONCE here rather than tracked reactively. Only the user's
|
|
12
|
+
* own choice is persisted, so a deployment that later drops its env pin falls back to
|
|
13
|
+
* whatever the user last picked instead of to the default.
|
|
14
|
+
*
|
|
15
|
+
* The collapse state is a PER-TIER preference (`railCollapsed`), not a single boolean. The two
|
|
16
|
+
* tiers want different resting states — basic opens railed, advanced expanded — so one shared
|
|
17
|
+
* flag can only serve one of them: it would either override the other tier's default or throw
|
|
18
|
+
* away the user's choice on every switch. Keying the preference by tier gives each its own
|
|
19
|
+
* default (`DEFAULT_RAIL_COLLAPSED`) AND its own memory, so switching tiers restores what that
|
|
20
|
+
* tier last looked like and neither has to forget. That is also why there is no watcher here:
|
|
21
|
+
* the collapse follows `mode` because it is indexed by it, not because something resets on
|
|
22
|
+
* change.
|
|
23
|
+
*/
|
|
24
|
+
export const useUiModeStore = defineStore(
|
|
25
|
+
'uiMode',
|
|
26
|
+
() => {
|
|
27
|
+
const envMode = parseUiMode(useRuntimeConfig().public.uiMode)
|
|
28
|
+
|
|
29
|
+
/** The user's explicit pick, persisted. `null` until they choose one. */
|
|
30
|
+
const storedMode = ref<UiMode | null>(null)
|
|
31
|
+
/** The rail state each tier was last left in, persisted. Seeded from the per-tier defaults. */
|
|
32
|
+
const railCollapsed = ref<Record<UiMode, boolean>>({ ...DEFAULT_RAIL_COLLAPSED })
|
|
33
|
+
|
|
34
|
+
const mode = computed<UiMode>(() => resolveUiMode(envMode, storedMode.value))
|
|
35
|
+
const isAdvanced = computed(() => mode.value === 'advanced')
|
|
36
|
+
/** Pinned by the deployment: the switcher is read-only, since a write would be ignored. */
|
|
37
|
+
const envPinned = computed(() => envMode !== null)
|
|
38
|
+
|
|
39
|
+
// `?? the default` because the restored value is untrusted input, exactly like the env
|
|
40
|
+
// string: a persisted blob written by an older build (or hand-edited) can be missing this
|
|
41
|
+
// tier's key, and a rail stuck on `undefined` would render as expanded in basic mode.
|
|
42
|
+
const navCollapsed = computed(
|
|
43
|
+
() => railCollapsed.value[mode.value] ?? DEFAULT_RAIL_COLLAPSED[mode.value],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Record the user's pick. A no-op under an env pin — the resolver would ignore it.
|
|
48
|
+
*
|
|
49
|
+
* This guard is hygiene, not the invariant. `storedMode` is returned from the store because
|
|
50
|
+
* a setup store can only persist what it returns (as in `auth`, `accounts`, `workspace`), so
|
|
51
|
+
* a caller CAN write it directly and skip this check. That is harmless by construction:
|
|
52
|
+
* `mode` runs `resolveUiMode(env, stored)`, which consults the env pin FIRST, so a direct
|
|
53
|
+
* write can leave a stale persisted value but can never change the tier under a pin. The
|
|
54
|
+
* "env pin wins, in both directions" case above is what actually pins that.
|
|
55
|
+
*/
|
|
56
|
+
function setMode(next: UiMode) {
|
|
57
|
+
if (envPinned.value) return
|
|
58
|
+
storedMode.value = next
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toggleMode() {
|
|
62
|
+
setMode(isAdvanced.value ? 'basic' : 'advanced')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Remember the rail state for the CURRENT tier; the other tier keeps its own. */
|
|
66
|
+
function setNavCollapsed(collapsed: boolean) {
|
|
67
|
+
railCollapsed.value = { ...railCollapsed.value, [mode.value]: collapsed }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function toggleNav() {
|
|
71
|
+
setNavCollapsed(!navCollapsed.value)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
mode,
|
|
76
|
+
isAdvanced,
|
|
77
|
+
envPinned,
|
|
78
|
+
storedMode,
|
|
79
|
+
railCollapsed,
|
|
80
|
+
navCollapsed,
|
|
81
|
+
setMode,
|
|
82
|
+
toggleMode,
|
|
83
|
+
setNavCollapsed,
|
|
84
|
+
toggleNav,
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{ persist: { pick: ['storedMode', 'railCollapsed'] } },
|
|
88
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ConnectionWarningCode } from '@cat-factory/contracts'
|
|
2
|
+
|
|
3
|
+
// A connection test can report non-fatal GAPS in the config beside its pass/fail verdict: the
|
|
4
|
+
// backend states them as machine-readable codes (it does not localize prose), and the copy the
|
|
5
|
+
// operator reads lives here.
|
|
6
|
+
//
|
|
7
|
+
// The exhaustive `Record<ConnectionWarningCode, …>` is the tier-2 drift guard: a backend that
|
|
8
|
+
// adds a warning code fails this typecheck until the SPA has copy for it, which the typed-key
|
|
9
|
+
// check alone cannot catch for a runtime-assembled key.
|
|
10
|
+
|
|
11
|
+
/** Config-gap warning code → i18n key. Leaf keys mirror the code verbatim. */
|
|
12
|
+
export const CONNECTION_WARNING_KEYS: Record<ConnectionWarningCode, string> = {
|
|
13
|
+
runner_manifest_no_release:
|
|
14
|
+
'settings.providerConnection.test.warnings.runner_manifest_no_release',
|
|
15
|
+
runner_manifest_no_status_path:
|
|
16
|
+
'settings.providerConnection.test.warnings.runner_manifest_no_status_path',
|
|
17
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { DEFAULT_UI_MODE, parseUiMode, resolveUiMode, showOverrideField, UI_MODES } from './uiMode'
|
|
3
|
+
|
|
4
|
+
describe('parseUiMode', () => {
|
|
5
|
+
it('accepts every known mode, case- and whitespace-insensitively', () => {
|
|
6
|
+
for (const mode of UI_MODES) {
|
|
7
|
+
expect(parseUiMode(mode)).toBe(mode)
|
|
8
|
+
expect(parseUiMode(` ${mode.toUpperCase()} `)).toBe(mode)
|
|
9
|
+
}
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('reports no opinion for an unset / unrecognised / non-string value', () => {
|
|
13
|
+
// '' is what `runtimeConfig.public.uiMode` carries when NUXT_PUBLIC_UI_MODE is unset, so
|
|
14
|
+
// it MUST read as "no env pin" rather than as an invalid mode.
|
|
15
|
+
for (const raw of ['', ' ', 'expert', 'Basic mode', undefined, null, 1, {}]) {
|
|
16
|
+
expect(parseUiMode(raw)).toBeNull()
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('resolveUiMode', () => {
|
|
22
|
+
it('lets the env pin win over the stored choice', () => {
|
|
23
|
+
expect(resolveUiMode('basic', 'advanced')).toBe('basic')
|
|
24
|
+
expect(resolveUiMode('advanced', 'basic')).toBe('advanced')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('falls back to the stored choice, then to the default', () => {
|
|
28
|
+
expect(resolveUiMode(null, 'advanced')).toBe('advanced')
|
|
29
|
+
expect(resolveUiMode(null, null)).toBe(DEFAULT_UI_MODE)
|
|
30
|
+
expect(DEFAULT_UI_MODE).toBe('basic')
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('showOverrideField', () => {
|
|
35
|
+
it('shows every override control in advanced mode, set or not', () => {
|
|
36
|
+
expect(showOverrideField(true)).toBe(true)
|
|
37
|
+
expect(showOverrideField(true, null)).toBe(true)
|
|
38
|
+
expect(showOverrideField(true, 'preset_1')).toBe(true)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('hides an unset override in basic mode', () => {
|
|
42
|
+
// The three shapes an unset override arrives in: absent, explicitly null, or the empty
|
|
43
|
+
// string a cleared picker writes. All mean "inherit the default", so nothing is concealed.
|
|
44
|
+
for (const unset of [undefined, null, '']) expect(showOverrideField(false, unset)).toBe(false)
|
|
45
|
+
expect(showOverrideField(false)).toBe(false)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('reveals a SET override in basic mode so it stays visible and clearable', () => {
|
|
49
|
+
// The regression this guards: a block that already carries an override would otherwise run
|
|
50
|
+
// on settings a basic-mode user can neither see nor clear.
|
|
51
|
+
expect(showOverrideField(false, 'policy_strict')).toBe(true)
|
|
52
|
+
expect(showOverrideField(false, true)).toBe(true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('treats `false` as a real override, not as absence', () => {
|
|
56
|
+
// `technical: false` means "business task" — an explicit choice that must NOT be read as
|
|
57
|
+
// unset, which a plain truthiness check would get wrong.
|
|
58
|
+
expect(showOverrideField(false, false)).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('reveals a multi-value group when any one of its values is set', () => {
|
|
62
|
+
// The tracker-writeback group edits three tri-states behind one heading.
|
|
63
|
+
expect(showOverrideField(false, null, null, null)).toBe(false)
|
|
64
|
+
expect(showOverrideField(false, null, false, null)).toBe(true)
|
|
65
|
+
expect(showOverrideField(false, null, null, true)).toBe(true)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interface tier the SPA renders at: `basic` shows the everyday surface, `advanced`
|
|
3
|
+
* shows every destination and every run/pipeline option. Pure resolution logic, kept out
|
|
4
|
+
* of the store so it is testable without Pinia or a Nuxt runtime.
|
|
5
|
+
*
|
|
6
|
+
* Precedence is fixed and NOT negotiable per surface: the deployment's env value always
|
|
7
|
+
* wins over the browser-stored user choice, which wins over the `basic` default. That
|
|
8
|
+
* ordering is what lets an operator pin a fleet of kiosk-ish deployments to one tier
|
|
9
|
+
* without a per-browser reset, so `setMode` is a no-op while the env pin is present
|
|
10
|
+
* rather than writing a preference the resolver would then ignore.
|
|
11
|
+
*/
|
|
12
|
+
export const UI_MODES = ['basic', 'advanced'] as const
|
|
13
|
+
|
|
14
|
+
export type UiMode = (typeof UI_MODES)[number]
|
|
15
|
+
|
|
16
|
+
/** The tier a deployment gets when neither env nor the browser says otherwise. */
|
|
17
|
+
export const DEFAULT_UI_MODE: UiMode = 'basic'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The side-navbar rail state each tier STARTS in, before the user touches the toggle. Basic
|
|
21
|
+
* opens railed (the icon rail is part of what makes it feel basic) and advanced opens
|
|
22
|
+
* expanded. A default, not a rule: the preference is per-tier and remembered, so an explicit
|
|
23
|
+
* choice in either tier survives both a reload and a round trip through the other tier.
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_RAIL_COLLAPSED: Record<UiMode, boolean> = { basic: true, advanced: false }
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Coerce an untrusted value (a `NUXT_PUBLIC_UI_MODE` string baked into the bundle, or a
|
|
29
|
+
* restored cookie/localStorage value) to a known mode. Anything unrecognised — including
|
|
30
|
+
* the empty string the runtime-config default carries when the env var is unset — resolves
|
|
31
|
+
* to `null`, i.e. "this layer has no opinion", so the next one down decides. Never throws:
|
|
32
|
+
* a typo'd env value must degrade to the default rather than fail the boot.
|
|
33
|
+
*/
|
|
34
|
+
export function parseUiMode(raw: unknown): UiMode | null {
|
|
35
|
+
if (typeof raw !== 'string') return null
|
|
36
|
+
const value = raw.trim().toLowerCase()
|
|
37
|
+
return (UI_MODES as readonly string[]).includes(value) ? (value as UiMode) : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Apply the precedence: env pin → browser-stored user choice → {@link DEFAULT_UI_MODE}. */
|
|
41
|
+
export function resolveUiMode(env: UiMode | null, stored: UiMode | null): UiMode {
|
|
42
|
+
return env ?? stored ?? DEFAULT_UI_MODE
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* An override value is SET when it deviates from the inherited default. `null`/`undefined`
|
|
47
|
+
* (and the empty string a cleared picker writes) mean "inherit"; every other value — INCLUDING
|
|
48
|
+
* `false`, which is a real choice on a tri-state like `technical` — is an explicit override.
|
|
49
|
+
*/
|
|
50
|
+
function isOverrideSet(value: unknown): boolean {
|
|
51
|
+
return value != null && value !== ''
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether an OVERRIDE control is rendered at the current tier.
|
|
56
|
+
*
|
|
57
|
+
* Basic mode hides the controls whose only job is to override something already decided
|
|
58
|
+
* elsewhere (a workspace-level default, an engine-inferred flag), which is safe ONLY while
|
|
59
|
+
* what remains is exactly the value the hidden control would have shown. The moment a block
|
|
60
|
+
* actually carries an override that stops being true: hiding it would leave a basic-mode user
|
|
61
|
+
* looking at a task that runs on settings they cannot see, let alone clear — and nothing else
|
|
62
|
+
* in the inspector surfaces them. So basic mode hides the control only while EVERY value it
|
|
63
|
+
* edits is unset, and reveals it (exactly as advanced mode renders it, editable, so the
|
|
64
|
+
* override can be cleared) as soon as one is not.
|
|
65
|
+
*
|
|
66
|
+
* Variadic because a single control can own several values — the tracker-writeback group edits
|
|
67
|
+
* three tri-states behind one heading, and any one of them being set must reveal the group.
|
|
68
|
+
*/
|
|
69
|
+
export function showOverrideField(isAdvanced: boolean, ...values: readonly unknown[]): boolean {
|
|
70
|
+
return isAdvanced || values.some(isOverrideSet)
|
|
71
|
+
}
|
package/i18n/locales/de.json
CHANGED
|
@@ -298,7 +298,12 @@
|
|
|
298
298
|
"test": {
|
|
299
299
|
"button": "Verbindung testen",
|
|
300
300
|
"ok": "Verbindung OK",
|
|
301
|
-
"failed": "Verbindung fehlgeschlagen"
|
|
301
|
+
"failed": "Verbindung fehlgeschlagen",
|
|
302
|
+
"warningsTitle": "Lücken in dieser Konfiguration",
|
|
303
|
+
"warnings": {
|
|
304
|
+
"runner_manifest_no_release": "Kein Release-Template: Beim Abbrechen eines Laufs kann dem Pool nicht mitgeteilt werden, dass er seinen Job stoppen soll. Ein verwaister Job belegt seinen Runner, bis der Pool ihn von selbst zurücknimmt.",
|
|
305
|
+
"runner_manifest_no_status_path": "Kein Statuspfad: Jede Abfrage wird als weiterhin laufend gelesen. Ein Job kann daher nur enden, wenn das Abfragebudget des Laufs aufgebraucht ist."
|
|
306
|
+
}
|
|
302
307
|
},
|
|
303
308
|
"toast": {
|
|
304
309
|
"saved": "{title} gespeichert",
|
|
@@ -1992,7 +1997,8 @@
|
|
|
1992
1997
|
"localModels": "Meine lokalen Runner",
|
|
1993
1998
|
"sandbox": "Sandbox öffnen",
|
|
1994
1999
|
"shortcuts": "Tastenkürzel",
|
|
1995
|
-
"bugHunt": "Fehlerjagd"
|
|
2000
|
+
"bugHunt": "Fehlerjagd",
|
|
2001
|
+
"toggleUiMode": "Oberflächenmodus wechseln"
|
|
1996
2002
|
},
|
|
1997
2003
|
"keywords": {
|
|
1998
2004
|
"newPipeline": "pipeline agents chain",
|
|
@@ -2013,7 +2019,8 @@
|
|
|
2013
2019
|
"localModels": "local model runner ollama lm studio llamacpp vllm endpoint",
|
|
2014
2020
|
"sandbox": "sandbox prompt model test experiment judge fixture benchmark evaluate",
|
|
2015
2021
|
"shortcuts": "keyboard shortcuts keys hotkeys cheatsheet help",
|
|
2016
|
-
"bugHunt": "fehler bug jagd triage backlog tracker nicht zugewiesen"
|
|
2022
|
+
"bugHunt": "fehler bug jagd triage backlog tracker nicht zugewiesen",
|
|
2023
|
+
"toggleUiMode": "oberfläche modus einfach erweitert anzeigen ausblenden"
|
|
2017
2024
|
}
|
|
2018
2025
|
},
|
|
2019
2026
|
"shortcuts": {
|
|
@@ -3242,10 +3249,11 @@
|
|
|
3242
3249
|
"searchPlaceholder": "Issues durchsuchen oder eine URL/einen Schlüssel einfügen…",
|
|
3243
3250
|
"refPlaceholder": "Eine Issue-URL oder einen -Schlüssel einfügen…",
|
|
3244
3251
|
"searchFailed": "Suche fehlgeschlagen: {error}",
|
|
3252
|
+
"searchNeedsRepo": "Dieser Service ist noch mit keinem Repository verknüpft, es gibt also nichts zu durchsuchen. Verknüpfen Sie ihn mit einem Repository oder fügen Sie eine Issue-URL ein, um ein Issue direkt anzuhängen.",
|
|
3245
3253
|
"imported": "importiert",
|
|
3246
3254
|
"attachByReference": "{ref} per Referenz anhängen",
|
|
3247
|
-
"noMatches": "Keine passenden Issues.",
|
|
3248
|
-
"emptySearchable": "
|
|
3255
|
+
"noMatches": "Keine passenden Issues im Repository dieses Service. Fügen Sie eine Issue-URL ein, um eines von anderswo anzuhängen.",
|
|
3256
|
+
"emptySearchable": "Durchsuchen Sie das Repository dieses Service nach Titel oder wählen Sie ein importiertes Issue aus.",
|
|
3249
3257
|
"emptyRefOnly": "Fügen Sie eine Issue-URL oder einen -Schlüssel ein, um es anzuhängen.",
|
|
3250
3258
|
"sourceLabel": "Quelle",
|
|
3251
3259
|
"noSource": "Quelle wählen",
|
|
@@ -4202,6 +4210,14 @@
|
|
|
4202
4210
|
"dismiss": "Schließen"
|
|
4203
4211
|
}
|
|
4204
4212
|
},
|
|
4213
|
+
"uiMode": {
|
|
4214
|
+
"switcher": "Oberfläche",
|
|
4215
|
+
"basic": "Einfach",
|
|
4216
|
+
"advanced": "Erweitert",
|
|
4217
|
+
"basicHint": "Nur die alltäglichen Werkzeuge. Für alles auf „Erweitert“ umschalten.",
|
|
4218
|
+
"advancedHint": "Alle Bereiche und alle Ausführungsoptionen.",
|
|
4219
|
+
"pinned": "Von dieser Installation festgelegt"
|
|
4220
|
+
},
|
|
4205
4221
|
"common": {
|
|
4206
4222
|
"loading": "Wird geladen…",
|
|
4207
4223
|
"save": "Speichern",
|
|
@@ -4274,6 +4290,8 @@
|
|
|
4274
4290
|
"menu": "Navigationsmenü",
|
|
4275
4291
|
"openMenu": "Menü öffnen",
|
|
4276
4292
|
"closeMenu": "Menü schließen",
|
|
4293
|
+
"expandSidebar": "Seitenleiste ausklappen",
|
|
4294
|
+
"collapseSidebar": "Seitenleiste einklappen",
|
|
4277
4295
|
"commandBar": "Suchen oder einen Befehl ausführen…",
|
|
4278
4296
|
"create": "Erstellen",
|
|
4279
4297
|
"buildPipeline": "Eine Pipeline erstellen",
|
package/i18n/locales/en.json
CHANGED
|
@@ -23,6 +23,17 @@
|
|
|
23
23
|
"dismiss": "Dismiss"
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
|
+
"uiMode": {
|
|
27
|
+
"switcher": "Interface",
|
|
28
|
+
"@switcher": {
|
|
29
|
+
"description": "Label above the picker that chooses how much of the app is shown (Basic or Advanced). It names the app's user interface, NOT a programming interface."
|
|
30
|
+
},
|
|
31
|
+
"basic": "Basic",
|
|
32
|
+
"advanced": "Advanced",
|
|
33
|
+
"basicHint": "Everyday tools only. Switch to Advanced to see everything.",
|
|
34
|
+
"advancedHint": "Every destination and every run option.",
|
|
35
|
+
"pinned": "Set by this deployment"
|
|
36
|
+
},
|
|
26
37
|
"common": {
|
|
27
38
|
"loading": "Loading…",
|
|
28
39
|
"save": "Save",
|
|
@@ -107,6 +118,8 @@
|
|
|
107
118
|
"menu": "Navigation menu",
|
|
108
119
|
"openMenu": "Open menu",
|
|
109
120
|
"closeMenu": "Close menu",
|
|
121
|
+
"expandSidebar": "Expand sidebar",
|
|
122
|
+
"collapseSidebar": "Collapse sidebar",
|
|
110
123
|
"commandBar": "Search or run a command…",
|
|
111
124
|
"create": "Create",
|
|
112
125
|
"buildPipeline": "Build a pipeline",
|
|
@@ -2006,7 +2019,8 @@
|
|
|
2006
2019
|
"localModels": "My local runners",
|
|
2007
2020
|
"sandbox": "Open Sandbox",
|
|
2008
2021
|
"shortcuts": "Keyboard shortcuts",
|
|
2009
|
-
"bugHunt": "Bug hunt"
|
|
2022
|
+
"bugHunt": "Bug hunt",
|
|
2023
|
+
"toggleUiMode": "Switch interface mode"
|
|
2010
2024
|
},
|
|
2011
2025
|
"keywords": {
|
|
2012
2026
|
"newPipeline": "pipeline agents chain",
|
|
@@ -2027,7 +2041,8 @@
|
|
|
2027
2041
|
"localModels": "local model runner ollama lm studio llamacpp vllm endpoint",
|
|
2028
2042
|
"sandbox": "sandbox prompt model test experiment judge fixture benchmark evaluate",
|
|
2029
2043
|
"shortcuts": "keyboard shortcuts keys hotkeys cheatsheet help",
|
|
2030
|
-
"bugHunt": "bug hunt triage backlog issue tracker unassigned"
|
|
2044
|
+
"bugHunt": "bug hunt triage backlog issue tracker unassigned",
|
|
2045
|
+
"toggleUiMode": "interface mode basic advanced simple expert show hide"
|
|
2031
2046
|
}
|
|
2032
2047
|
},
|
|
2033
2048
|
"shortcuts": {
|
|
@@ -2467,7 +2482,12 @@
|
|
|
2467
2482
|
"test": {
|
|
2468
2483
|
"button": "Test connection",
|
|
2469
2484
|
"ok": "Connection OK",
|
|
2470
|
-
"failed": "Connection failed"
|
|
2485
|
+
"failed": "Connection failed",
|
|
2486
|
+
"warningsTitle": "Gaps in this configuration",
|
|
2487
|
+
"warnings": {
|
|
2488
|
+
"runner_manifest_no_release": "No release template: cancelling a run cannot tell the pool to stop its job, so an orphaned job keeps its runner until the pool reclaims it on its own.",
|
|
2489
|
+
"runner_manifest_no_status_path": "No status path: every poll reads as still running, so a job can only end by exhausting the run's poll budget."
|
|
2490
|
+
}
|
|
2471
2491
|
},
|
|
2472
2492
|
"toast": {
|
|
2473
2493
|
"saved": "{title} saved",
|
|
@@ -3641,10 +3661,11 @@
|
|
|
3641
3661
|
"searchPlaceholder": "Search issues or paste a URL/key…",
|
|
3642
3662
|
"refPlaceholder": "Paste an issue URL or key…",
|
|
3643
3663
|
"searchFailed": "Search failed: {error}",
|
|
3664
|
+
"searchNeedsRepo": "This service isn't linked to a repository yet, so there's nothing to search. Link it to a repo, or paste an issue URL to attach one directly.",
|
|
3644
3665
|
"imported": "imported",
|
|
3645
3666
|
"attachByReference": "Attach {ref} by reference",
|
|
3646
|
-
"noMatches": "No matching issues.",
|
|
3647
|
-
"emptySearchable": "Search by title, or pick an imported issue.",
|
|
3667
|
+
"noMatches": "No matching issues in this service's repository. Paste an issue URL to attach one from elsewhere.",
|
|
3668
|
+
"emptySearchable": "Search this service's repository by title, or pick an imported issue.",
|
|
3648
3669
|
"emptyRefOnly": "Paste an issue URL or key to attach it.",
|
|
3649
3670
|
"sourceLabel": "Source",
|
|
3650
3671
|
"noSource": "Choose a source",
|
package/i18n/locales/es.json
CHANGED
|
@@ -23,6 +23,14 @@
|
|
|
23
23
|
"dismiss": "Descartar"
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
|
+
"uiMode": {
|
|
27
|
+
"switcher": "Interfaz",
|
|
28
|
+
"basic": "Básica",
|
|
29
|
+
"advanced": "Avanzada",
|
|
30
|
+
"basicHint": "Solo las herramientas del día a día. Cambia a Avanzada para verlo todo.",
|
|
31
|
+
"advancedHint": "Todas las secciones y todas las opciones de ejecución.",
|
|
32
|
+
"pinned": "Definido por este despliegue"
|
|
33
|
+
},
|
|
26
34
|
"common": {
|
|
27
35
|
"loading": "Cargando…",
|
|
28
36
|
"save": "Guardar",
|
|
@@ -95,6 +103,8 @@
|
|
|
95
103
|
"menu": "Menú de navegación",
|
|
96
104
|
"openMenu": "Abrir menú",
|
|
97
105
|
"closeMenu": "Cerrar menú",
|
|
106
|
+
"expandSidebar": "Expandir barra lateral",
|
|
107
|
+
"collapseSidebar": "Contraer barra lateral",
|
|
98
108
|
"commandBar": "Buscar o ejecutar un comando…",
|
|
99
109
|
"create": "Crear",
|
|
100
110
|
"buildPipeline": "Crear una canalización",
|
|
@@ -1940,7 +1950,8 @@
|
|
|
1940
1950
|
"localModels": "Mis ejecutores locales",
|
|
1941
1951
|
"sandbox": "Abrir el entorno de pruebas",
|
|
1942
1952
|
"shortcuts": "Atajos de teclado",
|
|
1943
|
-
"bugHunt": "Caza de errores"
|
|
1953
|
+
"bugHunt": "Caza de errores",
|
|
1954
|
+
"toggleUiMode": "Cambiar el modo de interfaz"
|
|
1944
1955
|
},
|
|
1945
1956
|
"keywords": {
|
|
1946
1957
|
"newPipeline": "canalización agentes cadena pipeline",
|
|
@@ -1961,7 +1972,8 @@
|
|
|
1961
1972
|
"localModels": "modelo local ejecutor ollama lm studio llamacpp vllm endpoint",
|
|
1962
1973
|
"sandbox": "entorno de pruebas prompt modelo prueba experimento juez fixture benchmark evaluar",
|
|
1963
1974
|
"shortcuts": "atajos teclado teclas ayuda",
|
|
1964
|
-
"bugHunt": "error bug caza triaje backlog incidencias sin asignar"
|
|
1975
|
+
"bugHunt": "error bug caza triaje backlog incidencias sin asignar",
|
|
1976
|
+
"toggleUiMode": "interfaz modo básico avanzado mostrar ocultar"
|
|
1965
1977
|
}
|
|
1966
1978
|
},
|
|
1967
1979
|
"integrationsHub": {
|
|
@@ -2202,7 +2214,12 @@
|
|
|
2202
2214
|
"test": {
|
|
2203
2215
|
"button": "Probar conexión",
|
|
2204
2216
|
"ok": "Conexión correcta",
|
|
2205
|
-
"failed": "Falló la conexión"
|
|
2217
|
+
"failed": "Falló la conexión",
|
|
2218
|
+
"warningsTitle": "Carencias en esta configuración",
|
|
2219
|
+
"warnings": {
|
|
2220
|
+
"runner_manifest_no_release": "Sin plantilla de release: al cancelar una ejecución no se puede indicar al pool que detenga su trabajo, así que un trabajo huérfano ocupa su runner hasta que el pool lo recupere por su cuenta.",
|
|
2221
|
+
"runner_manifest_no_status_path": "Sin ruta de estado: cada sondeo se interpreta como todavía en ejecución, así que un trabajo solo puede terminar agotando el presupuesto de sondeo de la ejecución."
|
|
2222
|
+
}
|
|
2206
2223
|
},
|
|
2207
2224
|
"toast": {
|
|
2208
2225
|
"saved": "{title} guardado",
|
|
@@ -3536,10 +3553,11 @@
|
|
|
3536
3553
|
"searchPlaceholder": "Busca incidencias o pega una URL/clave…",
|
|
3537
3554
|
"refPlaceholder": "Pega la URL o clave de una incidencia…",
|
|
3538
3555
|
"searchFailed": "La búsqueda falló: {error}",
|
|
3556
|
+
"searchNeedsRepo": "Este servicio aún no está vinculado a un repositorio, así que no hay nada donde buscar. Vincúlalo a un repositorio o pega la URL de una incidencia para adjuntarla directamente.",
|
|
3539
3557
|
"imported": "importada",
|
|
3540
3558
|
"attachByReference": "Adjuntar {ref} por referencia",
|
|
3541
|
-
"noMatches": "No hay incidencias coincidentes.",
|
|
3542
|
-
"emptySearchable": "Busca por título o elige una incidencia importada.",
|
|
3559
|
+
"noMatches": "No hay incidencias coincidentes en el repositorio de este servicio. Pega la URL de una incidencia para adjuntar una de otro sitio.",
|
|
3560
|
+
"emptySearchable": "Busca por título en el repositorio de este servicio o elige una incidencia importada.",
|
|
3543
3561
|
"emptyRefOnly": "Pega la URL o clave de una incidencia para adjuntarla.",
|
|
3544
3562
|
"sourceLabel": "Fuente",
|
|
3545
3563
|
"noSource": "Elige una fuente",
|