Compare commits

..

9 Commits

Author SHA1 Message Date
eaff7c4a52 Remove unused didUpdateWidget method from SubSpaceDialog 2025-06-16 09:21:36 +03:00
37b21ecdfb Refactor sub-space dialog to use Bloc for state management and simplify confirmation handling 2025-06-15 16:18:42 +03:00
8d408867bb Refactor routine creation logic and add new dropdown events (#254)
<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->



## Description

<!--- Describe your changes in detail -->
fix create new routines dialog 

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-06-15 14:12:54 +03:00
57508fe17e Refactor routine creation logic and add new dropdown events 2025-06-15 13:29:32 +03:00
994e9f4e57 Revert "formatted all files." (#250)
<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Description

Reverted formatting PR.
This reverts commit 04250ebc98.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [x] 🗑️ Chore
2025-06-12 16:09:32 +03:00
c642ba2644 Revert "formatted all files."
This reverts commit 04250ebc98.
2025-06-12 16:04:49 +03:00
218f43bacb Formatting all files (#249)
<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Description

Formatted all files in the repository.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [x] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-06-12 15:40:34 +03:00
04250ebc98 formatted all files. 2025-06-12 15:33:32 +03:00
29959f567e upgrade-flutter-version-in-deployment-actions. (#248)
<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Description

<!--- Describe your changes in detail -->

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [x] 🗑️ Chore
2025-06-12 15:07:12 +03:00
13 changed files with 428 additions and 223 deletions

View File

@ -25,3 +25,8 @@ linter:
prefer_int_literals: false prefer_int_literals: false
sort_constructors_first: false sort_constructors_first: false
avoid_redundant_argument_values: false avoid_redundant_argument_values: false
always_put_required_named_parameters_first: false
unnecessary_breaks: false
avoid_catches_without_on_clauses: false
cascade_invocations: false
overridden_fields: false

View File

@ -1,7 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart';
@ -66,14 +69,25 @@ class DeviceManagementContent extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.all(10.0), padding: const EdgeInsets.all(10.0),
child: InkWell( child: InkWell(
onTap: () { onTap: () async {
showSubSpaceDialog( final selectedSubSpace = await showSubSpaceDialog(
context, context,
communityUuid: device.community!.uuid!, communityUuid: device.community!.uuid!,
spaceUuid: device.spaces!.first.uuid!, spaceUuid: device.spaces!.first.uuid!,
subSpaces: subSpaces, subSpaces: subSpaces,
selected: device.subspace!.uuid, selected: deviceInfo.subspace.uuid,
); );
if (selectedSubSpace != null) {
Future.delayed(const Duration(milliseconds: 500), () {
context.read<SettingDeviceBloc>().add(
SettingBlocAssignRoom(
communityUuid: device.community!.uuid!,
spaceUuid: device.spaces!.first.uuid!,
subSpaceUuid: selectedSubSpace.id ?? '',
),
);
});
}
}, },
child: infoRow( child: infoRow(
label: 'Sub-Space:', label: 'Sub-Space:',

View File

@ -9,13 +9,11 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SubSpaceDialog extends StatefulWidget { class SubSpaceDialog extends StatefulWidget {
final List<SubSpaceModel> subSpaces; final List<SubSpaceModel> subSpaces;
final String? selected; final String? selected;
final void Function(SubSpaceModel?) onConfirmed;
const SubSpaceDialog({ const SubSpaceDialog({
Key? key, Key? key,
required this.subSpaces, required this.subSpaces,
this.selected, this.selected,
required this.onConfirmed,
}) : super(key: key); }) : super(key: key);
@override @override
@ -86,30 +84,21 @@ class _SubSpaceDialogState extends State<SubSpaceDialog> {
} }
} }
void showSubSpaceDialog( Future<SubSpaceModel?> showSubSpaceDialog(
BuildContext context, { BuildContext context, {
required List<SubSpaceModel> subSpaces, required List<SubSpaceModel> subSpaces,
String? selected, String? selected,
required String communityUuid, required String communityUuid,
required String spaceUuid, required String spaceUuid,
}) { }) {
showDialog( return showDialog<SubSpaceModel>(
context: context, context: context,
barrierDismissible: true, builder: (ctx) => BlocProvider.value(
builder: (ctx) => SubSpaceDialog( value: BlocProvider.of<SettingDeviceBloc>(context),
subSpaces: subSpaces, child: SubSpaceDialog(
selected: selected, subSpaces: subSpaces,
onConfirmed: (selectedModel) { selected: selected,
if (selectedModel != null) { ),
context.read<SettingDeviceBloc>().add(
SettingBlocAssignRoom(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
subSpaceUuid: selectedModel.id ?? '',
),
);
}
},
), ),
); );
} }

View File

@ -1,6 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
@ -62,11 +60,12 @@ class SubSpaceDialogButtons extends StatelessWidget {
? null ? null
: () { : () {
final selectedModel = widget.subSpaces.firstWhere( final selectedModel = widget.subSpaces.firstWhere(
(space) => space.id == _selectedId, (space) => space.id == _selectedId,
orElse: () => orElse: () =>
SubSpaceModel(id: null, name: '', devices: [])); SubSpaceModel(id: null, name: '', devices: []),
widget.onConfirmed(selectedModel); );
Navigator.of(context).pop(); Navigator.of(context)
.pop(selectedModel);
}, },
child: Text( child: Text(
'Confirm', 'Confirm',
@ -84,31 +83,3 @@ class SubSpaceDialogButtons extends StatelessWidget {
); );
} }
} }
void showSubSpaceDialog(
BuildContext context, {
required List<SubSpaceModel> subSpaces,
String? selected,
required String communityUuid,
required String spaceUuid,
}) {
showDialog(
context: context,
barrierDismissible: true,
builder: (ctx) => SubSpaceDialog(
subSpaces: subSpaces,
selected: selected,
onConfirmed: (selectedModel) {
if (selectedModel != null) {
context.read<SettingDeviceBloc>().add(
SettingBlocAssignRoom(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
subSpaceUuid: selectedModel.id ?? '',
),
);
}
},
),
);
}

View File

@ -11,7 +11,6 @@ class CreateRoutineBloc extends Bloc<CreateRoutineEvent, CreateRoutineState> {
on<SpaceOnlyWithDevicesEvent>(_fetchSpaceOnlyWithDevices); on<SpaceOnlyWithDevicesEvent>(_fetchSpaceOnlyWithDevices);
on<SaveCommunityIdAndSpaceIdEvent>(saveSpaceIdCommunityId); on<SaveCommunityIdAndSpaceIdEvent>(saveSpaceIdCommunityId);
on<ResetSelectedEvent>(resetSelected); on<ResetSelectedEvent>(resetSelected);
on<FetchCommunityEvent>(_fetchCommunity);
} }
String selectedSpaceId = ''; String selectedSpaceId = '';
@ -50,18 +49,4 @@ class CreateRoutineBloc extends Bloc<CreateRoutineEvent, CreateRoutineState> {
selectedCommunityId = ''; selectedCommunityId = '';
emit(const ResetSelectedState()); emit(const ResetSelectedState());
} }
Future<void> _fetchCommunity(
FetchCommunityEvent event, Emitter<CreateRoutineState> emit) async {
emit(const CommunitiesLoadingState());
try {
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
communities =
await CommunitySpaceManagementApi().fetchCommunities(projectUuid);
emit(const CommunityLoadedState());
} catch (e) {
emit(SpaceTreeErrorState('Error loading communities $e'));
}
}
} }

View File

@ -43,9 +43,3 @@ class ResetSelectedEvent extends CreateRoutineEvent {
} }
class FetchCommunityEvent extends CreateRoutineEvent {
const FetchCommunityEvent();
@override
List<Object> get props => [];
}

