Unify settings page submit actions

This commit is contained in:
Haitao Pan 2026-03-29 17:50:47 +08:00
parent a9b904c69d
commit 631d78359d
8 changed files with 45 additions and 177 deletions

View File

@ -92,8 +92,8 @@ extension SettingsPageGatewayConnectionMixinInternal
children: [
Text(
appText(
'这里维护外部 Gateway / ACP endpoint 连接源 profile。工作模式在会话区单独切换:single-agent 通过标准 ACP 协议直连外部 Agent;local/remote 继续走 Gateway。保存:仅保存配置,不立即生效。应用:立即按当前配置生效。',
'This card edits external Gateway / ACP endpoint profiles. Work mode is switched in the session UI: single-agent connects to an external Agent over the standard ACP protocol, while local/remote continue through Gateway. Save persists configuration only, while Apply makes it take effect immediately.',
'这里维护外部 Gateway / ACP endpoint 连接源 profile。工作模式在会话区单独切换:single-agent 通过标准 ACP 协议直连外部 Agent;local/remote 继续走 Gateway。保存并生效:立即按当前配置更新。',
'This card edits external Gateway / ACP endpoint profiles. Work mode is switched in the session UI: single-agent connects to an external Agent over the standard ACP protocol, while local/remote continue through Gateway. Save & apply updates the active configuration immediately.',
),
style: theme.textTheme.bodyMedium,
),
@ -251,12 +251,12 @@ extension SettingsPageGatewayConnectionMixinInternal
profileIndex: selectedProfileIndex,
),
storedHelperText: appText(
'已安全保存,默认以 **** 显示;可直接测试,也可通过本区保存/应用提交。',
'Stored securely. Test directly or submit with local Save / Apply actions.',
'已安全保存,默认以 **** 显示;可直接测试,也可通过本区保存并生效提交。',
'Stored securely. Test directly or submit with the local Save & apply action.',
),
emptyHelperText: appText(
'输入后先进入草稿;通过本区保存/应用提交。',
'Values stage into draft first; submit with local Save / Apply actions.',
'输入后先进入草稿;通过本区保存并生效提交。',
'Values stage into draft first; submit with the local Save & apply action.',
),
),
const SizedBox(height: 12),
@ -277,23 +277,21 @@ extension SettingsPageGatewayConnectionMixinInternal
profileIndex: selectedProfileIndex,
),
storedHelperText: appText(
'已安全保存,默认以 **** 显示;可直接测试,也可通过本区保存/应用提交。',
'Stored securely. Test directly or submit with local Save / Apply actions.',
'已安全保存,默认以 **** 显示;可直接测试,也可通过本区保存并生效提交。',
'Stored securely. Test directly or submit with the local Save & apply action.',
),
emptyHelperText: appText(
'输入后先进入草稿;通过本区保存/应用提交。',
'Values stage into draft first; submit with local Save / Apply actions.',
'输入后先进入草稿;通过本区保存并生效提交。',
'Values stage into draft first; submit with the local Save & apply action.',
),
),
const SizedBox(height: 16),
buildSettingsSectionActionsInternal(
controller: controller,
testKey: const ValueKey('gateway-test-button'),
saveKey: const ValueKey('gateway-save-button'),
applyKey: const ValueKey('gateway-apply-button'),
testing: gatewayTestingInternal,
onTest: () => testGatewayConnectionInternal(controller, settings),
onSave: () => saveGatewayAndPersistInternal(controller, settings),
onApply: () => saveGatewayAndApplyInternal(controller, settings),
),
const SizedBox(height: 16),
@ -390,21 +388,19 @@ extension SettingsPageGatewayConnectionMixinInternal
onSubmitted: (value) async => controller.saveVaultTokenDraft(value),
storedHelperText: appText(
'已安全保存;Vault root token 只会在测试连接或显式保存时使用。',
'Stored securely. The Vault root token is only used for test/apply flows.',
'Stored securely. The Vault root token is only used for test or save-and-apply flows.',
),
emptyHelperText: appText(
'输入后先进入草稿;点击 Save / Apply 后才会写入安全存储。',
'Values stage into draft first and only persist to secure storage after Save / Apply.',
'输入后先进入草稿;点击“保存并生效”后才会写入安全存储。',
'Values stage into draft first and only persist to secure storage after Save & apply.',
),
),
const SizedBox(height: 12),
buildSettingsSectionActionsInternal(
controller: controller,
testKey: const ValueKey('vault-test-button'),
saveKey: const ValueKey('vault-save-button'),
applyKey: const ValueKey('vault-apply-button'),
onTest: () => testVaultConnectionInternal(controller, settings),
onSave: () => handleTopLevelSaveInternal(controller),
onApply: () => handleTopLevelApplyInternal(controller),
testLabel:
'${appText('测试连接', 'Test Connection')} · ${controller.settingsController.vaultStatus}',

View File

@ -117,23 +117,21 @@ extension SettingsPageGatewayLlmMixinInternal on SettingsPageStateInternal {
onSubmitted: (value) async =>
controller.saveAiGatewayApiKeyDraft(value),
storedHelperText: appText(
'已安全保存,默认以 **** 显示;可直接测试,也可通过本区保存/应用提交。',
'Stored securely. Test directly or submit it with the local Save / Apply actions.',
'已安全保存,默认以 **** 显示;可直接测试,也可通过本区保存并生效提交。',
'Stored securely. Test directly or submit it with the local Save & apply action.',
),
emptyHelperText: appText(
'输入后可直接测试,也可通过本区保存/应用提交。',
'Test it now, or submit it with the local Save / Apply actions.',
'输入后可直接测试,也可通过本区保存并生效提交。',
'Test it now, or submit it with the local Save & apply action.',
),
),
const SizedBox(height: 12),
buildSettingsSectionActionsInternal(
controller: controller,
testKey: const ValueKey('ai-gateway-test-button'),
saveKey: const ValueKey('ai-gateway-save-button'),
applyKey: const ValueKey('ai-gateway-apply-button'),
testing: aiGatewayTestingInternal,
onTest: () => testAiGatewayConnectionInternal(controller, settings),
onSave: () => saveAiGatewayAndPersistInternal(controller, settings),
onApply: () => saveAiGatewayAndApplyInternal(controller, settings),
),
const SizedBox(height: 12),
@ -384,12 +382,12 @@ extension SettingsPageGatewayLlmMixinInternal on SettingsPageStateInternal {
onSubmitted: (value) async =>
controller.saveOllamaCloudApiKeyDraft(value),
storedHelperText: appText(
'已安全保存,默认以 **** 显示;可直接测试,也可通过本区保存/应用提交。',
'Stored securely. Test directly or submit it with the local Save / Apply actions.',
'已安全保存,默认以 **** 显示;可直接测试,也可通过本区保存并生效提交。',
'Stored securely. Test directly or submit it with the local Save & apply action.',
),
emptyHelperText: appText(
'输入后可直接测试,也可通过本区保存/应用提交。',
'Test it now, or submit it with the local Save / Apply actions.',
'输入后可直接测试,也可通过本区保存并生效提交。',
'Test it now, or submit it with the local Save & apply action.',
),
),
const SizedBox(height: 12),

View File

@ -186,17 +186,6 @@ XWorkmate Privacy Policy
return controller.saveSettingsDraft(snapshot);
}
Future<void> handleTopLevelSaveInternal(AppController controller) async {
await captureVisibleSecretDraftsInternal(controller);
await controller.persistSettingsDraft();
if (!mounted) {
return;
}
setStateInternal(() {
resetSecureFieldUiAfterPersistInternal(controller);
});
}
Future<void> handleTopLevelApplyInternal(AppController controller) async {
await captureVisibleSecretDraftsInternal(controller);
await controller.applySettingsDraft();
@ -473,14 +462,6 @@ XWorkmate Privacy Policy
await saveGatewayProfileInternal(controller, settings, profile);
}
Future<void> saveGatewayAndPersistInternal(
AppController controller,
SettingsSnapshot settings,
) async {
await saveGatewayDraftInternal(controller, settings);
await handleTopLevelSaveInternal(controller);
}
Future<void> saveGatewayAndApplyInternal(
AppController controller,
SettingsSnapshot settings,
@ -489,14 +470,6 @@ XWorkmate Privacy Policy
await handleTopLevelApplyInternal(controller);
}
Future<void> saveAiGatewayAndPersistInternal(
AppController controller,
SettingsSnapshot settings,
) async {
await saveAiGatewayDraftInternal(controller, settings);
await handleTopLevelSaveInternal(controller);
}
Future<void> saveAiGatewayAndApplyInternal(
AppController controller,
SettingsSnapshot settings,
@ -674,10 +647,8 @@ XWorkmate Privacy Policy
Widget buildSettingsSectionActionsInternal({
required AppController controller,
required Key testKey,
required Key saveKey,
required Key applyKey,
required Future<void> Function() onTest,
required Future<void> Function() onSave,
required Future<void> Function() onApply,
bool testing = false,
String? testLabel,
@ -695,15 +666,10 @@ XWorkmate Privacy Policy
: (testLabel ?? appText('测试连接', 'Test Connection')),
),
),
OutlinedButton(
key: saveKey,
onPressed: () => onSave(),
child: Text(appText('保存', 'Save')),
),
FilledButton.tonal(
key: applyKey,
onPressed: () => onApply(),
child: Text(appText('应用', 'Apply')),
child: Text(appText('保存并生效', 'Save & apply')),
),
],
);

