refactor: view selector (#7891)

* refactor: extract common view selection logic

* refactor: extract common component

* chore: make selected section selectable

* chore: max num of selected top-level pages
This commit is contained in:
Richard Shiue 2025-05-07 21:10:23 +08:00 committed by GitHub
parent 38839b4255
commit b1d3553110
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 406 additions and 394 deletions

View File

@ -5,6 +5,7 @@ export 'service/appflowy_ai_service.dart';
export 'service/error.dart';
export 'service/ai_model_state_notifier.dart';
export 'service/select_model_bloc.dart';
export 'service/view_selector_cubit.dart';
export 'widgets/loading_indicator.dart';
export 'widgets/prompt_input/action_buttons.dart';
export 'widgets/prompt_input/desktop_prompt_input.dart';

View File

@ -6,13 +6,12 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_select_sources_cubit.freezed.dart';
part 'view_selector_cubit.freezed.dart';
const int _kMaxSelectedParentPageCount = 3;
enum SourceSelectedStatus {
enum ViewSelectedStatus {
unselected,
selected,
partiallySelected;
@ -22,45 +21,46 @@ enum SourceSelectedStatus {
bool get isPartiallySelected => this == partiallySelected;
}
class ChatSource {
ChatSource({
class ViewSelectorItem {
ViewSelectorItem({
required this.view,
required this.parentView,
required this.children,
required bool isExpanded,
required SourceSelectedStatus selectedStatus,
required IgnoreViewType ignoreStatus,
required ViewSelectedStatus selectedStatus,
required bool isDisabled,
}) : isExpandedNotifier = ValueNotifier(isExpanded),
selectedStatusNotifier = ValueNotifier(selectedStatus),
ignoreStatusNotifier = ValueNotifier(ignoreStatus);
isDisabledNotifier = ValueNotifier(isDisabled);
final ViewPB view;
final ViewPB? parentView;
final List<ChatSource> children;
final List<ViewSelectorItem> children;
final ValueNotifier<bool> isExpandedNotifier;
final ValueNotifier<SourceSelectedStatus> selectedStatusNotifier;
final ValueNotifier<IgnoreViewType> ignoreStatusNotifier;
final ValueNotifier<bool> isDisabledNotifier;
final ValueNotifier<ViewSelectedStatus> selectedStatusNotifier;
bool get isExpanded => isExpandedNotifier.value;
SourceSelectedStatus get selectedStatus => selectedStatusNotifier.value;
IgnoreViewType get ignoreStatus => ignoreStatusNotifier.value;
ViewSelectedStatus get selectedStatus => selectedStatusNotifier.value;
bool get isDisabled => isDisabledNotifier.value;
void toggleIsExpanded() {
isExpandedNotifier.value = !isExpanded;
isExpandedNotifier.value = !isExpandedNotifier.value;
}
ChatSource copy() {
return ChatSource(
ViewSelectorItem copy() {
return ViewSelectorItem(
view: view,
parentView: parentView,
children: children.map<ChatSource>((child) => child.copy()).toList(),
ignoreStatus: ignoreStatus,
isExpanded: isExpanded,
selectedStatus: selectedStatus,
children:
children.map<ViewSelectorItem>((child) => child.copy()).toList(),
isDisabled: isDisabledNotifier.value,
isExpanded: isExpandedNotifier.value,
selectedStatus: selectedStatusNotifier.value,
);
}
ChatSource? findChildBySourceId(String sourceId) {
ViewSelectorItem? findChildBySourceId(String sourceId) {
if (view.id == sourceId) {
return this;
}
@ -73,20 +73,11 @@ class ChatSource {
return null;
}
void resetIgnoreViewTypeRecursive() {
ignoreStatusNotifier.value = view.layout.isDocumentView
? IgnoreViewType.none
: IgnoreViewType.disable;
void setIsDisabledRecursive(bool Function(ViewPB) newIsDisabled) {
isDisabledNotifier.value = newIsDisabled(view);
for (final child in children) {
child.resetIgnoreViewTypeRecursive();
}
}
void updateIgnoreViewTypeRecursive(IgnoreViewType newIgnoreViewType) {
ignoreStatusNotifier.value = newIgnoreViewType;
for (final child in children) {
child.updateIgnoreViewTypeRecursive(newIgnoreViewType);
child.setIsDisabledRecursive(newIsDisabled);
}
}
@ -96,21 +87,25 @@ class ChatSource {
}
isExpandedNotifier.dispose();
selectedStatusNotifier.dispose();
ignoreStatusNotifier.dispose();
isDisabledNotifier.dispose();
}
}
class ChatSettingsCubit extends Cubit<ChatSettingsState> {
ChatSettingsCubit({
this.hideDisabled = false,
}) : super(ChatSettingsState.initial());
class ViewSelectorCubit extends Cubit<ViewSelectorState> {
ViewSelectorCubit({
required this.getIgnoreViewType,
this.maxSelectedParentPageCount,
}) : super(ViewSelectorState.initial()) {
filterTextController.addListener(onFilterChanged);
}
final bool hideDisabled;
final IgnoreViewType Function(ViewPB) getIgnoreViewType;
final int? maxSelectedParentPageCount;
final List<String> selectedSourceIds = [];
final List<ChatSource> sources = [];
final List<ChatSource> selectedSources = [];
String filter = '';
final List<ViewSelectorItem> sources = [];
final List<ViewSelectorItem> selectedSources = [];
final filterTextController = TextEditingController();
void updateSelectedSources(List<String> newSelectedSourceIds) {
selectedSourceIds.clear();
@ -118,7 +113,7 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
}
void refreshSources(List<ViewPB> spaceViews, ViewPB? currentSpace) async {
filter = "";
filterTextController.clear();
final newSources = await Future.wait(
spaceViews.map((view) => _recursiveBuild(view, null)),
@ -155,29 +150,30 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
..addAll(selected.map((e) => e.copy()));
}
Future<ChatSource> _recursiveBuild(ViewPB view, ViewPB? parentView) async {
SourceSelectedStatus selectedStatus = SourceSelectedStatus.unselected;
Future<ViewSelectorItem> _recursiveBuild(
ViewPB view,
ViewPB? parentView,
) async {
ViewSelectedStatus selectedStatus = ViewSelectedStatus.unselected;
final isThisSourceSelected = selectedSourceIds.contains(view.id);
final childrenViews =
await ViewBackendService.getChildViews(viewId: view.id).toNullable();
int selectedCount = 0;
final children = <ChatSource>[];
final children = <ViewSelectorItem>[];
if (childrenViews != null) {
for (final childView in childrenViews) {
if (childView.layout == ViewLayoutPB.Chat) {
if (getIgnoreViewType(childView) == IgnoreViewType.hide) {
continue;
}
if (childView.layout != ViewLayoutPB.Document && hideDisabled) {
continue;
}
final childChatSource = await _recursiveBuild(childView, view);
if (childChatSource.selectedStatus.isSelected) {
final childItem = await _recursiveBuild(childView, view);
if (childItem.selectedStatus.isSelected) {
selectedCount++;
}
children.add(childChatSource);
children.add(childItem);
}
final areAllChildrenSelectedOrNoChildren =
@ -186,47 +182,49 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
children.any((e) => !e.selectedStatus.isUnselected);
if (isThisSourceSelected && areAllChildrenSelectedOrNoChildren) {
selectedStatus = SourceSelectedStatus.selected;
selectedStatus = ViewSelectedStatus.selected;
} else if (isThisSourceSelected || isAnyChildNotUnselected) {
selectedStatus = SourceSelectedStatus.partiallySelected;
selectedStatus = ViewSelectedStatus.partiallySelected;
}
} else if (isThisSourceSelected) {
selectedStatus = SourceSelectedStatus.selected;
selectedStatus = ViewSelectedStatus.selected;
}
return ChatSource(
return ViewSelectorItem(
view: view,
parentView: parentView,
children: children,
ignoreStatus: view.layout.isDocumentView
? IgnoreViewType.none
: IgnoreViewType.disable,
isDisabled: getIgnoreViewType(view) == IgnoreViewType.disable,
isExpanded: false,
selectedStatus: selectedStatus,
);
}
void _restrictSelectionIfNecessary(List<ChatSource> sources) {
void _restrictSelectionIfNecessary(List<ViewSelectorItem> sources) {
if (maxSelectedParentPageCount == null) {
return;
}
for (final source in sources) {
source.resetIgnoreViewTypeRecursive();
source.setIsDisabledRecursive((view) {
return getIgnoreViewType(view) == IgnoreViewType.disable;
});
}
if (sources.where((e) => !e.selectedStatus.isUnselected).length >=
_kMaxSelectedParentPageCount) {
maxSelectedParentPageCount!) {
sources
.where((e) => e.selectedStatus == SourceSelectedStatus.unselected)
.where((e) => e.selectedStatus == ViewSelectedStatus.unselected)
.forEach(
(e) => e.updateIgnoreViewTypeRecursive(IgnoreViewType.disable),
(e) => e.setIsDisabledRecursive((_) => true),
);
}
}
void updateFilter(String filter) {
this.filter = filter;
void onFilterChanged() {
for (final source in state.visibleSources) {
source.dispose();
}
if (sources.isEmpty) {
emit(ChatSettingsState.initial());
emit(ViewSelectorState.initial());
} else {
final selected =
selectedSources.map(_buildSearchResults).nonNulls.toList();
@ -242,13 +240,13 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
}
/// traverse tree to build up search query
ChatSource? _buildSearchResults(ChatSource chatSource) {
final isVisible = chatSource.view.nameOrDefault
ViewSelectorItem? _buildSearchResults(ViewSelectorItem item) {
final isVisible = item.view.nameOrDefault
.toLowerCase()
.contains(filter.toLowerCase());
.contains(filterTextController.text.toLowerCase());
final childrenResults = <ChatSource>[];
for (final childSource in chatSource.children) {
final childrenResults = <ViewSelectorItem>[];
for (final childSource in item.children) {
final childResult = _buildSearchResults(childSource);
if (childResult != null) {
childrenResults.add(childResult);
@ -256,48 +254,50 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
}
return isVisible || childrenResults.isNotEmpty
? ChatSource(
view: chatSource.view,
parentView: chatSource.parentView,
? ViewSelectorItem(
view: item.view,
parentView: item.parentView,
children: childrenResults,
ignoreStatus: chatSource.ignoreStatus,
isExpanded: chatSource.isExpanded,
selectedStatus: chatSource.selectedStatus,
isDisabled: item.isDisabled,
isExpanded: item.isExpanded,
selectedStatus: item.selectedStatus,
)
: null;
}
/// traverse tree to build up selected sources
Iterable<ChatSource> _buildSelectedSources(ChatSource chatSource) {
final children = <ChatSource>[];
Iterable<ViewSelectorItem> _buildSelectedSources(
ViewSelectorItem item,
) {
final children = <ViewSelectorItem>[];
for (final childSource in chatSource.children) {
for (final childSource in item.children) {
children.addAll(_buildSelectedSources(childSource));
}
return selectedSourceIds.contains(chatSource.view.id)
return selectedSourceIds.contains(item.view.id)
? [
ChatSource(
view: chatSource.view,
parentView: chatSource.parentView,
ViewSelectorItem(
view: item.view,
parentView: item.parentView,
children: children,
ignoreStatus: chatSource.ignoreStatus,
selectedStatus: chatSource.selectedStatus,
isDisabled: item.isDisabled,
selectedStatus: item.selectedStatus,
isExpanded: true,
),
]
: children;
}
void toggleSelectedStatus(ChatSource chatSource) {
if (chatSource.view.isSpace) {
void toggleSelectedStatus(ViewSelectorItem item, bool isSelectedSection) {
if (item.view.isSpace) {
return;
}
final allIds = _recursiveGetSourceIds(chatSource);
final allIds = _recursiveGetSourceIds(item);
if (chatSource.selectedStatus.isUnselected ||
chatSource.selectedStatus.isPartiallySelected &&
!chatSource.view.layout.isDocumentView) {
if (item.selectedStatus.isUnselected ||
item.selectedStatus.isPartiallySelected &&
!item.view.layout.isDocumentView) {
for (final id in allIds) {
if (!selectedSourceIds.contains(id)) {
selectedSourceIds.add(id);
@ -311,13 +311,20 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
}
}
if (isSelectedSection) {
item.selectedStatusNotifier.value = item.selectedStatus.isUnselected ||
item.selectedStatus.isPartiallySelected
? ViewSelectedStatus.selected
: ViewSelectedStatus.unselected;
}
updateSelectedStatus();
}
List<String> _recursiveGetSourceIds(ChatSource chatSource) {
List<String> _recursiveGetSourceIds(ViewSelectorItem item) {
return [
if (chatSource.view.layout.isDocumentView) chatSource.view.id,
for (final childSource in chatSource.children)
if (item.view.layout.isDocumentView) item.view.id,
for (final childSource in item.children)
..._recursiveGetSourceIds(childSource),
];
}
@ -342,44 +349,42 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
);
}
SourceSelectedStatus _recursiveUpdateSelectedStatus(ChatSource chatSource) {
SourceSelectedStatus selectedStatus = SourceSelectedStatus.unselected;
ViewSelectedStatus _recursiveUpdateSelectedStatus(ViewSelectorItem item) {
ViewSelectedStatus selectedStatus = ViewSelectedStatus.unselected;
int selectedCount = 0;
for (final childSource in chatSource.children) {
for (final childSource in item.children) {
final childStatus = _recursiveUpdateSelectedStatus(childSource);
if (childStatus.isSelected) {
selectedCount++;
}
}
final isThisSourceSelected = selectedSourceIds.contains(chatSource.view.id);
final isThisSourceSelected = selectedSourceIds.contains(item.view.id);
final areAllChildrenSelectedOrNoChildren =
chatSource.children.length == selectedCount;
item.children.length == selectedCount;
final isAnyChildNotUnselected =
chatSource.children.any((e) => !e.selectedStatus.isUnselected);
item.children.any((e) => !e.selectedStatus.isUnselected);
if (isThisSourceSelected && areAllChildrenSelectedOrNoChildren) {
selectedStatus = SourceSelectedStatus.selected;
selectedStatus = ViewSelectedStatus.selected;
} else if (isThisSourceSelected || isAnyChildNotUnselected) {
selectedStatus = SourceSelectedStatus.partiallySelected;
selectedStatus = ViewSelectedStatus.partiallySelected;
}
chatSource.selectedStatusNotifier.value = selectedStatus;
item.selectedStatusNotifier.value = selectedStatus;
return selectedStatus;
}
void toggleIsExpanded(ChatSource chatSource, bool isSelectedSection) {
chatSource.toggleIsExpanded();
void toggleIsExpanded(ViewSelectorItem item, bool isSelectedSection) {
item.toggleIsExpanded();
if (isSelectedSection) {
for (final selectedSource in selectedSources) {
selectedSource
.findChildBySourceId(chatSource.view.id)
?.toggleIsExpanded();
selectedSource.findChildBySourceId(item.view.id)?.toggleIsExpanded();
}
} else {
for (final source in sources) {
final child = source.findChildBySourceId(chatSource.view.id);
final child = source.findChildBySourceId(item.view.id);
if (child != null) {
child.toggleIsExpanded();
break;
@ -402,18 +407,19 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
for (final child in state.visibleSources) {
child.dispose();
}
filterTextController.dispose();
return super.close();
}
}
@freezed
class ChatSettingsState with _$ChatSettingsState {
const factory ChatSettingsState({
required List<ChatSource> visibleSources,
required List<ChatSource> selectedSources,
}) = _ChatSettingsState;
class ViewSelectorState with _$ViewSelectorState {
const factory ViewSelectorState({
required List<ViewSelectorItem> visibleSources,
required List<ViewSelectorItem> selectedSources,
}) = _ViewSelectorState;
factory ChatSettingsState.initial() => const ChatSettingsState(
factory ViewSelectorState.initial() => const ViewSelectorState(
visibleSources: [],
selectedSources: [],
);

View File

@ -2,12 +2,13 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart';
import 'package:appflowy/ai/service/view_selector_cubit.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -32,7 +33,18 @@ class PromptInputMobileSelectSourcesButton extends StatefulWidget {
class _PromptInputMobileSelectSourcesButtonState
extends State<PromptInputMobileSelectSourcesButton> {
late final cubit = ChatSettingsCubit();
late final cubit = ViewSelectorCubit(
maxSelectedParentPageCount: 3,
getIgnoreViewType: (view) {
if (view.isSpace) {
return IgnoreViewType.none;
}
if (view.layout != ViewLayoutPB.Document) {
return IgnoreViewType.hide;
}
return IgnoreViewType.none;
},
);
@override
void initState() {
@ -91,7 +103,7 @@ class _PromptInputMobileSelectSourcesButtonState
),
onTap: () async {
context
.read<ChatSettingsCubit>()
.read<ViewSelectorCubit>()
.refreshSources(state.spaces, state.currentSpace);
await showMobileBottomSheet<void>(
context,
@ -129,32 +141,17 @@ class _PromptInputMobileSelectSourcesButtonState
}
}
class _MobileSelectSourcesSheetBody extends StatefulWidget {
class _MobileSelectSourcesSheetBody extends StatelessWidget {
const _MobileSelectSourcesSheetBody({
required this.scrollController,
});
final ScrollController scrollController;
@override
State<_MobileSelectSourcesSheetBody> createState() =>
_MobileSelectSourcesSheetBodyState();
}
class _MobileSelectSourcesSheetBodyState
extends State<_MobileSelectSourcesSheetBody> {
final textController = TextEditingController();
@override
void dispose() {
textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomScrollView(
controller: widget.scrollController,
controller: scrollController,
shrinkWrap: true,
slivers: [
SliverPersistentHeader(
@ -183,10 +180,9 @@ class _MobileSelectSourcesSheetBodyState
child: SizedBox(
height: 44.0,
child: FlowySearchTextField(
controller: textController,
onChanged: (value) => context
.read<ChatSettingsCubit>()
.updateFilter(value),
controller: context
.read<ViewSelectorCubit>()
.filterTextController,
),
),
),
@ -196,27 +192,25 @@ class _MobileSelectSourcesSheetBodyState
),
),
),
BlocBuilder<ChatSettingsCubit, ChatSettingsState>(
BlocBuilder<ViewSelectorCubit, ViewSelectorState>(
builder: (context, state) {
final sources = state.visibleSources
.where((e) => e.ignoreStatus != IgnoreViewType.hide);
return SliverList(
delegate: SliverChildBuilderDelegate(
childCount: sources.length,
childCount: state.visibleSources.length,
(context, index) {
final source = sources.elementAt(index);
return ChatSourceTreeItem(
final source = state.visibleSources.elementAt(index);
return ViewSelectorTreeItem(
key: ValueKey(
'visible_select_sources_tree_item_${source.view.id}',
),
chatSource: source,
viewSelectorItem: source,
level: 0,
isDescendentOfSpace: source.view.isSpace,
isSelectedSection: false,
onSelected: (chatSource) {
onSelected: (item) {
context
.read<ChatSettingsCubit>()
.toggleSelectedStatus(chatSource);
.read<ViewSelectorCubit>()
.toggleSelectedStatus(item, false);
},
height: 40.0,
);

View File

@ -2,15 +2,13 @@ import 'dart:math';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -18,6 +16,8 @@ import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../service/view_selector_cubit.dart';
import '../view_selector.dart';
import 'layout_define.dart';
import 'mention_page_menu.dart';
@ -38,7 +38,21 @@ class PromptInputDesktopSelectSourcesButton extends StatefulWidget {
class _PromptInputDesktopSelectSourcesButtonState
extends State<PromptInputDesktopSelectSourcesButton> {
late final cubit = ChatSettingsCubit();
late final cubit = ViewSelectorCubit(
maxSelectedParentPageCount: 3,
getIgnoreViewType: (view) {
if (view.isSpace) {
return IgnoreViewType.none;
}
if (view.layout == ViewLayoutPB.Chat) {
return IgnoreViewType.hide;
}
if (view.layout != ViewLayoutPB.Document) {
return IgnoreViewType.disable;
}
return IgnoreViewType.none;
},
);
final popoverController = PopoverController();
@override
@ -59,23 +73,10 @@ class _PromptInputDesktopSelectSourcesButtonState
@override
Widget build(BuildContext context) {
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
final userProfile = userWorkspaceBloc.state.userProfile;
final workspaceId =
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SpaceBloc(
userProfile: userProfile,
workspaceId: workspaceId,
)..add(const SpaceEvent.initial(openFirstPage: false)),
),
BlocProvider.value(
value: cubit,
),
],
return ViewSelector(
viewSelectorCubit: BlocProvider.value(
value: cubit,
),
child: BlocBuilder<SpaceBloc, SpaceState>(
builder: (context, state) {
return AppFlowyPopover(
@ -86,18 +87,18 @@ class _PromptInputDesktopSelectSourcesButtonState
controller: popoverController,
onOpen: () {
context
.read<ChatSettingsCubit>()
.read<ViewSelectorCubit>()
.refreshSources(state.spaces, state.currentSpace);
},
onClose: () {
widget.onUpdateSelectedSources(cubit.selectedSourceIds);
context
.read<ChatSettingsCubit>()
.read<ViewSelectorCubit>()
.refreshSources(state.spaces, state.currentSpace);
},
popupBuilder: (_) {
return BlocProvider.value(
value: context.read<ChatSettingsCubit>(),
value: context.read<ViewSelectorCubit>(),
child: const _PopoverContent(),
);
},
@ -186,20 +187,26 @@ class _PopoverContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<ChatSettingsCubit, ChatSettingsState>(
return BlocBuilder<ViewSelectorCubit, ViewSelectorState>(
builder: (context, state) {
final theme = AppFlowyTheme.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(8, 12, 8, 8),
child: SpaceSearchField(
width: 600,
onSearch: (context, value) =>
context.read<ChatSettingsCubit>().updateFilter(value),
child: AFTextField(
size: AFTextFieldSize.m,
controller:
context.read<ViewSelectorCubit>().filterTextController,
hintText: LocaleKeys.search_label.tr(),
),
),
_buildDivider(),
AFDivider(
startIndent: theme.spacing.l,
endIndent: theme.spacing.l,
),
Flexible(
child: ListView(
shrinkWrap: true,
@ -208,9 +215,10 @@ class _PopoverContent extends StatelessWidget {
..._buildSelectedSources(context, state),
if (state.selectedSources.isNotEmpty &&
state.visibleSources.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: _buildDivider(),
AFDivider(
spacing: 4.0,
startIndent: theme.spacing.l,
endIndent: theme.spacing.l,
),
..._buildVisibleSources(context, state),
],
@ -222,70 +230,53 @@ class _PopoverContent extends StatelessWidget {
);
}
Widget _buildDivider() {
return const Divider(
height: 1.0,
thickness: 1.0,
indent: 8.0,
endIndent: 8.0,
);
}
Iterable<Widget> _buildSelectedSources(
BuildContext context,
ChatSettingsState state,
ViewSelectorState state,
) {
return state.selectedSources
.where((e) => e.ignoreStatus != IgnoreViewType.hide)
.map(
(e) => ChatSourceTreeItem(
key: ValueKey(
'selected_select_sources_tree_item_${e.view.id}',
),
chatSource: e,
level: 0,
isDescendentOfSpace: e.view.isSpace,
isSelectedSection: true,
onSelected: (chatSource) {
context
.read<ChatSettingsCubit>()
.toggleSelectedStatus(chatSource);
},
height: 30.0,
),
);
return state.selectedSources.map(
(e) => ViewSelectorTreeItem(
key: ValueKey(
'selected_select_sources_tree_item_${e.view.id}',
),
viewSelectorItem: e,
level: 0,
isDescendentOfSpace: e.view.isSpace,
isSelectedSection: true,
onSelected: (item) {
context.read<ViewSelectorCubit>().toggleSelectedStatus(item, true);
},
height: 30.0,
),
);
}
Iterable<Widget> _buildVisibleSources(
BuildContext context,
ChatSettingsState state,
ViewSelectorState state,
) {
return state.visibleSources
.where((e) => e.ignoreStatus != IgnoreViewType.hide)
.map(
(e) => ChatSourceTreeItem(
key: ValueKey(
'visible_select_sources_tree_item_${e.view.id}',
),
chatSource: e,
level: 0,
isDescendentOfSpace: e.view.isSpace,
isSelectedSection: false,
onSelected: (chatSource) {
context
.read<ChatSettingsCubit>()
.toggleSelectedStatus(chatSource);
},
height: 30.0,
),
);
return state.visibleSources.map(
(e) => ViewSelectorTreeItem(
key: ValueKey(
'visible_select_sources_tree_item_${e.view.id}',
),
viewSelectorItem: e,
level: 0,
isDescendentOfSpace: e.view.isSpace,
isSelectedSection: false,
onSelected: (item) {
context.read<ViewSelectorCubit>().toggleSelectedStatus(item, false);
},
height: 30.0,
),
);
}
}
class ChatSourceTreeItem extends StatefulWidget {
const ChatSourceTreeItem({
class ViewSelectorTreeItem extends StatefulWidget {
const ViewSelectorTreeItem({
super.key,
required this.chatSource,
required this.viewSelectorItem,
required this.level,
required this.isDescendentOfSpace,
required this.isSelectedSection,
@ -296,7 +287,7 @@ class ChatSourceTreeItem extends StatefulWidget {
this.showCheckbox = true,
});
final ChatSource chatSource;
final ViewSelectorItem viewSelectorItem;
/// nested level of the view item
final int level;
@ -305,9 +296,9 @@ class ChatSourceTreeItem extends StatefulWidget {
final bool isSelectedSection;
final void Function(ChatSource chatSource) onSelected;
final void Function(ViewSelectorItem viewSelectorItem) onSelected;
final void Function(ChatSource chatSource)? onAdd;
final void Function(ViewSelectorItem viewSelectorItem)? onAdd;
final bool showSaveButton;
@ -316,16 +307,16 @@ class ChatSourceTreeItem extends StatefulWidget {
final bool showCheckbox;
@override
State<ChatSourceTreeItem> createState() => _ChatSourceTreeItemState();
State<ViewSelectorTreeItem> createState() => _ViewSelectorTreeItemState();
}
class _ChatSourceTreeItemState extends State<ChatSourceTreeItem> {
class _ViewSelectorTreeItemState extends State<ViewSelectorTreeItem> {
@override
Widget build(BuildContext context) {
final child = SizedBox(
height: widget.height,
child: ChatSourceTreeItemInner(
chatSource: widget.chatSource,
child: ViewSelectorTreeItemInner(
viewSelectorItem: widget.viewSelectorItem,
level: widget.level,
isDescendentOfSpace: widget.isDescendentOfSpace,
isSelectedSection: widget.isSelectedSection,
@ -336,33 +327,30 @@ class _ChatSourceTreeItemState extends State<ChatSourceTreeItem> {
),
);
final disabledEnabledChild =
widget.chatSource.ignoreStatus == IgnoreViewType.disable
? FlowyTooltip(
message: widget.showCheckbox
? switch (widget.chatSource.view.layout) {
ViewLayoutPB.Document =>
LocaleKeys.chat_sourcesLimitReached.tr(),
_ => LocaleKeys.chat_sourceUnsupported.tr(),
}
: "",
child: Opacity(
opacity: 0.5,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: IgnorePointer(child: child),
),
),
)
: child;
final disabledEnabledChild = widget.viewSelectorItem.isDisabled
? FlowyTooltip(
message: widget.showCheckbox
? switch (widget.viewSelectorItem.view.layout) {
ViewLayoutPB.Document =>
LocaleKeys.chat_sourcesLimitReached.tr(),
_ => LocaleKeys.chat_sourceUnsupported.tr(),
}
: "",
child: Opacity(
opacity: 0.5,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: IgnorePointer(child: child),
),
),
)
: child;
return ValueListenableBuilder(
valueListenable: widget.chatSource.isExpandedNotifier,
valueListenable: widget.viewSelectorItem.isExpandedNotifier,
builder: (context, isExpanded, child) {
// filter the child views that should be ignored
final childViews = widget.chatSource.children
.where((e) => e.ignoreStatus != IgnoreViewType.hide)
.toList();
final childViews = widget.viewSelectorItem.children;
if (!isExpanded || childViews.isEmpty) {
return disabledEnabledChild;
@ -373,11 +361,11 @@ class _ChatSourceTreeItemState extends State<ChatSourceTreeItem> {
children: [
disabledEnabledChild,
...childViews.map(
(childSource) => ChatSourceTreeItem(
(childSource) => ViewSelectorTreeItem(
key: ValueKey(
'select_sources_tree_item_${childSource.view.id}',
),
chatSource: childSource,
viewSelectorItem: childSource,
level: widget.level + 1,
isDescendentOfSpace: widget.isDescendentOfSpace,
isSelectedSection: widget.isSelectedSection,
@ -395,10 +383,10 @@ class _ChatSourceTreeItemState extends State<ChatSourceTreeItem> {
}
}
class ChatSourceTreeItemInner extends StatelessWidget {
const ChatSourceTreeItemInner({
class ViewSelectorTreeItemInner extends StatelessWidget {
const ViewSelectorTreeItemInner({
super.key,
required this.chatSource,
required this.viewSelectorItem,
required this.level,
required this.isDescendentOfSpace,
required this.isSelectedSection,
@ -408,34 +396,27 @@ class ChatSourceTreeItemInner extends StatelessWidget {
this.onAdd,
});
final ChatSource chatSource;
final ViewSelectorItem viewSelectorItem;
final int level;
final bool isDescendentOfSpace;
final bool isSelectedSection;
final bool showCheckbox;
final bool showSaveButton;
final void Function(ChatSource)? onSelected;
final void Function(ChatSource)? onAdd;
final void Function(ViewSelectorItem)? onSelected;
final void Function(ViewSelectorItem)? onAdd;
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
if (!isSelectedSection) {
onSelected?.call(chatSource);
}
},
onTap: () => onSelected?.call(viewSelectorItem),
child: FlowyHover(
cursor: isSelectedSection ? SystemMouseCursors.basic : null,
style: HoverStyle(
hoverColor: isSelectedSection
? Colors.transparent
: AFThemeExtension.of(context).lightGreyHover,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
),
builder: (context, onHover) {
final isSaveButtonVisible =
showSaveButton && !chatSource.view.isSpace;
showSaveButton && !viewSelectorItem.view.isSpace;
final isAddButtonVisible = onAdd != null;
return Row(
children: [
@ -443,26 +424,26 @@ class ChatSourceTreeItemInner extends StatelessWidget {
HSpace(max(20.0 * level - (isDescendentOfSpace ? 2 : 0), 0)),
// builds the >, ^ or · button
ToggleIsExpandedButton(
chatSource: chatSource,
viewSelectorItem: viewSelectorItem,
isSelectedSection: isSelectedSection,
),
const HSpace(2.0),
// checkbox
if (!chatSource.view.isSpace && showCheckbox) ...[
if (!viewSelectorItem.view.isSpace && showCheckbox) ...[
SourceSelectedStatusCheckbox(
chatSource: chatSource,
viewSelectorItem: viewSelectorItem,
),
const HSpace(4.0),
],
// icon
MentionViewIcon(
view: chatSource.view,
view: viewSelectorItem.view,
),
const HSpace(6.0),
// title
Expanded(
child: FlowyText(
chatSource.view.nameOrDefault,
viewSelectorItem.view.nameOrDefault,
overflow: TextOverflow.ellipsis,
fontSize: 14.0,
figmaLineHeight: 18.0,
@ -479,7 +460,7 @@ class ChatSourceTreeItemInner extends StatelessWidget {
size: const Size.square(16),
color: Theme.of(context).hintColor,
),
onPressed: () => onSelected?.call(chatSource),
onPressed: () => onSelected?.call(viewSelectorItem),
),
if (isSaveButtonVisible && isAddButtonVisible)
const HSpace(4.0),
@ -492,7 +473,7 @@ class ChatSourceTreeItemInner extends StatelessWidget {
size: const Size.square(16),
color: Theme.of(context).hintColor,
),
onPressed: () => onAdd?.call(chatSource),
onPressed: () => onAdd?.call(viewSelectorItem),
),
const HSpace(4.0),
],
@ -507,27 +488,30 @@ class ChatSourceTreeItemInner extends StatelessWidget {
class ToggleIsExpandedButton extends StatelessWidget {
const ToggleIsExpandedButton({
super.key,
required this.chatSource,
required this.viewSelectorItem,
required this.isSelectedSection,
});
final ChatSource chatSource;
final ViewSelectorItem viewSelectorItem;
final bool isSelectedSection;
@override
Widget build(BuildContext context) {
if (isReferencedDatabaseView(chatSource.view, chatSource.parentView)) {
if (isReferencedDatabaseView(
viewSelectorItem.view,
viewSelectorItem.parentView,
)) {
return const _DotIconWidget();
}
if (chatSource.children.isEmpty) {
if (viewSelectorItem.children.isEmpty) {
return const SizedBox.square(dimension: 16.0);
}
return FlowyHover(
child: GestureDetector(
child: ValueListenableBuilder(
valueListenable: chatSource.isExpandedNotifier,
valueListenable: viewSelectorItem.isExpandedNotifier,
builder: (context, value, _) => FlowySvg(
value
? FlowySvgs.view_item_expand_s
@ -536,8 +520,8 @@ class ToggleIsExpandedButton extends StatelessWidget {
),
),
onTap: () => context
.read<ChatSettingsCubit>()
.toggleIsExpanded(chatSource, isSelectedSection),
.read<ViewSelectorCubit>()
.toggleIsExpanded(viewSelectorItem, isSelectedSection),
),
);
}
@ -565,20 +549,20 @@ class _DotIconWidget extends StatelessWidget {
class SourceSelectedStatusCheckbox extends StatelessWidget {
const SourceSelectedStatusCheckbox({
super.key,
required this.chatSource,
required this.viewSelectorItem,
});
final ChatSource chatSource;
final ViewSelectorItem viewSelectorItem;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: chatSource.selectedStatusNotifier,
valueListenable: viewSelectorItem.selectedStatusNotifier,
builder: (context, selectedStatus, _) => FlowySvg(
switch (selectedStatus) {
SourceSelectedStatus.unselected => FlowySvgs.uncheck_s,
SourceSelectedStatus.selected => FlowySvgs.check_filled_s,
SourceSelectedStatus.partiallySelected => FlowySvgs.check_partial_s,
ViewSelectedStatus.unselected => FlowySvgs.uncheck_s,
ViewSelectedStatus.selected => FlowySvgs.check_filled_s,
ViewSelectedStatus.partiallySelected => FlowySvgs.check_partial_s,
},
size: const Size.square(18.0),
blendMode: null,

View File

@ -0,0 +1,39 @@
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/user/prelude.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/single_child_widget.dart';
class ViewSelector extends StatelessWidget {
const ViewSelector({
super.key,
required this.viewSelectorCubit,
required this.child,
});
final SingleChildWidget viewSelectorCubit;
final Widget child;
@override
Widget build(BuildContext context) {
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
final userProfile = userWorkspaceBloc.state.userProfile;
final workspaceId =
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) {
return SpaceBloc(
userProfile: userProfile,
workspaceId: workspaceId,
)..add(const SpaceEvent.initial(openFirstPage: false));
},
),
viewSelectorCubit,
],
child: child,
);
}
}

View File

@ -11,5 +11,4 @@ export 'chat_message_service.dart';
export 'chat_message_stream.dart';
export 'chat_notification.dart';
export 'chat_select_message_bloc.dart';
export 'chat_select_sources_cubit.dart';
export 'chat_user_message_bloc.dart';

View File

@ -1,17 +1,18 @@
import 'dart:async';
import 'package:appflowy/ai/ai.dart';
import 'package:appflowy/ai/widgets/view_selector.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_edit_document_service.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_select_message_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/user/prelude.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_result/appflowy_result.dart';
@ -133,23 +134,20 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
@override
Widget build(BuildContext context) {
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
final userProfile = userWorkspaceBloc.state.userProfile;
final workspaceId =
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SpaceBloc(
userProfile: userProfile,
workspaceId: workspaceId,
)..add(const SpaceEvent.initial(openFirstPage: false)),
return ViewSelector(
viewSelectorCubit: BlocProvider(
create: (context) => ViewSelectorCubit(
getIgnoreViewType: (view) {
if (view.isSpace) {
return IgnoreViewType.none;
}
if (view.layout != ViewLayoutPB.Document) {
return IgnoreViewType.hide;
}
return IgnoreViewType.none;
},
),
BlocProvider(
create: (context) => ChatSettingsCubit(hideDisabled: true),
),
],
),
child: BlocSelector<SpaceBloc, SpaceState, ViewPB?>(
selector: (state) => state.currentSpace,
builder: (context, spaceView) {
@ -189,7 +187,7 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
} else {
if (spaceView != null) {
context
.read<ChatSettingsCubit>()
.read<ViewSelectorCubit>()
.refreshSources([spaceView], spaceView);
}
popoverController.show();
@ -210,7 +208,7 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
Widget buildPopover(BuildContext context) {
return BlocProvider.value(
value: context.read<ChatSettingsCubit>(),
value: context.read<ViewSelectorCubit>(),
child: SaveToPagePopoverContent(
onAddToNewPage: (parentViewId) async {
await addMessageToNewPage(context, parentViewId);

View File

@ -2,11 +2,11 @@ import 'dart:async';
import 'dart:convert';
import 'package:appflowy/ai/ai.dart';
import 'package:appflowy/ai/widgets/view_selector.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_edit_document_service.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
@ -15,16 +15,15 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/user/prelude.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -515,23 +514,20 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
@override
Widget build(BuildContext context) {
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
final userProfile = userWorkspaceBloc.state.userProfile;
final workspaceId =
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SpaceBloc(
userProfile: userProfile,
workspaceId: workspaceId,
)..add(const SpaceEvent.initial(openFirstPage: false)),
return ViewSelector(
viewSelectorCubit: BlocProvider(
create: (context) => ViewSelectorCubit(
getIgnoreViewType: (view) {
if (view.isSpace) {
return IgnoreViewType.none;
}
if (view.layout != ViewLayoutPB.Document) {
return IgnoreViewType.hide;
}
return IgnoreViewType.none;
},
),
BlocProvider(
create: (context) => ChatSettingsCubit(hideDisabled: true),
),
],
),
child: BlocSelector<SpaceBloc, SpaceState, ViewPB?>(
selector: (state) => state.currentSpace,
builder: (context, spaceView) {
@ -546,7 +542,7 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
onClose: () {
if (spaceView != null) {
context
.read<ChatSettingsCubit>()
.read<ViewSelectorCubit>()
.refreshSources([spaceView], spaceView);
}
widget.onOverrideVisibility?.call(false);
@ -584,7 +580,7 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
widget.onOverrideVisibility?.call(true);
if (spaceView != null) {
context
.read<ChatSettingsCubit>()
.read<ViewSelectorCubit>()
.refreshSources([spaceView], spaceView);
}
popoverController.show();
@ -596,7 +592,7 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
Widget buildPopover(BuildContext context) {
return BlocProvider.value(
value: context.read<ChatSettingsCubit>(),
value: context.read<ViewSelectorCubit>(),
child: SaveToPagePopoverContent(
onAddToNewPage: (parentViewId) {
addMessageToNewPage(context, parentViewId);
@ -697,8 +693,10 @@ class SaveToPagePopoverContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<ChatSettingsCubit, ChatSettingsState>(
return BlocBuilder<ViewSelectorCubit, ViewSelectorState>(
builder: (context, state) {
final theme = AppFlowyTheme.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -707,22 +705,26 @@ class SaveToPagePopoverContent extends StatelessWidget {
margin: const EdgeInsets.fromLTRB(12, 8, 12, 4),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: FlowyText(
child: Text(
LocaleKeys.chat_addToPageTitle.tr(),
fontSize: 12.0,
color: Theme.of(context).hintColor,
style: theme.textStyle.caption
.standard(color: theme.textColorScheme.secondary),
),
),
),
Padding(
padding: const EdgeInsets.only(left: 12, right: 12, bottom: 8),
child: SpaceSearchField(
width: 600,
onSearch: (context, value) =>
context.read<ChatSettingsCubit>().updateFilter(value),
child: AFTextField(
controller:
context.read<ViewSelectorCubit>().filterTextController,
hintText: LocaleKeys.search_label.tr(),
size: AFTextFieldSize.m,
),
),
_buildDivider(),
AFDivider(
startIndent: theme.spacing.l,
endIndent: theme.spacing.l,
),
Expanded(
child: ListView(
shrinkWrap: true,
@ -736,44 +738,33 @@ class SaveToPagePopoverContent extends StatelessWidget {
);
}
Widget _buildDivider() {
return const Divider(
height: 1.0,
thickness: 1.0,
indent: 12.0,
endIndent: 12.0,
);
}
Iterable<Widget> _buildVisibleSources(
BuildContext context,
ChatSettingsState state,
ViewSelectorState state,
) {
return state.visibleSources
.where((e) => e.ignoreStatus != IgnoreViewType.hide)
.map(
(e) => ChatSourceTreeItem(
key: ValueKey(
'save_to_page_tree_item_${e.view.id}',
),
chatSource: e,
level: 0,
isDescendentOfSpace: e.view.isSpace,
isSelectedSection: false,
showCheckbox: false,
showSaveButton: true,
onSelected: (source) {
if (source.view.isSpace) {
onAddToNewPage(source.view.id);
} else {
onAddToExistingPage(source.view.id);
}
},
onAdd: (source) {
onAddToNewPage(source.view.id);
},
height: 30.0,
),
);
return state.visibleSources.map(
(e) => ViewSelectorTreeItem(
key: ValueKey(
'save_to_page_tree_item_${e.view.id}',
),
viewSelectorItem: e,
level: 0,
isDescendentOfSpace: e.view.isSpace,
isSelectedSection: false,
showCheckbox: false,
showSaveButton: true,
onSelected: (source) {
if (source.view.isSpace) {
onAddToNewPage(source.view.id);
} else {
onAddToExistingPage(source.view.id);
}
},
onAdd: (source) {
onAddToNewPage(source.view.id);
},
height: 30.0,
),
);
}
}