View File

@ -1,13 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/create_new_routines/dropdown_menu_content.dart'; import 'package:syncrow_web/pages/routines/create_new_routines/dropdown_menu_content.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_state.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'space_tree_dropdown_bloc.dart'; import 'space_tree_dropdown_bloc.dart';
class SpaceTreeDropdown extends StatefulWidget { class SpaceTreeDropdown extends StatelessWidget {
final String? selectedSpaceId; final String? selectedSpaceId;
final Function(String?)? onChanged; final Function(String?)? onChanged;
@ -18,23 +16,33 @@ class SpaceTreeDropdown extends StatefulWidget {
}); });
@override @override
State<SpaceTreeDropdown> createState() => _SpaceTreeDropdownState(); Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
final bloc = SpaceTreeDropdownBloc(selectedSpaceId);
bloc.add(FetchSpacesEvent());
return bloc;
},
child: _DropdownContent(onChanged: onChanged),
);
}
} }
class _SpaceTreeDropdownState extends State<SpaceTreeDropdown> { class _DropdownContent extends StatefulWidget {
late SpaceTreeDropdownBloc _dropdownBloc; final Function(String?)? onChanged;
const _DropdownContent({this.onChanged});
@override
State<_DropdownContent> createState() => _DropdownContentState();
}
class _DropdownContentState extends State<_DropdownContent> {
final LayerLink _layerLink = LayerLink(); final LayerLink _layerLink = LayerLink();
OverlayEntry? _overlayEntry; OverlayEntry? _overlayEntry;
@override
void initState() {
super.initState();
_dropdownBloc = SpaceTreeDropdownBloc(widget.selectedSpaceId);
}
@override @override
void dispose() { void dispose() {
_dropdownBloc.close();
_removeOverlay(); _removeOverlay();
super.dispose(); super.dispose();
} }
@ -46,100 +54,120 @@ class _SpaceTreeDropdownState extends State<SpaceTreeDropdown> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider.value( return Column(
value: _dropdownBloc, crossAxisAlignment: CrossAxisAlignment.start,
child: BlocBuilder<SpaceTreeBloc, SpaceTreeState>( mainAxisAlignment: MainAxisAlignment.start,
builder: (context, spaceTreeState) { children: [
final communities = spaceTreeState.searchQuery.isNotEmpty Padding(
? spaceTreeState.filteredCommunity padding: const EdgeInsets.symmetric(horizontal: 10),
: spaceTreeState.communityList; child: Text(
"Community",
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w400,
fontSize: 13,
color: ColorsManager.blackColor,
),
),
),
CompositedTransformTarget(
link: _layerLink,
child: GestureDetector(
onTap: () => _toggleDropdown(context),
child: BlocBuilder<SpaceTreeDropdownBloc, SpaceTreeDropdownState>(
builder: (context, state) {
return _buildDropdownTrigger(state);
},
),
),
),
],
);
}
return BlocBuilder<SpaceTreeDropdownBloc, SpaceTreeDropdownState>( Widget _buildDropdownTrigger(SpaceTreeDropdownState state) {
builder: (context, dropdownState) { if (state.status == SpaceTreeDropdownStatus.loading) {
final selectedCommunity = _findCommunity( return Container(
communities, height: 46,
dropdownState.selectedSpaceId, decoration: BoxDecoration(
); border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.symmetric(horizontal: 10),
child: const Center(child: CircularProgressIndicator()),
);
}
return Column( if (state.status == SpaceTreeDropdownStatus.failure) {
crossAxisAlignment: CrossAxisAlignment.start, return Container(
mainAxisAlignment: MainAxisAlignment.start, height: 46,
children: [ decoration: BoxDecoration(
Padding( border: Border.all(color: Colors.grey.shade300),
padding: const EdgeInsets.symmetric(horizontal: 10), borderRadius: BorderRadius.circular(12),
child: Text( ),
"Community", margin: const EdgeInsets.symmetric(horizontal: 10),
style: Theme.of(context).textTheme.bodyMedium!.copyWith( child: Center(
fontWeight: FontWeight.w400, child: Text(
fontSize: 13, 'Error: ${state.errorMessage}',
color: ColorsManager.blackColor, style: const TextStyle(color: Colors.red),
), ),
), ),
), );
CompositedTransformTarget( }
link: _layerLink,
child: GestureDetector( final selectedCommunity = _findCommunity(state, state.selectedSpaceId);
onTap: () => _toggleDropdown(context, communities),
child: Container( return Container(
height: 46, height: 46,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300), border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
margin: const EdgeInsets.symmetric(horizontal: 10), margin: const EdgeInsets.symmetric(horizontal: 10),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Padding( Padding(
padding: padding: const EdgeInsets.symmetric(horizontal: 10),
const EdgeInsets.symmetric(horizontal: 10), child: Text(
child: Text( selectedCommunity?.name ?? 'Please Select',
selectedCommunity?.name ?? 'Please Select', style: TextStyle(
style: TextStyle( color: selectedCommunity != null
color: selectedCommunity != null ? ColorsManager.blackColor
? ColorsManager.blackColor : ColorsManager.textGray,
: ColorsManager.textGray, overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis, fontWeight: FontWeight.w400,
fontWeight: FontWeight.w400, fontSize: 13,
fontSize: 13, ),
), ),
), ),
), Container(
Container( decoration: BoxDecoration(
decoration: BoxDecoration( color: Colors.grey[200],
color: Colors.grey[200], borderRadius: const BorderRadius.only(
borderRadius: const BorderRadius.only( topRight: Radius.circular(10),
topRight: Radius.circular(10), bottomRight: Radius.circular(10),
bottomRight: Radius.circular(10), ),
), ),
), height: 45,
height: 45, width: 33,
width: 33, child: const Icon(
child: const Icon( Icons.keyboard_arrow_down,
Icons.keyboard_arrow_down, color: ColorsManager.textGray,
color: ColorsManager.textGray, ),
), ),
), ],
],
),
),
),
),
],
);
},
);
},
), ),
); );
} }
void _toggleDropdown(BuildContext context, List<CommunityModel> communities) { void _toggleDropdown(BuildContext context) {
if (_overlayEntry != null) { if (_overlayEntry != null) {
_removeOverlay(); _removeOverlay();
return; return;
} }
final bloc = context.read<SpaceTreeDropdownBloc>();
_overlayEntry = OverlayEntry( _overlayEntry = OverlayEntry(
builder: (context) => Positioned( builder: (context) => Positioned(
width: 300, width: 300,
@ -148,18 +176,22 @@ class _SpaceTreeDropdownState extends State<SpaceTreeDropdown> {
showWhenUnlinked: false, showWhenUnlinked: false,
offset: const Offset(0, 48), offset: const Offset(0, 48),
child: Material( child: Material(
color: ColorsManager.whiteColors,
elevation: 8, elevation: 8,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: DropdownMenuContent( child: BlocProvider.value(
selectedSpaceId: _dropdownBloc.state.selectedSpaceId, value: bloc,
onChanged: (id) { child: DropdownMenuContent(
if (id != null && mounted) { selectedSpaceId: bloc.state.selectedSpaceId,
_dropdownBloc.add(SpaceTreeDropdownSelectEvent(id)); onChanged: (id) {
widget.onChanged?.call(id); if (id != null && mounted) {
_removeOverlay(); bloc.add(SpaceTreeDropdownSelectEvent(id));
} widget.onChanged?.call(id);
}, _removeOverlay();
onClose: _removeOverlay, }
},
onClose: _removeOverlay,
),
), ),
), ),
), ),
@ -170,10 +202,13 @@ class _SpaceTreeDropdownState extends State<SpaceTreeDropdown> {
} }
CommunityModel? _findCommunity( CommunityModel? _findCommunity(
List<CommunityModel> communities, String? communityId) { SpaceTreeDropdownState state, String? communityId) {
if (communityId == null) return null; if (communityId == null) return null;
try { try {
return communities.firstWhere((c) => c.uuid == communityId); return state.filteredCommunities.firstWhere((c) => c.uuid == communityId);
} catch (_) {}
try {
return state.communities.firstWhere((c) => c.uuid == communityId);
} catch (e) { } catch (e) {
return null; return null;
} }

View File

@ -23,8 +23,7 @@ class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (BuildContext context) => create: (BuildContext context) => CreateRoutineBloc(),
CreateRoutineBloc()..add(const FetchCommunityEvent()),
child: BlocBuilder<CreateRoutineBloc, CreateRoutineState>( child: BlocBuilder<CreateRoutineBloc, CreateRoutineState>(
builder: (context, state) { builder: (context, state) {
final _bloc = BlocProvider.of<CreateRoutineBloc>(context); final _bloc = BlocProvider.of<CreateRoutineBloc>(context);

View File

@ -1,12 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; import 'space_tree_dropdown_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_state.dart';
class DropdownMenuContent extends StatefulWidget { class DropdownMenuContent extends StatefulWidget {
final String? selectedSpaceId; final String? selectedSpaceId;
@ -14,6 +9,7 @@ class DropdownMenuContent extends StatefulWidget {
final VoidCallback onClose; final VoidCallback onClose;
const DropdownMenuContent({ const DropdownMenuContent({
super.key,
required this.selectedSpaceId, required this.selectedSpaceId,
required this.onChanged, required this.onChanged,
required this.onClose, required this.onClose,
@ -26,6 +22,7 @@ class DropdownMenuContent extends StatefulWidget {
class _DropdownMenuContentState extends State<DropdownMenuContent> { class _DropdownMenuContentState extends State<DropdownMenuContent> {
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
Timer? _debounceTimer;
@override @override
void initState() { void initState() {
@ -35,43 +32,49 @@ class _DropdownMenuContentState extends State<DropdownMenuContent> {
@override @override
void dispose() { void dispose() {
_debounceTimer?.cancel();
_scrollController.dispose(); _scrollController.dispose();
_searchController.dispose(); _searchController.dispose();
super.dispose(); super.dispose();
} }
void _onScroll() { void _onScroll() {
final bloc = context.read<SpaceTreeBloc>(); final bloc = context.read<SpaceTreeDropdownBloc>();
final state = bloc.state; final state = bloc.state;
if (_scrollController.position.pixels >= if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 30) { _scrollController.position.maxScrollExtent - 30) {
if (state is SpaceTreeState && !state.paginationIsLoading) { if (state.paginationModel?.hasNext == true &&
bloc.add(PaginationEvent(state.paginationModel, state.communityList)); !state.paginationIsLoading) {
bloc.add(PaginationEvent());
} }
} }
} }
void _handleSearch(String query) {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 500), () {
context.read<SpaceTreeDropdownBloc>().add(SearchQueryEvent(query));
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ConstrainedBox( return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300), constraints: const BoxConstraints(maxHeight: 300),
child: BlocBuilder<SpaceTreeBloc, SpaceTreeState>( child: BlocBuilder<SpaceTreeDropdownBloc, SpaceTreeDropdownState>(
builder: (context, state) { builder: (context, state) {
final communities = state.searchQuery.isNotEmpty final communities = state.searchQuery.isNotEmpty
? state.filteredCommunity ? state.filteredCommunities
: state.communityList; : state.communities;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Search bar
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: TextFormField( child: TextFormField(
controller: _searchController, controller: _searchController,
onChanged: (query) { onChanged: _handleSearch,
context.read<SpaceTreeBloc>().add(SearchQueryEvent(query));
},
style: const TextStyle(fontSize: 14, color: Colors.black), style: const TextStyle(fontSize: 14, color: Colors.black),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Search for space...', hintText: 'Search for space...',
@ -85,7 +88,6 @@ class _DropdownMenuContentState extends State<DropdownMenuContent> {
), ),
), ),
), ),
// Community list
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
controller: _scrollController, controller: _scrollController,
@ -121,19 +123,12 @@ class _DropdownMenuContentState extends State<DropdownMenuContent> {
), ),
), ),
onTap: () { onTap: () {
setState(() { context
_searchController.clear(); .read<SpaceTreeDropdownBloc>()
_searchController.text.isEmpty .add(SearchQueryEvent(''));
? context
.read<SpaceTreeBloc>()
.add(SearchQueryEvent(''))
: context.read<SpaceTreeBloc>().add(
SearchQueryEvent(_searchController.text));
});
// Future.delayed(const Duration(seconds: 1), () {
widget.onChanged(community.uuid); widget.onChanged(community.uuid);
widget.onClose(); widget.onClose();
// });
}, },
); );
}, },

View File

@ -1,5 +1,9 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_tree/model/pagination_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/services/space_mana_api.dart';
part 'space_tree_dropdown_event.dart'; part 'space_tree_dropdown_event.dart';
part 'space_tree_dropdown_state.dart'; part 'space_tree_dropdown_state.dart';
@ -9,19 +13,158 @@ class SpaceTreeDropdownBloc
: super(SpaceTreeDropdownState(selectedSpaceId: initialId)) { : super(SpaceTreeDropdownState(selectedSpaceId: initialId)) {
on<SpaceTreeDropdownSelectEvent>(_onSelect); on<SpaceTreeDropdownSelectEvent>(_onSelect);
on<SpaceTreeDropdownResetEvent>(_onReset); on<SpaceTreeDropdownResetEvent>(_onReset);
on<FetchSpacesEvent>(_fetchSpaces);
on<SearchQueryEvent>(_onSearch);
on<PaginationEvent>(_onPagination);
on<DebouncedSearchEvent>(_onDebouncedSearch);
} }
Timer? _debounceTimer;
void _onSelect( void _onSelect(
SpaceTreeDropdownSelectEvent event, SpaceTreeDropdownSelectEvent event,
Emitter<SpaceTreeDropdownState> emit, Emitter<SpaceTreeDropdownState> emit,
) { ) {
emit(SpaceTreeDropdownState(selectedSpaceId: event.spaceId)); final exists = state.communities.any((c) => c.uuid == event.spaceId);
if (!exists) {
final community = state.filteredCommunities.firstWhere(
(c) => c.uuid == event.spaceId,
orElse: () => CommunityModel(
uuid: event.spaceId!,
name: 'Loading...',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
spaces: [],
description: ''),
);
emit(state.copyWith(
selectedSpaceId: event.spaceId,
communities: [...state.communities, community],
));
} else {
emit(state.copyWith(selectedSpaceId: event.spaceId));
}
} }
void _onReset( void _onReset(
SpaceTreeDropdownResetEvent event, SpaceTreeDropdownResetEvent event,
Emitter<SpaceTreeDropdownState> emit, Emitter<SpaceTreeDropdownState> emit,
) { ) {
emit(SpaceTreeDropdownState(selectedSpaceId: event.initialId)); emit(state.copyWith(selectedSpaceId: event.initialId));
}
Future<void> _fetchSpaces(
FetchSpacesEvent event,
Emitter<SpaceTreeDropdownState> emit,
) async {
if (state.status != SpaceTreeDropdownStatus.initial) return;
emit(state.copyWith(status: SpaceTreeDropdownStatus.loading));
try {
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
final paginationModel = await CommunitySpaceManagementApi()
.fetchCommunitiesAndSpaces(projectId: projectUuid, page: 1);
emit(state.copyWith(
status: SpaceTreeDropdownStatus.success,
communities: paginationModel.communities,
filteredCommunities: paginationModel.communities,
paginationModel: paginationModel,
));
} catch (e) {
emit(state.copyWith(
status: SpaceTreeDropdownStatus.failure,
errorMessage: 'Error loading communities: $e',
));
}
}
void _onSearch(
SearchQueryEvent event,
Emitter<SpaceTreeDropdownState> emit,
) {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(seconds: 1), () {
add(DebouncedSearchEvent(event.searchQuery));
});
}
Future<void> _onDebouncedSearch(
DebouncedSearchEvent event,
Emitter<SpaceTreeDropdownState> emit,
) async {
emit(state.copyWith(isSearching: true));
try {
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
final paginationModel =
await CommunitySpaceManagementApi().fetchCommunitiesAndSpaces(
projectId: projectUuid,
page: 1,
search: event.searchQuery,
);
emit(state.copyWith(
filteredCommunities: paginationModel.communities,
isSearching: false,
searchQuery: event.searchQuery,
paginationModel: paginationModel,
));
} catch (e) {
emit(state.copyWith(
isSearching: false,
errorMessage: 'Error searching communities: $e',
));
}
}
@override
Future<void> close() {
_debounceTimer?.cancel();
return super.close();
}
Future<void> _onPagination(
PaginationEvent event,
Emitter<SpaceTreeDropdownState> emit,
) async {
if (state.paginationIsLoading || state.paginationModel?.hasNext != true) {
return;
}
emit(state.copyWith(paginationIsLoading: true));
try {
final nextPage = state.paginationModel!.pageNum;
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
final newPagination = await CommunitySpaceManagementApi()
.fetchCommunitiesAndSpaces(projectId: projectUuid, page: nextPage);
final combinedCommunities = [
...state.communities,
...newPagination.communities
];
List<CommunityModel> filteredCommunities;
if (state.searchQuery.isNotEmpty) {
final query = state.searchQuery.toLowerCase();
filteredCommunities = combinedCommunities.where((community) {
return community.name.toLowerCase().contains(query);
}).toList();
} else {
filteredCommunities = combinedCommunities;
}
emit(state.copyWith(
communities: combinedCommunities,
filteredCommunities: filteredCommunities,
paginationModel: newPagination,
paginationIsLoading: false,
));
} catch (e) {
emit(state.copyWith(
paginationIsLoading: false,
errorMessage: 'Error loading more communities: $e',
));
}
} }
} }

View File

@ -13,3 +13,19 @@ class SpaceTreeDropdownResetEvent extends SpaceTreeDropdownEvent {
SpaceTreeDropdownResetEvent(this.initialId); SpaceTreeDropdownResetEvent(this.initialId);
} }
class FetchSpacesEvent extends SpaceTreeDropdownEvent {}
class SearchQueryEvent extends SpaceTreeDropdownEvent {
final String searchQuery;
SearchQueryEvent(this.searchQuery);
}
class DebouncedSearchEvent extends SpaceTreeDropdownEvent {
final String searchQuery;
DebouncedSearchEvent(this.searchQuery);
}
class PaginationEvent extends SpaceTreeDropdownEvent {}

View File

@ -1,7 +1,51 @@
part of 'space_tree_dropdown_bloc.dart'; part of 'space_tree_dropdown_bloc.dart';
enum SpaceTreeDropdownStatus { initial, loading, success, failure }
class SpaceTreeDropdownState { class SpaceTreeDropdownState {
final String? selectedSpaceId; final String? selectedSpaceId;
final List<CommunityModel> communities;
final List<CommunityModel> filteredCommunities;
final SpaceTreeDropdownStatus status;
final String? errorMessage;
final String searchQuery;
final bool paginationIsLoading;
final PaginationModel? paginationModel;
final bool isSearching;
SpaceTreeDropdownState({this.selectedSpaceId}); SpaceTreeDropdownState({
this.selectedSpaceId,
this.communities = const [],
this.filteredCommunities = const [],
this.status = SpaceTreeDropdownStatus.initial,
this.errorMessage,
this.searchQuery = '',
this.paginationIsLoading = false,
this.paginationModel,
this.isSearching = false,
});
SpaceTreeDropdownState copyWith({
String? selectedSpaceId,
List<CommunityModel>? communities,
List<CommunityModel>? filteredCommunities,
SpaceTreeDropdownStatus? status,
String? errorMessage,
String? searchQuery,
bool? paginationIsLoading,
PaginationModel? paginationModel,
bool? isSearching,
}) {
return SpaceTreeDropdownState(
selectedSpaceId: selectedSpaceId ?? this.selectedSpaceId,
communities: communities ?? this.communities,
filteredCommunities: filteredCommunities ?? this.filteredCommunities,
status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage,
searchQuery: searchQuery ?? this.searchQuery,
paginationIsLoading: paginationIsLoading ?? this.paginationIsLoading,
paginationModel: paginationModel ?? this.paginationModel,
isSearching: isSearching ?? this.isSearching,
);
}
} }

View File

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_