View File

@ -96,8 +96,8 @@ class ConversationWorkspaceInternal extends StatelessWidget {
'This thread is not ready. Check Single Agent configuration, or switch to a connected gateway target.',
)
: appText(
'当前线程目标网关未连接。请先在 Settings 中 Test / Save / Apply。',
'The gateway target for this thread is offline. Use Test / Save / Apply in Settings first.',
'当前线程目标网关未连接。请先在 Settings 中测试并保存生效。',
'The gateway target for this thread is offline. Test it in Settings and save it into effect first.',
),
),
),

View File

@ -155,31 +155,6 @@ extension WebSettingsPageGatewayMixinInternal on WebSettingsPageStateInternal {
},
child: Text(appText('测试连接', 'Test')),
),
FilledButton(
onPressed: controller.aiGatewayBusy
? null
: () async {
await controller.saveAiGatewayConfiguration(
name: directNameControllerInternal.text,
baseUrl: directBaseUrlControllerInternal.text,
provider:
directProviderControllerInternal.text,
apiKey: directApiKeyControllerInternal.text,
defaultModel:
controller.resolvedAiGatewayModel,
);
if (!mounted) {
return;
}
setStateInternal(() {
directMessageInternal = appText(
'配置已保存,尚未同步模型目录。',
'Configuration saved; model catalog not synced yet.',
);
});
},
child: Text(appText('保存', 'Save')),
),
FilledButton.icon(
onPressed: controller.aiGatewayBusy
? null
@ -229,7 +204,7 @@ extension WebSettingsPageGatewayMixinInternal on WebSettingsPageStateInternal {
),
)
: const Icon(Icons.play_circle_outline_rounded),
label: Text(appText('应用', 'Apply')),
label: Text(appText('保存并生效', 'Save & apply')),
),
],
),
@ -387,11 +362,6 @@ extension WebSettingsPageGatewayMixinInternal on WebSettingsPageStateInternal {
);
}
Future<void> handleTopLevelSaveInternal(AppController controller) async {
stageExternalAcpDraftInternal(controller);
await controller.persistSettingsDraft();
}
Future<void> handleTopLevelApplyInternal(AppController controller) async {
stageExternalAcpDraftInternal(controller);
await controller.applySettingsDraft();
@ -684,33 +654,6 @@ extension WebSettingsPageGatewayMixinInternal on WebSettingsPageStateInternal {
},
child: Text(appText('Test', 'Test')),
),
FilledButton(
onPressed: controller.relayBusy
? null
: () async {
await controller.saveRelayConfiguration(
profileIndex: profileIndex,
host: hostController.text,
port: parsePortInternal(
portController.text,
fallback: 443,
),
tls: tls,
token: tokenController.text,
password: passwordController.text,
);
if (!mounted) {
return;
}
onMessageChanged(
appText(
'配置已保存,尚未应用到当前线程连接。',
'Configuration saved but not applied to active thread connections yet.',
),
);
},
child: Text(appText('Save', 'Save')),
),
FilledButton.icon(
onPressed: controller.relayBusy
? null
@ -750,7 +693,7 @@ extension WebSettingsPageGatewayMixinInternal on WebSettingsPageStateInternal {
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.play_circle_outline_rounded),
label: Text(appText('Apply', 'Apply')),
label: Text(appText('保存并生效', 'Save & apply')),
),
],
),

