Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions frontend/src/views/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
</div>

<!-- Server edition / multi-user settings (server edition only) -->
<div v-if="hasTeams" v-show="activeTab === 'teams'" class="card bg-base-100 shadow-md">
<div v-if="hasServerEdition" v-show="activeTab === 'teams'" class="card bg-base-100 shadow-md">
<div class="card-body">
<h2 class="card-title text-lg" data-test="settings-server-edition-title">{{ serverEditionTitle }}</h2>
<SettingsSection section-id="teams" :fields="serverEditionFields" :working="state.working" :original="state.original" />
Expand Down Expand Up @@ -212,15 +212,20 @@ const loadError = ref('')
const activeTab = ref<string>('security')
const showConnect = ref(false)
const state = reactive<{ working: any; original: any }>({ working: {}, original: {} })
const hasTeams = computed(() => state.working && state.working.teams != null)
// Server-edition (multi-user) config lives under `server_edition` (MCP-1086).
// Gate on the canonical key, falling back to the legacy `teams` key so a config
// written before the rename still surfaces the Server Edition tab.
const hasServerEdition = computed(
() => state.working && (state.working.server_edition != null || state.working.teams != null)
)

// cross-section search: type to find any setting across all tabs
const search = ref('')
const allFields = computed<SettingField[]>(() => [
...securityFields,
...generalFields,
...advancedAccordions.flatMap((a) => a.fields),
...(hasTeams.value ? serverEditionFields : []),
...(hasServerEdition.value ? serverEditionFields : []),
])
const filteredFields = computed<SettingField[]>(() => {
const q = search.value.trim().toLowerCase()
Expand Down Expand Up @@ -255,7 +260,7 @@ const tabs = computed(() => {
{ id: 'general', label: 'General', icon: '⚙️' },
{ id: 'advanced', label: 'Advanced', icon: '🧰' },
] as Array<{ id: string; label: string; icon: string }>
if (hasTeams.value) base.push({ id: 'teams', label: SERVER_EDITION_TAB_LABEL, icon: '👥' })
if (hasServerEdition.value) base.push({ id: 'teams', label: SERVER_EDITION_TAB_LABEL, icon: '👥' })
base.push({ id: 'raw', label: 'Raw JSON', icon: '{ }' })
return base
})
Expand All @@ -282,15 +287,29 @@ function clone<T>(v: T): T {
return JSON.parse(JSON.stringify(v))
}

// Back-compat for the teams -> server_edition rename (MCP-1086): if a config
// only carries the legacy `teams` key, mirror it onto `server_edition` so the
// form (which binds to `server_edition.*`) hydrates. Mutates and returns cfg.
function aliasServerEdition(cfg: any): any {
if (cfg && cfg.server_edition == null && cfg.teams != null) {
cfg.server_edition = cfg.teams
}
return cfg
}

async function loadConfig() {
loading.value = true
loadError.value = ''
try {
const response = await api.getConfig()
if (response.success && response.data) {
const cfg = response.data.config
state.working = clone(cfg)
state.original = clone(cfg)
// The server-edition form binds to `server_edition.*` (MCP-1086). The
// backend loader already normalizes a legacy `teams` key to
// `server_edition`, but alias it here too so a config still carrying the
// old key hydrates the form (edits always save under `server_edition`).
state.working = aliasServerEdition(clone(cfg))
state.original = aliasServerEdition(clone(cfg))
configJson.value = JSON.stringify(cfg, null, 2)
configStatus.value = { valid: true }
loaded.value = true
Expand Down
17 changes: 9 additions & 8 deletions frontend/src/views/settings/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,17 +262,18 @@ export const GENERAL_FIELDS: SettingField[] = [
]

// ---- Server edition (multi-user) section ----
// User-facing wording is "Server Edition" (MCP-1087). The config dot-paths
// deliberately stay on the legacy `teams.*` key: the backend rename of the
// top-level config key (`teams` -> `server_edition`, MCP-1085 / PR #607) is
// not merged, so a live config is still `teams`-keyed. Flip these to
// `server_edition.*` in the follow-up only once that backend change lands.
// User-facing wording is "Server Edition" (MCP-1087). The backend rename of the
// top-level config key (`teams` -> `server_edition`, MCP-1086) has landed, so
// these dot-paths write/read the canonical `server_edition.*` key. Legacy
// `teams`-keyed configs still populate the form: the backend loader normalizes
// `teams` -> `server_edition` on load, and Settings.vue aliases it defensively
// so old configs hydrate the form while edits always save under `server_edition`.
export const SERVER_EDITION_TAB_LABEL = 'Server Edition'
export const SERVER_EDITION_SECTION_TITLE = '👥 Server Edition'
export const SERVER_EDITION_FIELDS: SettingField[] = [
{ key: 'teams.enabled', label: 'Enable multi-user mode', control: 'toggle', restart: true },
{ key: 'teams.oauth.provider', label: 'OAuth provider', control: 'select', options: ['', 'google', 'github', 'microsoft'].map((v) => ({ value: v, label: v || '(none)' })) },
{ key: 'teams.max_user_servers', label: 'Max servers per user', control: 'number', min: 0 },
{ key: 'server_edition.enabled', label: 'Enable multi-user mode', control: 'toggle', restart: true },
{ key: 'server_edition.oauth.provider', label: 'OAuth provider', control: 'select', options: ['', 'google', 'github', 'microsoft'].map((v) => ({ value: v, label: v || '(none)' })) },
{ key: 'server_edition.max_user_servers', label: 'Max servers per user', control: 'number', min: 0 },
]

// ---- Section 3: Advanced (subsystem accordions) ----
Expand Down
16 changes: 7 additions & 9 deletions frontend/tests/unit/settings-server-edition-wording.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ import {
SERVER_EDITION_FIELDS,
} from '../../src/views/settings/fields'

// MCP-1087: the Settings server-edition surface must read "Server Edition",
// not the legacy "Teams" wording. The config *keys*, however, deliberately
// stay on the legacy `teams.*` dot-paths until the backend rename of the
// config key (`teams` -> `server_edition`, MCP-1085 / PR #607, currently
// unmerged) lands. Flipping the keys early would make `hasTeams` read
// `state.working.server_edition`, which a live `teams`-keyed config doesn't
// have -> the whole server-edition tab silently disappears.
// MCP-1087 + MCP-1086: the Settings server-edition surface must read "Server
// Edition" (not "Teams"), and the config dot-paths must target the canonical
// `server_edition.*` key now that MCP-1086 (backend rename) has landed.
// Legacy `teams`-keyed configs still hydrate via aliasServerEdition() in
// Settings.vue + the backend loader alias.
describe('Settings server-edition wording (MCP-1087)', () => {
it('uses "Server Edition" wording with no "Teams" left in user-facing labels', () => {
expect(SERVER_EDITION_TAB_LABEL).toBe('Server Edition')
Expand All @@ -20,10 +18,10 @@ describe('Settings server-edition wording (MCP-1087)', () => {
expect(SERVER_EDITION_SECTION_TITLE).toMatch(/Server Edition/)
})

it('keeps the config field keys on the legacy `teams.*` contract (backend rename pending)', () => {
it('uses the canonical server_edition.* config dot-paths (MCP-1086 backend rename landed)', () => {
expect(SERVER_EDITION_FIELDS.length).toBeGreaterThan(0)
for (const f of SERVER_EDITION_FIELDS) {
expect(f.key).toMatch(/^teams\./)
expect(f.key).toMatch(/^server_edition\./)
}
})
})
Loading