Flatten settings navigation into unified overview
This commit is contained in:
parent
fb248928a1
commit
f2e1cb2609
@ -222,7 +222,16 @@ class _AppShellState extends State<AppShell> {
|
||||
sidebarState: sidebarState,
|
||||
appLanguage: controller.appLanguage,
|
||||
themeMode: controller.themeMode,
|
||||
onSectionChanged: controller.navigateTo,
|
||||
onSectionChanged: (destination) {
|
||||
if (destination ==
|
||||
WorkspaceDestination.settings) {
|
||||
controller.openSettings(
|
||||
tab: SettingsTab.general,
|
||||
);
|
||||
return;
|
||||
}
|
||||
controller.navigateTo(destination);
|
||||
},
|
||||
onToggleLanguage: controller.toggleAppLanguage,
|
||||
onCycleSidebarState: controller.cycleSidebarState,
|
||||
onExpandFromCollapsed: () => controller
|
||||
|
||||
@ -152,7 +152,13 @@ class _AppShellState extends State<AppShell> {
|
||||
sidebarState: _sidebarState,
|
||||
appLanguage: controller.appLanguage,
|
||||
themeMode: controller.themeMode,
|
||||
onSectionChanged: controller.navigateTo,
|
||||
onSectionChanged: (destination) {
|
||||
if (destination == WorkspaceDestination.settings) {
|
||||
controller.openSettings(tab: SettingsTab.general);
|
||||
return;
|
||||
}
|
||||
controller.navigateTo(destination);
|
||||
},
|
||||
onToggleLanguage: controller.toggleAppLanguage,
|
||||
onCycleSidebarState: _cycleSidebarState,
|
||||
onExpandFromCollapsed: () {
|
||||
@ -238,7 +244,10 @@ class _AppShellState extends State<AppShell> {
|
||||
WorkspaceDestination.aiGateway => WebAiGatewayPage(
|
||||
controller: controller,
|
||||
),
|
||||
WorkspaceDestination.settings => WebSettingsPage(controller: controller),
|
||||
WorkspaceDestination.settings => WebSettingsPage(
|
||||
controller: controller,
|
||||
showSectionTabs: false,
|
||||
),
|
||||
_ => WebAssistantPage(controller: controller),
|
||||
};
|
||||
}
|
||||
|
||||
@ -127,12 +127,14 @@ workspacePageSpecsInternal = <WorkspaceDestination, WorkspacePageSpec>{
|
||||
initialTab: controller.settingsTab,
|
||||
initialDetail: controller.settingsDetail,
|
||||
navigationContext: controller.settingsNavigationContext,
|
||||
showSectionTabs: false,
|
||||
),
|
||||
mobileBuilder: (controller, onOpenDetail) => SettingsPage(
|
||||
controller: controller,
|
||||
initialTab: controller.settingsTab,
|
||||
initialDetail: controller.settingsDetail,
|
||||
navigationContext: controller.settingsNavigationContext,
|
||||
showSectionTabs: false,
|
||||
),
|
||||
),
|
||||
WorkspaceDestination.account: WorkspacePageSpec(
|
||||
|
||||
@ -36,12 +36,14 @@ class SettingsPage extends StatefulWidget {
|
||||
this.initialTab = SettingsTab.general,
|
||||
this.initialDetail,
|
||||
this.navigationContext,
|
||||
this.showSectionTabs = false,
|
||||
});
|
||||
|
||||
final AppController controller;
|
||||
final SettingsTab initialTab;
|
||||
final SettingsDetailPage? initialDetail;
|
||||
final SettingsNavigationContext? navigationContext;
|
||||
final bool showSectionTabs;
|
||||
|
||||
@override
|
||||
State<SettingsPage> createState() => SettingsPageStateInternal();
|
||||
@ -214,8 +216,11 @@ class SettingsPageStateInternal extends State<SettingsPage> {
|
||||
final settings = controller.settingsDraft;
|
||||
final showingDetail = detailInternal != null;
|
||||
final showGlobalApplyBar =
|
||||
tabInternal != SettingsTab.gateway ||
|
||||
integrationSubTabInternal == GatewayIntegrationSubTabInternal.acp;
|
||||
!showingDetail &&
|
||||
(!widget.showSectionTabs ||
|
||||
tabInternal != SettingsTab.gateway ||
|
||||
integrationSubTabInternal ==
|
||||
GatewayIntegrationSubTabInternal.acp);
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(32, 32, 32, 8),
|
||||
child: Column(
|
||||
@ -265,7 +270,7 @@ class SettingsPageStateInternal extends State<SettingsPage> {
|
||||
buildGlobalApplyBarInternal(context, controller),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
if (!showingDetail) ...[
|
||||
if (!showingDetail && widget.showSectionTabs) ...[
|
||||
SectionTabs(
|
||||
items: availableTabs.map((item) => item.label).toList(),
|
||||
value: tabInternal.label,
|
||||
@ -280,12 +285,26 @@ class SettingsPageStateInternal extends State<SettingsPage> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
...buildContentForCurrentStateInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
uiFeatures,
|
||||
),
|
||||
...(showingDetail
|
||||
? buildContentForCurrentStateInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
uiFeatures,
|
||||
)
|
||||
: widget.showSectionTabs
|
||||
? buildContentForCurrentStateInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
uiFeatures,
|
||||
)
|
||||
: buildOverviewContentInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
uiFeatures,
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@ -34,6 +34,14 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal {
|
||||
SettingsSnapshot settings,
|
||||
UiFeatureAccess uiFeatures,
|
||||
) {
|
||||
if (!widget.showSectionTabs) {
|
||||
return buildUnifiedGatewaySectionsInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
uiFeatures,
|
||||
);
|
||||
}
|
||||
final tabLabel = switch (integrationSubTabInternal) {
|
||||
GatewayIntegrationSubTabInternal.gateway => 'OpenClaw Gateway',
|
||||
GatewayIntegrationSubTabInternal.vault => appText(
|
||||
@ -147,6 +155,59 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal {
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> buildUnifiedGatewaySectionsInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
UiFeatureAccess uiFeatures,
|
||||
) {
|
||||
return [
|
||||
buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: 'OpenClaw Gateway',
|
||||
expanded: openClawGatewayExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
openClawGatewayExpandedInternal = value;
|
||||
}),
|
||||
child: buildOpenClawGatewayCardInternal(context, controller, settings),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (uiFeatures.supportsVaultServer)
|
||||
buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: appText('Vault Server', 'Vault Server'),
|
||||
expanded: vaultServerExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
vaultServerExpandedInternal = value;
|
||||
}),
|
||||
child: buildVaultProviderCardInternal(context, controller, settings),
|
||||
)
|
||||
else
|
||||
SurfaceCard(
|
||||
child: Text(
|
||||
appText(
|
||||
'当前发布配置未开放 Vault Server 参数。',
|
||||
'Vault Server settings are disabled in this release configuration.',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: appText('LLM 接入点', 'LLM Endpoints'),
|
||||
expanded: aiGatewayExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
aiGatewayExpandedInternal = value;
|
||||
}),
|
||||
child: buildLlmEndpointManagerInternal(context, controller, settings),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
buildExternalAcpEndpointManagerInternal(context, controller, settings),
|
||||
const SizedBox(height: 16),
|
||||
SkillDirectoryAuthorizationCard(controller: controller),
|
||||
];
|
||||
}
|
||||
|
||||
Widget buildExternalAcpEndpointManagerInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
|
||||
@ -137,23 +137,56 @@ extension SettingsPageGatewayConnectionMixinInternal
|
||||
if (selectedProfileIndex != kGatewayLocalProfileIndex &&
|
||||
!forceSetupCodeMode &&
|
||||
setupCodeFeatureEnabled) ...[
|
||||
SectionTabs(
|
||||
items: [appText('配置码', 'Setup Code'), appText('手动配置', 'Manual')],
|
||||
value: useSetupCode
|
||||
? appText('配置码', 'Setup Code')
|
||||
: appText('手动配置', 'Manual'),
|
||||
size: SectionTabsSize.small,
|
||||
onChanged: (value) {
|
||||
final nextUseSetupCode = value == appText('配置码', 'Setup Code');
|
||||
unawaited(
|
||||
saveGatewayProfileInternal(
|
||||
controller,
|
||||
settings,
|
||||
gatewayProfile.copyWith(useSetupCode: nextUseSetupCode),
|
||||
).catchError((_) {}),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (widget.showSectionTabs)
|
||||
SectionTabs(
|
||||
items: [
|
||||
appText('配置码', 'Setup Code'),
|
||||
appText('手动配置', 'Manual'),
|
||||
],
|
||||
value: useSetupCode
|
||||
? appText('配置码', 'Setup Code')
|
||||
: appText('手动配置', 'Manual'),
|
||||
size: SectionTabsSize.small,
|
||||
onChanged: (value) {
|
||||
final nextUseSetupCode = value == appText('配置码', 'Setup Code');
|
||||
unawaited(
|
||||
saveGatewayProfileInternal(
|
||||
controller,
|
||||
settings,
|
||||
gatewayProfile.copyWith(useSetupCode: nextUseSetupCode),
|
||||
).catchError((_) {}),
|
||||
);
|
||||
},
|
||||
)
|
||||
else
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
FilterChip(
|
||||
label: Text(appText('配置码', 'Setup Code')),
|
||||
selected: useSetupCode,
|
||||
onSelected: (_) => unawaited(
|
||||
saveGatewayProfileInternal(
|
||||
controller,
|
||||
settings,
|
||||
gatewayProfile.copyWith(useSetupCode: true),
|
||||
).catchError((_) {}),
|
||||
),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(appText('手动配置', 'Manual')),
|
||||
selected: !useSetupCode,
|
||||
onSelected: (_) => unawaited(
|
||||
saveGatewayProfileInternal(
|
||||
controller,
|
||||
settings,
|
||||
gatewayProfile.copyWith(useSetupCode: false),
|
||||
).catchError((_) {}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
if (selectedProfileIndex != kGatewayLocalProfileIndex &&
|
||||
|
||||
@ -28,6 +28,67 @@ import 'settings_page_device.dart';
|
||||
import 'settings_page_widgets.dart';
|
||||
|
||||
extension SettingsPageSectionsMixinInternal on SettingsPageStateInternal {
|
||||
List<SettingsTab> orderedOverviewTabsInternal(
|
||||
AppController controller,
|
||||
UiFeatureAccess uiFeatures,
|
||||
) {
|
||||
final availableTabs = uiFeatures.availableSettingsTabs;
|
||||
final current = uiFeatures.sanitizeSettingsTab(controller.settingsTab);
|
||||
return <SettingsTab>[
|
||||
current,
|
||||
...availableTabs.where((item) => item != current),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> buildOverviewContentInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
UiFeatureAccess uiFeatures,
|
||||
) {
|
||||
final orderedTabs = orderedOverviewTabsInternal(controller, uiFeatures);
|
||||
final sections = <Widget>[];
|
||||
for (final tab in orderedTabs) {
|
||||
final content = switch (tab) {
|
||||
SettingsTab.general => buildGeneralInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
uiFeatures,
|
||||
),
|
||||
SettingsTab.workspace => buildWorkspaceInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
SettingsTab.gateway => buildGatewayInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
uiFeatures,
|
||||
),
|
||||
SettingsTab.agents => buildAgentsInternal(context, controller, settings),
|
||||
SettingsTab.appearance => buildAppearanceInternal(context, controller),
|
||||
SettingsTab.diagnostics => buildDiagnosticsInternal(context, controller),
|
||||
SettingsTab.experimental => buildExperimentalInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
uiFeatures,
|
||||
),
|
||||
SettingsTab.about => buildAboutInternal(context, controller),
|
||||
};
|
||||
if (content.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
if (sections.isNotEmpty) {
|
||||
sections.add(const SizedBox(height: 24));
|
||||
}
|
||||
sections.addAll(content);
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
List<Widget> buildContentForCurrentStateInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
|
||||
@ -18,9 +18,14 @@ import 'web_settings_page_gateway.dart';
|
||||
import 'web_settings_page_support.dart';
|
||||
|
||||
class WebSettingsPage extends StatefulWidget {
|
||||
const WebSettingsPage({super.key, required this.controller});
|
||||
const WebSettingsPage({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.showSectionTabs = false,
|
||||
});
|
||||
|
||||
final AppController controller;
|
||||
final bool showSectionTabs;
|
||||
|
||||
@override
|
||||
State<WebSettingsPage> createState() => WebSettingsPageStateInternal();
|
||||
@ -243,6 +248,7 @@ class WebSettingsPageStateInternal extends State<WebSettingsPage> {
|
||||
controller.settingsTab,
|
||||
);
|
||||
final showGlobalApplyBar =
|
||||
!widget.showSectionTabs ||
|
||||
currentTab != SettingsTab.gateway ||
|
||||
gatewaySubTabInternal == WebGatewaySettingsSubTabInternal.acp;
|
||||
return DesktopWorkspaceScaffold(
|
||||
@ -285,34 +291,33 @@ class WebSettingsPageStateInternal extends State<WebSettingsPage> {
|
||||
buildGlobalApplyBarInternal(context, controller),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
SectionTabs(
|
||||
items: availableTabs.map((item) => item.label).toList(),
|
||||
value: currentTab.label,
|
||||
onChanged: (label) {
|
||||
final tab = availableTabs.firstWhere(
|
||||
(item) => item.label == label,
|
||||
);
|
||||
controller.setSettingsTab(tab);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
...switch (currentTab) {
|
||||
SettingsTab.general => buildGeneralInternal(
|
||||
context,
|
||||
controller,
|
||||
controller.settingsDraft,
|
||||
if (widget.showSectionTabs) ...[
|
||||
SectionTabs(
|
||||
items: availableTabs.map((item) => item.label).toList(),
|
||||
value: currentTab.label,
|
||||
onChanged: (label) {
|
||||
final tab = availableTabs.firstWhere(
|
||||
(item) => item.label == label,
|
||||
);
|
||||
controller.setSettingsTab(tab);
|
||||
},
|
||||
),
|
||||
SettingsTab.gateway => buildGatewayInternal(
|
||||
context,
|
||||
controller,
|
||||
controller.settingsDraft,
|
||||
),
|
||||
SettingsTab.appearance => buildAppearanceInternal(
|
||||
context,
|
||||
controller,
|
||||
),
|
||||
_ => buildAboutInternal(context),
|
||||
},
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
...(widget.showSectionTabs
|
||||
? buildTabContentInternal(
|
||||
context,
|
||||
controller,
|
||||
controller.settingsDraft,
|
||||
currentTab,
|
||||
)
|
||||
: buildOverviewContentInternal(
|
||||
context,
|
||||
controller,
|
||||
controller.settingsDraft,
|
||||
availableTabs,
|
||||
currentTab,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -18,6 +18,45 @@ import 'web_settings_page_gateway.dart';
|
||||
import 'web_settings_page_support.dart';
|
||||
|
||||
extension WebSettingsPageSectionsMixinInternal on WebSettingsPageStateInternal {
|
||||
List<Widget> buildTabContentInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
SettingsTab tab,
|
||||
) {
|
||||
return switch (tab) {
|
||||
SettingsTab.general => buildGeneralInternal(context, controller, settings),
|
||||
SettingsTab.gateway => buildGatewayInternal(context, controller, settings),
|
||||
SettingsTab.appearance => buildAppearanceInternal(context, controller),
|
||||
_ => buildAboutInternal(context),
|
||||
};
|
||||
}
|
||||
|
||||
List<Widget> buildOverviewContentInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
List<SettingsTab> availableTabs,
|
||||
SettingsTab currentTab,
|
||||
) {
|
||||
final orderedTabs = <SettingsTab>[
|
||||
currentTab,
|
||||
...availableTabs.where((item) => item != currentTab),
|
||||
];
|
||||
final sections = <Widget>[];
|
||||
for (final tab in orderedTabs) {
|
||||
final content = buildTabContentInternal(context, controller, settings, tab);
|
||||
if (content.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
if (sections.isNotEmpty) {
|
||||
sections.add(const SizedBox(height: 24));
|
||||
}
|
||||
sections.addAll(content);
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
Widget buildGlobalApplyBarInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
@ -147,6 +186,15 @@ extension WebSettingsPageSectionsMixinInternal on WebSettingsPageStateInternal {
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
) {
|
||||
if (!widget.showSectionTabs) {
|
||||
return [
|
||||
...buildGatewayOverviewInternal(context, controller),
|
||||
const SizedBox(height: 16),
|
||||
...buildLlmEndpointManagerInternal(context, controller, settings),
|
||||
const SizedBox(height: 16),
|
||||
buildExternalAcpEndpointManagerInternal(context, controller),
|
||||
];
|
||||
}
|
||||
return [
|
||||
SectionTabs(
|
||||
items: <String>[
|
||||
|
||||
@ -8,6 +8,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:xworkmate/app/app_controller.dart';
|
||||
import 'package:xworkmate/features/settings/settings_page.dart';
|
||||
import 'package:xworkmate/models/app_models.dart';
|
||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
|
||||
import '../test_support.dart';
|
||||
@ -61,13 +62,9 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
controller.setSettingsTab(SettingsTab.gateway);
|
||||
await pumpPage(tester, child: SettingsPage(controller: controller));
|
||||
|
||||
await tester.tap(find.text('集成'));
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.tap(find.text('LLM 接入点'));
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('ai-gateway-name-field')),
|
||||
'default',
|
||||
@ -104,7 +101,7 @@ void main() {
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey('settings-global-apply-button')),
|
||||
findsNothing,
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
expect(
|
||||
|
||||
@ -10,6 +10,7 @@ import 'package:xworkmate/models/app_models.dart';
|
||||
import 'package:xworkmate/runtime/desktop_platform_service.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
import 'package:xworkmate/runtime/skill_directory_access.dart';
|
||||
import 'package:xworkmate/widgets/section_tabs.dart';
|
||||
|
||||
import '../test_support.dart';
|
||||
|
||||
@ -133,20 +134,36 @@ class _FakeSkillDirectoryAccessService implements SkillDirectoryAccessService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pumpSettingsPage(
|
||||
WidgetTester tester,
|
||||
AppController controller, {
|
||||
SettingsTab tab = SettingsTab.general,
|
||||
TargetPlatform platform = TargetPlatform.macOS,
|
||||
}) async {
|
||||
controller.setSettingsTab(tab);
|
||||
await pumpPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: platform,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _ensureVisible(WidgetTester tester, Finder finder) async {
|
||||
await tester.ensureVisible(finder.first);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('SettingsPage theme chips update controller theme mode', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final controller = await createTestController(tester);
|
||||
|
||||
await pumpPage(
|
||||
await _pumpSettingsPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
controller,
|
||||
tab: SettingsTab.appearance,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('外观'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('深色'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@ -204,33 +221,52 @@ void main() {
|
||||
(WidgetTester tester) async {
|
||||
final controller = await createTestController(tester);
|
||||
|
||||
await pumpPage(
|
||||
await _pumpSettingsPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
controller,
|
||||
tab: SettingsTab.workspace,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('工作区'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('远程项目根目录'), findsNothing);
|
||||
expect(find.text('Remote Project Root'), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('SettingsPage workspace edits enable the top save-and-apply flow', (
|
||||
testWidgets('SettingsPage can render as a unified page without internal tabs', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final controller = await createTestController(tester);
|
||||
|
||||
await pumpPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
child: SettingsPage(
|
||||
controller: controller,
|
||||
showSectionTabs: false,
|
||||
),
|
||||
platform: TargetPlatform.macOS,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('工作区'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(SectionTabs), findsNothing);
|
||||
expect(find.text('Application'), findsOneWidget);
|
||||
expect(find.text('工作区'), findsWidgets);
|
||||
expect(find.text('OpenClaw Gateway'), findsWidgets);
|
||||
expect(find.text('LLM 接入点'), findsWidgets);
|
||||
expect(
|
||||
find.byKey(const ValueKey('external-acp-provider-add-button')),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('SettingsPage workspace edits enable the top save-and-apply flow', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final controller = await createTestController(tester);
|
||||
|
||||
await _pumpSettingsPage(
|
||||
tester,
|
||||
controller,
|
||||
tab: SettingsTab.workspace,
|
||||
);
|
||||
|
||||
await tester.enterText(
|
||||
find.byType(TextFormField).first,
|
||||
@ -252,24 +288,15 @@ void main() {
|
||||
) async {
|
||||
final controller = await createTestController(tester);
|
||||
|
||||
await pumpPage(
|
||||
await _pumpSettingsPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
controller,
|
||||
tab: SettingsTab.gateway,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('集成'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('OpenClaw Gateway'), findsWidgets);
|
||||
expect(find.text('LLM 接入点'), findsOneWidget);
|
||||
expect(find.text('ACP 外部接入'), findsOneWidget);
|
||||
expect(find.text('Vault Server'), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('vault-server-url-field')), findsNothing);
|
||||
expect(
|
||||
find.byKey(const ValueKey('vault-root-access-token-field')),
|
||||
findsNothing,
|
||||
);
|
||||
expect(find.text('Vault Server'), findsAtLeastNWidgets(1));
|
||||
expect(find.byKey(const ValueKey('gateway-test-button')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('gateway-save-button')), findsNothing);
|
||||
expect(find.byKey(const ValueKey('gateway-apply-button')), findsOneWidget);
|
||||
@ -306,9 +333,6 @@ void main() {
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Vault Server').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byKey(const ValueKey('vault-server-url-field')),
|
||||
findsOneWidget,
|
||||
@ -317,9 +341,13 @@ void main() {
|
||||
find.byKey(const ValueKey('vault-root-access-token-field')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byKey(const ValueKey('ai-gateway-url-field')), findsNothing);
|
||||
expect(find.byKey(const ValueKey('ai-gateway-url-field')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('gateway-mode-field')), findsNothing);
|
||||
expect(find.text('认证诊断'), findsNothing);
|
||||
expect(find.text('认证诊断'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey('external-acp-provider-add-button')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('保存并生效'), findsWidgets);
|
||||
});
|
||||
|
||||
@ -328,18 +356,13 @@ void main() {
|
||||
) async {
|
||||
final controller = await createTestController(tester);
|
||||
|
||||
await pumpPage(
|
||||
await _pumpSettingsPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
controller,
|
||||
tab: SettingsTab.gateway,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('集成'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Vault Server').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Vault Server'), findsWidgets);
|
||||
expect(find.text('Vault Server'), findsAtLeastNWidgets(1));
|
||||
expect(find.text('VAULT_SERVER_URL'), findsOneWidget);
|
||||
expect(
|
||||
find.textContaining('VAULT_SERVER_ROOT_ACCESS_TOKEN'),
|
||||
@ -354,17 +377,12 @@ void main() {
|
||||
) async {
|
||||
final controller = await createTestController(tester);
|
||||
|
||||
await pumpPage(
|
||||
await _pumpSettingsPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
controller,
|
||||
tab: SettingsTab.gateway,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('集成'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('ACP 外部接入').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('外部 ACP Server Endpoint'), findsOneWidget);
|
||||
expect(find.textContaining('Codex'), findsWidgets);
|
||||
expect(find.textContaining('OpenCode'), findsWidgets);
|
||||
@ -386,7 +404,7 @@ void main() {
|
||||
find.byKey(const ValueKey('settings-global-apply-button')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('保存并生效'), findsOneWidget);
|
||||
expect(find.text('保存并生效'), findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('SettingsPage ACP wizard adds a custom provider card', (
|
||||
@ -394,17 +412,16 @@ void main() {
|
||||
) async {
|
||||
final controller = await createTestController(tester);
|
||||
|
||||
await pumpPage(
|
||||
await _pumpSettingsPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
controller,
|
||||
tab: SettingsTab.gateway,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('集成'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('ACP 外部接入').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await _ensureVisible(
|
||||
tester,
|
||||
find.byKey(const ValueKey('external-acp-provider-add-button')),
|
||||
);
|
||||
await tester.tap(
|
||||
find.byKey(const ValueKey('external-acp-provider-add-button')),
|
||||
);
|
||||
@ -435,17 +452,12 @@ void main() {
|
||||
_FakeSkillDirectoryAccessService(userHomeDirectory: '/Users/tester'),
|
||||
);
|
||||
|
||||
await pumpPage(
|
||||
await _pumpSettingsPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
controller,
|
||||
tab: SettingsTab.gateway,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('集成'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('SKILLS 目录授权'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('~/.agents/skills'), findsOneWidget);
|
||||
expect(find.text('/Users/tester/.agents/skills'), findsOneWidget);
|
||||
expect(find.text('~/.codex/skills'), findsOneWidget);
|
||||
@ -474,16 +486,15 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await pumpPage(
|
||||
await _pumpSettingsPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
controller,
|
||||
tab: SettingsTab.gateway,
|
||||
);
|
||||
await _ensureVisible(
|
||||
tester,
|
||||
find.byKey(const ValueKey('skill-directory-batch-add-button')),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('集成'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('SKILLS 目录授权'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.byKey(const ValueKey('skill-directory-batch-add-button')),
|
||||
);
|
||||
@ -536,16 +547,15 @@ paths:
|
||||
),
|
||||
);
|
||||
|
||||
await pumpPage(
|
||||
await _pumpSettingsPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
controller,
|
||||
tab: SettingsTab.gateway,
|
||||
);
|
||||
await _ensureVisible(
|
||||
tester,
|
||||
find.byKey(const ValueKey('skill-directory-batch-add-button')),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('集成'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('SKILLS 目录授权'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.byKey(const ValueKey('skill-directory-batch-add-button')),
|
||||
);
|
||||
@ -581,16 +591,15 @@ paths:
|
||||
_FakeSkillDirectoryAccessService(userHomeDirectory: '/Users/tester'),
|
||||
);
|
||||
|
||||
await pumpPage(
|
||||
await _pumpSettingsPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
controller,
|
||||
tab: SettingsTab.gateway,
|
||||
);
|
||||
await _ensureVisible(
|
||||
tester,
|
||||
find.byKey(const ValueKey('skill-directory-batch-add-button')),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('集成'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('SKILLS 目录授权'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.byKey(const ValueKey('skill-directory-batch-add-button')),
|
||||
);
|
||||
@ -627,15 +636,12 @@ paths:
|
||||
) async {
|
||||
final controller = await createTestController(tester);
|
||||
|
||||
await pumpPage(
|
||||
await _pumpSettingsPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
controller,
|
||||
tab: SettingsTab.gateway,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('集成'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byTooltip('折叠').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@ -665,11 +671,7 @@ paths:
|
||||
desktopPlatformService: _DesktopServiceStub(),
|
||||
);
|
||||
|
||||
await pumpPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
);
|
||||
await _pumpSettingsPage(tester, controller);
|
||||
|
||||
expect(
|
||||
find.byKey(const ValueKey('linux-desktop-integration-card')),
|
||||
@ -700,6 +702,7 @@ paths:
|
||||
child: const SizedBox(width: 1100, height: 900, child: Placeholder()),
|
||||
platform: TargetPlatform.macOS,
|
||||
);
|
||||
controller.setSettingsTab(SettingsTab.agents);
|
||||
await pumpPage(
|
||||
tester,
|
||||
child: SizedBox(
|
||||
@ -710,9 +713,6 @@ paths:
|
||||
platform: TargetPlatform.macOS,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('多 Agent'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final titleFinder = find.text('多 Agent 协作');
|
||||
expect(titleFinder, findsOneWidget);
|
||||
expect(tester.getSize(titleFinder).width, greaterThan(80));
|
||||
@ -728,14 +728,11 @@ paths:
|
||||
) async {
|
||||
final controller = await createTestController(tester);
|
||||
|
||||
await pumpPage(
|
||||
await _pumpSettingsPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
controller,
|
||||
tab: SettingsTab.gateway,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('集成'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const ValueKey('gateway-profile-chip-1')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@ -762,10 +759,12 @@ paths:
|
||||
message: 'pairing required',
|
||||
);
|
||||
|
||||
await pumpPage(tester, child: SettingsPage(controller: controller));
|
||||
|
||||
await tester.tap(find.text('诊断'));
|
||||
await tester.pumpAndSettle();
|
||||
await _pumpSettingsPage(
|
||||
tester,
|
||||
controller,
|
||||
tab: SettingsTab.diagnostics,
|
||||
platform: TargetPlatform.android,
|
||||
);
|
||||
|
||||
expect(find.byKey(const ValueKey('runtime-log-card')), findsOneWidget);
|
||||
expect(find.textContaining('connected remote gateway'), findsOneWidget);
|
||||
@ -807,11 +806,7 @@ paths:
|
||||
uiFeatureManifest: manifest,
|
||||
);
|
||||
|
||||
await pumpPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
);
|
||||
await _pumpSettingsPage(tester, controller);
|
||||
|
||||
expect(find.text('诊断'), findsNothing);
|
||||
expect(find.text('实验特性'), findsNothing);
|
||||
@ -822,10 +817,11 @@ paths:
|
||||
(WidgetTester tester) async {
|
||||
final controller = await createTestController(tester);
|
||||
|
||||
await pumpPage(tester, child: SettingsPage(controller: controller));
|
||||
|
||||
await tester.tap(find.text('诊断'));
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await _pumpSettingsPage(
|
||||
tester,
|
||||
controller,
|
||||
tab: SettingsTab.diagnostics,
|
||||
);
|
||||
|
||||
expect(
|
||||
find.byKey(const ValueKey('assistant-local-state-card')),
|
||||
@ -889,6 +885,33 @@ paths:
|
||||
expect(find.text('搜索设置'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Sidebar settings entry resets to general overview', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final controller = await createTestController(tester);
|
||||
controller.openSettings(tab: SettingsTab.workspace);
|
||||
|
||||
controller.navigateTo(WorkspaceDestination.assistant);
|
||||
controller.openSettings(tab: SettingsTab.general);
|
||||
|
||||
expect(controller.destination, WorkspaceDestination.settings);
|
||||
expect(controller.settingsTab, SettingsTab.general);
|
||||
|
||||
await pumpPage(
|
||||
tester,
|
||||
child: SettingsPage(
|
||||
controller: controller,
|
||||
initialTab: controller.settingsTab,
|
||||
initialDetail: controller.settingsDetail,
|
||||
navigationContext: controller.settingsNavigationContext,
|
||||
),
|
||||
platform: TargetPlatform.macOS,
|
||||
);
|
||||
|
||||
expect(find.byType(SectionTabs), findsNothing);
|
||||
expect(find.text('Application'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('SettingsPage expands optional LLM endpoints with add button', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
|
||||
@ -8,6 +8,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:xworkmate/app/app_controller.dart';
|
||||
import 'package:xworkmate/features/settings/settings_page.dart';
|
||||
import 'package:xworkmate/models/app_models.dart';
|
||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
|
||||
import '../test_support.dart';
|
||||
@ -40,17 +41,13 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
controller.setSettingsTab(SettingsTab.gateway);
|
||||
await pumpPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('集成'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Vault Server').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byKey(const ValueKey('vault-server-url-field')),
|
||||
findsOneWidget,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user