feat: 目的地中文显示名(labels 映射)+ 改名自动同步默认目的地 + 保存校验提示可用目的地
This commit is contained in:
@@ -90,6 +90,13 @@ profile 的 `package.json`:
|
|||||||
- 只读/调试下白名单不生效但会保留,切回更改时复用;
|
- 只读/调试下白名单不生效但会保留,切回更改时复用;
|
||||||
- 白名单存在 `<workspace>/.dsh-abap-adt/ui-settings.json`(本插件私有,abap-adt 不会读取)。
|
- 白名单存在 `<workspace>/.dsh-abap-adt/ui-settings.json`(本插件私有,abap-adt 不会读取)。
|
||||||
|
|
||||||
|
### 显示名(中文)
|
||||||
|
|
||||||
|
- 每个目的地可设置**显示名**(如「广东开发」),只影响界面展示;工具调用仍使用稳定的「标识」(`name`);
|
||||||
|
- 显示名存在 `ui-settings.json` 的 `labels` 里,不影响 abap-adt 的 schema;
|
||||||
|
- 改名(标识)时默认目的地下拉会自动跟随,不会出现 `defaultDestination ... is not a destination name`;
|
||||||
|
- 会话状态徽章、目的地表格、默认目的地选择都会显示「显示名(标识)」。
|
||||||
|
|
||||||
### 安全
|
### 安全
|
||||||
|
|
||||||
- 只读 ≠ 无害:仍能读取 SAP 源码与业务数据,建议连开发/测试系统 + 最小权限账号;
|
- 只读 ≠ 无害:仍能读取 SAP 源码与业务数据,建议连开发/测试系统 + 最小权限账号;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ window.__ModuleLoader__.load({
|
|||||||
factory(require) {
|
factory(require) {
|
||||||
const React = require('react')
|
const React = require('react')
|
||||||
const h = React.createElement
|
const h = React.createElement
|
||||||
const { useCallback, useEffect, useMemo, useState } = React
|
const { useCallback, useEffect, useState } = React
|
||||||
|
|
||||||
const NS = 'abap-adt-ui'
|
const NS = 'abap-adt-ui'
|
||||||
const API = '/abap-adt'
|
const API = '/abap-adt'
|
||||||
@@ -19,7 +19,9 @@ window.__ModuleLoader__.load({
|
|||||||
destinations: '目的地',
|
destinations: '目的地',
|
||||||
empty: '还没有配置目的地。内置 demo 模拟系统始终可用。',
|
empty: '还没有配置目的地。内置 demo 模拟系统始终可用。',
|
||||||
add: '新增目的地',
|
add: '新增目的地',
|
||||||
name: '名称',
|
name: '标识(工具调用用)',
|
||||||
|
displayName: '显示名(中文)',
|
||||||
|
displayNameHint: '只用于界面展示;工具调用仍用「标识」',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
client: '客户端',
|
client: '客户端',
|
||||||
language: '语言',
|
language: '语言',
|
||||||
@@ -80,7 +82,9 @@ window.__ModuleLoader__.load({
|
|||||||
destinations: 'Destinations',
|
destinations: 'Destinations',
|
||||||
empty: 'No destinations configured yet. The built-in demo mock system is always available.',
|
empty: 'No destinations configured yet. The built-in demo mock system is always available.',
|
||||||
add: 'Add destination',
|
add: 'Add destination',
|
||||||
name: 'Name',
|
name: 'Identifier (for tool calls)',
|
||||||
|
displayName: 'Display name',
|
||||||
|
displayNameHint: 'Shown in the UI only; tool calls still use the identifier',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
client: 'Client',
|
client: 'Client',
|
||||||
language: 'Language',
|
language: 'Language',
|
||||||
@@ -191,7 +195,7 @@ window.__ModuleLoader__.load({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function blankDestination() {
|
function blankDestination() {
|
||||||
return { name: '', url: '', client: '100', language: 'EN', username: '', passwordEnv: '', strictSSL: false, profile: 'dev', description: '' }
|
return { name: '', label: '', url: '', client: '100', language: 'EN', username: '', passwordEnv: '', strictSSL: false, profile: 'dev', description: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
function Field({ label, hint, children }) {
|
function Field({ label, hint, children }) {
|
||||||
@@ -273,13 +277,12 @@ window.__ModuleLoader__.load({
|
|||||||
load()
|
load()
|
||||||
}, [load])
|
}, [load])
|
||||||
|
|
||||||
const destinationNames = useMemo(
|
|
||||||
() => destinations.map((dest) => dest.name).filter((value) => value && value.trim()),
|
|
||||||
[destinations],
|
|
||||||
)
|
|
||||||
|
|
||||||
const updateDestination = (index, key, value) => {
|
const updateDestination = (index, key, value) => {
|
||||||
setDestinations((rows) => rows.map((row, rowIndex) => (rowIndex === index ? { ...row, [key]: value } : row)))
|
const row = destinations[index]
|
||||||
|
// Renaming the identifier keeps the default destination in sync, so a
|
||||||
|
// rename cannot leave defaultDestination pointing at a gone name.
|
||||||
|
if (key === 'name' && row && defaultDestination === row.name) setDefaultDestination(value)
|
||||||
|
setDestinations((rows) => rows.map((item, itemIndex) => (itemIndex === index ? { ...item, [key]: value } : item)))
|
||||||
}
|
}
|
||||||
|
|
||||||
const addDestination = () => {
|
const addDestination = () => {
|
||||||
@@ -288,6 +291,8 @@ window.__ModuleLoader__.load({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removeDestination = (index) => {
|
const removeDestination = (index) => {
|
||||||
|
const removed = destinations[index]
|
||||||
|
if (removed && removed.name && removed.name === defaultDestination) setDefaultDestination('')
|
||||||
setDestinations((rows) => rows.filter((_, rowIndex) => rowIndex !== index))
|
setDestinations((rows) => rows.filter((_, rowIndex) => rowIndex !== index))
|
||||||
setMessage('')
|
setMessage('')
|
||||||
}
|
}
|
||||||
@@ -309,9 +314,16 @@ window.__ModuleLoader__.load({
|
|||||||
strictSSL: dest.strictSSL,
|
strictSSL: dest.strictSSL,
|
||||||
profile: dest.profile,
|
profile: dest.profile,
|
||||||
}))
|
}))
|
||||||
|
// Display labels are UI-only and keyed by the stable identifier.
|
||||||
|
const labels = {}
|
||||||
|
for (const dest of destinations) {
|
||||||
|
const name = String(dest.name || '').trim()
|
||||||
|
const label = String(dest.label || '').trim()
|
||||||
|
if (name && label) labels[name] = label
|
||||||
|
}
|
||||||
const payload = await api('/save', {
|
const payload = await api('/save', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ workspace, defaultDestination, destinations: payloadDestinations }),
|
body: JSON.stringify({ workspace, defaultDestination, destinations: payloadDestinations, labels }),
|
||||||
})
|
})
|
||||||
setMessage(`${DICTS.zh.saved}: ${payload.path}`)
|
setMessage(`${DICTS.zh.saved}: ${payload.path}`)
|
||||||
setMessageTone('ok')
|
setMessageTone('ok')
|
||||||
@@ -473,7 +485,8 @@ window.__ModuleLoader__.load({
|
|||||||
},
|
},
|
||||||
h(
|
h(
|
||||||
'div',
|
'div',
|
||||||
{ style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 10 } },
|
{ style: { display: 'grid', gridTemplateColumns: '1.2fr 1fr 1.6fr 0.55fr 0.55fr', gap: 10 } },
|
||||||
|
h(Field, { label: DICTS.zh.displayName, hint: DICTS.zh.displayNameHint }, h('input', { style: inputStyle, value: dest.label || '', onChange: (e) => updateDestination(index, 'label', e.target.value), placeholder: '广东开发' })),
|
||||||
h(Field, { label: DICTS.zh.name }, h('input', { style: inputStyle, value: dest.name || '', onChange: (e) => updateDestination(index, 'name', e.target.value), placeholder: 'dev' })),
|
h(Field, { label: DICTS.zh.name }, h('input', { style: inputStyle, value: dest.name || '', onChange: (e) => updateDestination(index, 'name', e.target.value), placeholder: 'dev' })),
|
||||||
h(Field, { label: DICTS.zh.url }, h('input', { style: inputStyle, value: dest.url || '', onChange: (e) => updateDestination(index, 'url', e.target.value), placeholder: 'https://sap.example.com:44300' })),
|
h(Field, { label: DICTS.zh.url }, h('input', { style: inputStyle, value: dest.url || '', onChange: (e) => updateDestination(index, 'url', e.target.value), placeholder: 'https://sap.example.com:44300' })),
|
||||||
h(Field, { label: DICTS.zh.client }, h('input', { style: inputStyle, value: dest.client || '', onChange: (e) => updateDestination(index, 'client', e.target.value), placeholder: '100' })),
|
h(Field, { label: DICTS.zh.client }, h('input', { style: inputStyle, value: dest.client || '', onChange: (e) => updateDestination(index, 'client', e.target.value), placeholder: '100' })),
|
||||||
@@ -628,7 +641,11 @@ window.__ModuleLoader__.load({
|
|||||||
'select',
|
'select',
|
||||||
{ style: inputStyle, value: defaultDestination, onChange: (e) => setDefaultDestination(e.target.value) },
|
{ style: inputStyle, value: defaultDestination, onChange: (e) => setDefaultDestination(e.target.value) },
|
||||||
h('option', { value: '' }, 'demo'),
|
h('option', { value: '' }, 'demo'),
|
||||||
destinationNames.map((value) => h('option', { key: value, value }, value)),
|
destinations
|
||||||
|
.filter((dest) => dest.name)
|
||||||
|
.map((dest) =>
|
||||||
|
h('option', { key: dest.name, value: dest.name }, dest.label ? `${dest.label}(${dest.name})` : dest.name),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -682,7 +699,12 @@ window.__ModuleLoader__.load({
|
|||||||
const destinations = state.destinations || []
|
const destinations = state.destinations || []
|
||||||
const names = destinations.map((dest) => dest.name).filter(Boolean)
|
const names = destinations.map((dest) => dest.name).filter(Boolean)
|
||||||
const selected = state.defaultDestination || 'demo'
|
const selected = state.defaultDestination || 'demo'
|
||||||
const options = ['demo', ...names.filter((name) => name !== 'demo')]
|
const options = [
|
||||||
|
{ name: 'demo', label: DICTS.zh.chipDemo },
|
||||||
|
...destinations
|
||||||
|
.filter((dest) => dest.name && dest.name !== 'demo')
|
||||||
|
.map((dest) => ({ name: dest.name, label: dest.label || dest.name })),
|
||||||
|
]
|
||||||
const modeText = DICTS.zh.modes[state.mode]
|
const modeText = DICTS.zh.modes[state.mode]
|
||||||
const modeBadge = modeText
|
const modeBadge = modeText
|
||||||
? { text: modeText, tone: state.mode === 'change' ? 'err' : state.mode === 'debug' ? 'brand' : 'warn' }
|
? { text: modeText, tone: state.mode === 'change' ? 'err' : state.mode === 'debug' ? 'brand' : 'warn' }
|
||||||
@@ -744,7 +766,13 @@ window.__ModuleLoader__.load({
|
|||||||
maxWidth: 170,
|
maxWidth: 170,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
options.map((name) => h('option', { key: name, value: name }, name)),
|
options.map((option) =>
|
||||||
|
h(
|
||||||
|
'option',
|
||||||
|
{ key: option.name, value: option.name },
|
||||||
|
option.label && option.label !== option.name ? `${option.label}(${option.name})` : option.name,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
names.length ? h('span', { style: { fontSize: 11, color: C.muted } }, `${names.length} ${DICTS.zh.chipDest}`) : null,
|
names.length ? h('span', { style: { fontSize: 11, color: C.muted } }, `${names.length} ${DICTS.zh.chipDest}`) : null,
|
||||||
modeBadge ? h(Badge, { tone: modeBadge.tone }, modeBadge.text) : null,
|
modeBadge ? h(Badge, { tone: modeBadge.tone }, modeBadge.text) : null,
|
||||||
|
|||||||
@@ -358,15 +358,23 @@ export function apply(ctx, config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Read the workspace's UI-only settings (whitelists); defaults when absent. */
|
/** Read the workspace's UI-only settings (whitelists + display labels). */
|
||||||
async function readUiSettings(workspace) {
|
async function readUiSettings(workspace) {
|
||||||
const fallback = { ...WHITELIST_DEFAULTS }
|
const fallback = { ...WHITELIST_DEFAULTS, labels: {} }
|
||||||
try {
|
try {
|
||||||
const raw = await readFile(join(workspace, CONFIG_DIR, UI_SETTINGS_FILE), 'utf8')
|
const raw = await readFile(join(workspace, CONFIG_DIR, UI_SETTINGS_FILE), 'utf8')
|
||||||
const parsed = JSON.parse(raw)
|
const parsed = JSON.parse(raw)
|
||||||
const pick = (key) =>
|
const pick = (key) =>
|
||||||
typeof parsed?.[key] === 'string' && parsed[key].trim() ? parsed[key].trim() : fallback[key]
|
typeof parsed?.[key] === 'string' && parsed[key].trim() ? parsed[key].trim() : fallback[key]
|
||||||
return { allowedPackages: pick('allowedPackages'), allowedTransports: pick('allowedTransports') }
|
const labels = {}
|
||||||
|
if (parsed?.labels && typeof parsed.labels === 'object') {
|
||||||
|
for (const [name, label] of Object.entries(parsed.labels)) {
|
||||||
|
if (typeof name === 'string' && name && typeof label === 'string' && label.trim()) {
|
||||||
|
labels[name] = label.trim().slice(0, 80)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { allowedPackages: pick('allowedPackages'), allowedTransports: pick('allowedTransports'), labels }
|
||||||
} catch {
|
} catch {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
@@ -376,6 +384,7 @@ export function apply(ctx, config) {
|
|||||||
const payload = {
|
const payload = {
|
||||||
allowedPackages: values.allowedPackages || WHITELIST_DEFAULTS.allowedPackages,
|
allowedPackages: values.allowedPackages || WHITELIST_DEFAULTS.allowedPackages,
|
||||||
allowedTransports: values.allowedTransports || WHITELIST_DEFAULTS.allowedTransports,
|
allowedTransports: values.allowedTransports || WHITELIST_DEFAULTS.allowedTransports,
|
||||||
|
labels: values.labels && typeof values.labels === 'object' ? values.labels : {},
|
||||||
}
|
}
|
||||||
await mkdir(join(workspace, CONFIG_DIR), { recursive: true })
|
await mkdir(join(workspace, CONFIG_DIR), { recursive: true })
|
||||||
await writeFile(join(workspace, CONFIG_DIR, UI_SETTINGS_FILE), `${JSON.stringify(payload, null, 2)}\n`, 'utf8')
|
await writeFile(join(workspace, CONFIG_DIR, UI_SETTINGS_FILE), `${JSON.stringify(payload, null, 2)}\n`, 'utf8')
|
||||||
@@ -510,12 +519,21 @@ export function apply(ctx, config) {
|
|||||||
const presetInfo = presetInfoOf(sessionId)
|
const presetInfo = presetInfoOf(sessionId)
|
||||||
const mode = modeFromPolicy(parsed.policy)
|
const mode = modeFromPolicy(parsed.policy)
|
||||||
const whitelist = await readUiSettings(cwd)
|
const whitelist = await readUiSettings(cwd)
|
||||||
|
// Display labels are UI-only: attach the Chinese label beside the stable
|
||||||
|
// destination name so the page/chip can show it without touching the plugin
|
||||||
|
// schema (which accepts only the known destination keys).
|
||||||
|
const labels = whitelist.labels || {}
|
||||||
|
const destinations = normalizeDestinations(parsed.destinations).map((dest) => ({
|
||||||
|
...dest,
|
||||||
|
label: labels[dest.name] || '',
|
||||||
|
}))
|
||||||
return {
|
return {
|
||||||
workspace: cwd,
|
workspace: cwd,
|
||||||
workspaces: listWorkspaces(),
|
workspaces: listWorkspaces(),
|
||||||
file: { path: file, exists, bytes: raw.length },
|
file: { path: file, exists, bytes: raw.length },
|
||||||
chip: chipGate(cwd, exists),
|
chip: chipGate(cwd, exists),
|
||||||
destinations: normalizeDestinations(parsed.destinations),
|
destinations,
|
||||||
|
labels,
|
||||||
defaultDestination: parsed.defaultDestination || '',
|
defaultDestination: parsed.defaultDestination || '',
|
||||||
policy: parsed.policy || {},
|
policy: parsed.policy || {},
|
||||||
parser: parsed.parser,
|
parser: parsed.parser,
|
||||||
@@ -723,7 +741,10 @@ export function apply(ctx, config) {
|
|||||||
defaultDestination !== 'demo' &&
|
defaultDestination !== 'demo' &&
|
||||||
!destinations.some((dest) => dest.name === defaultDestination)
|
!destinations.some((dest) => dest.name === defaultDestination)
|
||||||
) {
|
) {
|
||||||
sendJson(res, 400, { ok: false, error: `defaultDestination "${defaultDestination}" is not a destination name` })
|
sendJson(res, 400, {
|
||||||
|
ok: false,
|
||||||
|
error: `defaultDestination "${defaultDestination}" is not a destination name (available: demo, ${destinations.map((dest) => dest.name).join(', ') || '(none)'})`,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const file = existingWorkspaceFile(workspace)
|
const file = existingWorkspaceFile(workspace)
|
||||||
@@ -737,10 +758,23 @@ export function apply(ctx, config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await mkdir(join(workspace, CONFIG_DIR), { recursive: true })
|
await mkdir(join(workspace, CONFIG_DIR), { recursive: true })
|
||||||
const whitelist = await readUiSettings(workspace)
|
const uiSettings = await readUiSettings(workspace)
|
||||||
const text = setModeInText(emitDestinations({ defaultDestination, destinations }), existingMode, whitelist)
|
// Display labels: keep only labels of destinations that still exist.
|
||||||
|
const names = new Set(destinations.map((dest) => dest.name))
|
||||||
|
let labels = uiSettings.labels || {}
|
||||||
|
if (body.labels && typeof body.labels === 'object') {
|
||||||
|
labels = {}
|
||||||
|
for (const [name, label] of Object.entries(body.labels)) {
|
||||||
|
if (typeof name === 'string' && name && typeof label === 'string' && label.trim()) {
|
||||||
|
labels[name] = label.trim().slice(0, 80)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labels = Object.fromEntries(Object.entries(labels).filter(([name]) => names.has(name)))
|
||||||
|
await writeUiSettings(workspace, { ...uiSettings, labels })
|
||||||
|
const text = setModeInText(emitDestinations({ defaultDestination, destinations }), existingMode, uiSettings)
|
||||||
await writeFile(file, text, 'utf8')
|
await writeFile(file, text, 'utf8')
|
||||||
sendJson(res, 200, { ok: true, path: file, destinations: destinations.length, mode: existingMode })
|
sendJson(res, 200, { ok: true, path: file, destinations: destinations.length, mode: existingMode, labels })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,7 +815,8 @@ export function apply(ctx, config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (body.allowedPackages !== undefined || body.allowedTransports !== undefined) {
|
if (body.allowedPackages !== undefined || body.allowedTransports !== undefined) {
|
||||||
await writeUiSettings(workspace, nextWhitelist)
|
// Preserve display labels when persisting whitelists.
|
||||||
|
await writeUiSettings(workspace, { ...current, ...nextWhitelist })
|
||||||
}
|
}
|
||||||
await writeFile(file, setModeInText(raw, mode, nextWhitelist), 'utf8')
|
await writeFile(file, setModeInText(raw, mode, nextWhitelist), 'utf8')
|
||||||
sendJson(res, 200, {
|
sendJson(res, 200, {
|
||||||
|
|||||||
Reference in New Issue
Block a user