Preserve legacy personal settings in scoped editors

Why: Plugin descriptors without an explicit scope remain personal-compatible in the backend, but the editor hid them.

Evidence: Frontend tests (290), production build, and check-knip pass; focused regression was red before the fix and green after.

Impact: Legacy plugin settings remain visible in user and project-personal editors while shared project and system editors stay scoped.

Refs: Dev feature/rdm-settings-completion P1-P2
This commit is contained in:
2026-10-14 04:58:27 +00:00
parent a5becea652
commit 1f366ecc8d
3 changed files with 22 additions and 3 deletions
+5 -1
View File
@@ -17,7 +17,11 @@ function readableValue(value: unknown) {
const text = String(value)
return optionLabels[text] || text
}
export function scopedSchema(schema: SettingDesc[], scope: string) { return schema.filter(desc => String(desc.scope) === scope) }
export function scopedSchema(schema: SettingDesc[], scope: string) {
return schema.filter(desc => !desc.scope
? scope === 'user' || scope === 'project-personal'
: desc.scope === scope || (scope === 'project-personal' && desc.scope === 'user'))
}
export function settingInputAttrs(desc: SettingDesc): { type: 'text' | 'password'; autoComplete: string } {
return desc.secret || desc.type === 'secret' ? { type: 'password', autoComplete: 'new-password' } : { type: 'text', autoComplete: 'off' }
}
+2 -2
View File
@@ -25,7 +25,7 @@ export function SettingsModal() {
const [notice, setNotice] = useState('')
const [busy, setBusy] = useState(false)
const schema = settings ? scopedSchema(settings.schema, 'user') : []
const personalSchema = projectSettings ? (projectSettings.personalSchema || []).filter(desc => ['user', 'project-personal'].includes(String(desc.scope))) : []
const personalSchema = projectSettings ? scopedSchema(projectSettings.personalSchema || [], 'project-personal') : []
const initial = settings ? scopedDraft(settings.user, schema) : {}
const personalInitial = projectSettings ? scopedDraft(projectSettings.personalProject || {}, personalSchema) : {}
const dirty = JSON.stringify(draft) !== JSON.stringify(initial) || JSON.stringify(personalDraft) !== JSON.stringify(personalInitial)
@@ -41,7 +41,7 @@ export function SettingsModal() {
if (projectId) {
const out = await api.getSettings(projectId)
if (!current()) return false
const personal = (out.personalSchema || []).filter(desc => ['user', 'project-personal'].includes(String(desc.scope)))
const personal = scopedSchema(out.personalSchema || [], 'project-personal')
setProjectSettings(out); setPersonalDraft(scopedDraft(out.personalProject || {}, personal))
}
return true
+15
View File
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import { renderToStaticMarkup } from 'react-dom/server'
import { SettingInput } from '../src/components/SettingsModal'
import { scopedSchema } from '../src/components/SettingsFields'
import { BorrowPanels } from '../src/components/BorrowPanels'
import type { SettingDesc } from '../src/contracts'
@@ -10,6 +11,20 @@ const field = (type: SettingDesc['type'], options?: string[]): SettingDesc => ({
})
describe('setting editors preserve types and readable values', () => {
it('keeps legacy personal plugin settings visible only in personal scopes', () => {
const schema: SettingDesc[] = [
{ ...field('string'), key: 'plugin.legacy', scope: undefined },
{ ...field('string'), key: 'plugin.user', scope: 'user' },
{ ...field('string'), key: 'plugin.project-personal', scope: 'project-personal' },
{ ...field('string'), key: 'plugin.project', scope: 'project' },
{ ...field('string'), key: 'plugin.system', scope: 'system' },
]
const keys = (scope: string) => scopedSchema(schema, scope).map(desc => desc.key)
expect(keys('user')).toEqual(['plugin.legacy', 'plugin.user'])
expect(keys('project-personal')).toEqual(['plugin.legacy', 'plugin.user', 'plugin.project-personal'])
expect(keys('project')).toEqual(['plugin.project'])
expect(keys('system')).toEqual(['plugin.system'])
})
it('uses a number input for numeric values rather than submitting arbitrary strings', () => {
expect(renderToStaticMarkup(<SettingInput desc={field('number')} value={12} onChange={() => {}} />)).toContain('type="number"')
})