View File

@ -44,13 +44,13 @@ extension WebSettingsPageSectionsMixinInternal on WebSettingsPageStateInternal {
? message
: hasDraft
? appText(
'当前存在未保存草稿。保存:仅保存配置,不立即生效。',
'There are unsaved drafts. Save persists configuration only and does not apply it immediately.',
'当前存在未保存草稿。保存并生效:按当前配置立即更新。',
'There are unsaved drafts. Save & apply updates the current configuration immediately.',
)
: hasPendingApply
? appText(
'当前存在已保存但未应用的更改。应用:立即按当前配置生效。',
'There are saved changes waiting to be applied. Apply makes the current configuration take effect immediately.',
'当前存在待生效更改。保存并生效:立即按当前配置更新。',
'There are saved changes waiting to be applied. Save & apply updates the current configuration immediately.',
)
: appText(
'当前没有待提交更改。',
@ -65,16 +65,6 @@ extension WebSettingsPageSectionsMixinInternal on WebSettingsPageStateInternal {
spacing: 10,
runSpacing: 10,
children: [
OutlinedButton(
key: const ValueKey('settings-global-save-button'),
onPressed:
hasDraft ||
gatewaySubTabInternal ==
WebGatewaySettingsSubTabInternal.acp
? () => handleTopLevelSaveInternal(controller)
: null,
child: Text(appText('保存', 'Save')),
),
FilledButton.tonal(
key: const ValueKey('settings-global-apply-button'),
onPressed:
@ -84,7 +74,7 @@ extension WebSettingsPageSectionsMixinInternal on WebSettingsPageStateInternal {
WebGatewaySettingsSubTabInternal.acp)
? () => handleTopLevelApplyInternal(controller)
: null,
child: Text(appText('应用', 'Apply')),
child: Text(appText('保存并生效', 'Save & apply')),
),
],
),
@ -234,8 +224,8 @@ extension WebSettingsPageSectionsMixinInternal on WebSettingsPageStateInternal {
const SizedBox(height: 8),
Text(
appText(
'这里维护 Local / Remote Gateway 与浏览器会话持久化配置。保存:仅保存配置,不立即生效。应用:立即按当前配置生效。',
'Maintain Local / Remote Gateway and browser session persistence here. Save persists configuration only, while Apply makes it take effect immediately.',
'这里维护 Local / Remote Gateway 与浏览器会话持久化配置。保存并生效:立即按当前配置更新。',
'Maintain Local / Remote Gateway and browser session persistence here. Save & apply updates the active configuration immediately.',
),
),
],
@ -363,24 +353,6 @@ extension WebSettingsPageSectionsMixinInternal on WebSettingsPageStateInternal {
spacing: 10,
runSpacing: 10,
children: [
FilledButton(
onPressed: () async {
await controller.saveWebSessionPersistenceConfiguration(
mode: sessionPersistenceModeInternal,
remoteBaseUrl:
sessionRemoteBaseUrlControllerInternal.text,
apiToken: sessionApiTokenControllerInternal.text,
);
if (!mounted) {
return;
}
setStateInternal(() {
sessionPersistenceMessageInternal =
controller.sessionPersistenceStatusMessage;
});
},
child: Text(appText('Save', 'Save')),
),
FilledButton.tonal(
onPressed: () async {
await controller.saveWebSessionPersistenceConfiguration(
@ -394,12 +366,12 @@ extension WebSettingsPageSectionsMixinInternal on WebSettingsPageStateInternal {
}
setStateInternal(() {
sessionPersistenceMessageInternal = appText(
'会话存储配置已应用到当前浏览器会话。',
'Session persistence settings are now applied to this browser session.',
'会话存储配置已保存并生效。',
'Session persistence settings are saved and applied.',
);
});
},
child: Text(appText('Apply', 'Apply')),
child: Text(appText('保存并生效', 'Save & apply')),
),
],
),

View File

@ -92,7 +92,7 @@ void main() {
);
expect(
find.byKey(const ValueKey('ai-gateway-save-button')),
findsOneWidget,
findsNothing,
);
expect(
find.byKey(const ValueKey('ai-gateway-apply-button')),
@ -113,24 +113,16 @@ void main() {
);
expect(controller.settings.aiGateway.baseUrl, isEmpty);
final saveButton = tester.widget<OutlinedButton>(
find.byKey(const ValueKey('ai-gateway-save-button')),
);
await tester.runAsync(() async {
saveButton.onPressed!.call();
await _waitFor(() => controller.hasPendingSettingsApply);
});
await tester.pump(const Duration(milliseconds: 300));
expect(controller.hasPendingSettingsApply, isTrue);
expect(controller.settings.aiGateway.baseUrl, 'https://api.svc.plus/v1');
final applyButton = tester.widget<FilledButton>(
find.byKey(const ValueKey('ai-gateway-apply-button')),
);
await tester.runAsync(() async {
applyButton.onPressed!.call();
await _waitFor(() => !controller.hasPendingSettingsApply);
await _waitFor(
() =>
controller.settings.aiGateway.baseUrl ==
'https://api.svc.plus/v1',
);
});
await tester.pump(const Duration(milliseconds: 300));

View File

@ -248,8 +248,9 @@ void main() {
expect(find.byKey(const ValueKey('gateway-mode-field')), findsNothing);
expect(find.text('认证诊断'), findsNothing);
expect(find.byKey(const ValueKey('gateway-test-button')), findsOneWidget);
expect(find.byKey(const ValueKey('gateway-save-button')), findsOneWidget);
expect(find.byKey(const ValueKey('gateway-save-button')), findsNothing);
expect(find.byKey(const ValueKey('gateway-apply-button')), findsOneWidget);
expect(find.text('保存并生效'), findsWidgets);
expect(
find.byKey(const ValueKey('gateway-profile-chip-0')),
findsOneWidget,
@ -304,7 +305,7 @@ void main() {
find.textContaining('VAULT_SERVER_ROOT_ACCESS_TOKEN'),
findsOneWidget,
);
expect(find.byKey(const ValueKey('vault-save-button')), findsOneWidget);
expect(find.byKey(const ValueKey('vault-save-button')), findsNothing);
expect(find.byKey(const ValueKey('vault-apply-button')), findsOneWidget);
});